SQL存储过程中的动态排序


126

我过去花了数小时研究这个问题。在我看来,现代RDBMS解决方案应该解决此问题,但是到目前为止,我还没有发现任何能够真正解决我认为在任何具有数据库后端的Web或Windows应用程序中非常普遍的需求的东西。

我说的是动态排序。在我的幻想世界中,它应该像下面这样简单:

ORDER BY @sortCol1, @sortCol2

这是新手SQL和存储过程开发人员在整个Internet论坛上给出的规范示例。“为什么这不可能?” 他们问。最终,总是有人总会向他们讲授存储过程的编译性质,一般的执行计划以及无法将参数直接放入ORDER BY子句的各种其他原因。


我知道你们中有些人已经在想:“那么,让客户来进行分类。” 自然,这会减轻数据库的工作量。在我们的案例中,我们的数据库服务器甚至在99%的时间内都不会汗水,甚至还不是多核的,也不是每6个月进行一次其他的系统架构改进。仅出于这个原因,让我们的数据库处理排序将不是问题。此外,数据库非常擅长分类。他们为此进行了优化,并且已经花了好几年的时间来实现它,其实现语言令人难以置信的灵活,直观和简单,最重要的是,任何初学者SQL编写者都知道如何做,更重要的是,他们知道如何进行编辑,进行更改,进行维护等。当您的数据库需要负担很多费用而您只想简化(并缩短!)开发时间时,这似乎是一个显而易见的选择。

然后是网络问题。我一直在使用JavaScript进行HTML表格的客户端排序,但是它们不可避免地不够灵活,无法满足我的需求,而且,由于我的数据库没有负担过多,并且可以非常轻松地进行排序,因此我很难证明重新编写或自己拥有JavaScript分选器所需的时间。服务器端排序通常也是如此,尽管它可能已经比JavaScript更受青睐。我不是特别喜欢DataSet开销的人,所以起诉我。

但这又回到了不可能的点,或者说不容易。对于以前的系统,我已经完成了一种获得动态排序的骇人听闻的破解方法。它不是很漂亮,也不是直观,简单或灵活的,初学者SQL编写器将在几秒钟内丢失。看起来这已经不是“解决方案”,而是“复杂性”。


以下示例无意提供任何种类的最佳实践或良好的编码风格,也不表示我作为T-SQL程序员的能力。它们就是它们的样子,我完全承认它们是令人困惑的,糟糕的形式,而且仅仅是简单的破解。

我们将一个整数值作为参数传递给存储过程(我们称该参数为“ sort”),然后从中确定一堆其他变量。例如...假设sort为1(或默认值):

DECLARE @sortCol1 AS varchar(20)
DECLARE @sortCol2 AS varchar(20)
DECLARE @dir1 AS varchar(20)
DECLARE @dir2 AS varchar(20)
DECLARE @col1 AS varchar(20)
DECLARE @col2 AS varchar(20)

SET @col1 = 'storagedatetime';
SET @col2 = 'vehicleid';

IF @sort = 1                -- Default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'asc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'asc';
END
ELSE IF @sort = 2           -- Reversed order default sort.
BEGIN
    SET @sortCol1 = @col1;
    SET @dir1 = 'desc';
    SET @sortCol2 = @col2;
    SET @dir2 = 'desc';
END

您已经知道如果我声明更多的@colX变量来定义其他列,我真的可以根据“ sort”的值对要进行排序的列进行创意...使用它,它通常最终看起来像下面的样子令人难以置信的凌乱条款:

ORDER BY
    CASE @dir1
        WHEN 'desc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir1
        WHEN 'asc' THEN
            CASE @sortCol1
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END,
    CASE @dir2
        WHEN 'desc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END DESC,
    CASE @dir2
        WHEN 'asc' THEN
            CASE @sortCol2
                WHEN @col1 THEN [storagedatetime]
                WHEN @col2 THEN [vehicleid]
            END
    END

显然,这是一个非常简单的例子。真正的东西,因为我们通常有4到5列支持排序,每列除此以外还可能有第二列或什至是第三列(例如,日期递减,然后按名称升序进行第二排序),并且每一个都支持双向定向排序可有效地将案件数量加倍。是的...真的很快就长了毛。

这种想法是,人们可以“轻松”地更改排序条件,以便在存储日期时间之前对车辆ID进行排序……但是至少在此简单示例中,伪柔韧性才真正到此结束。本质上,每个未通过测试的案例(因为这次我们的sort方法都不适用)会呈现NULL值。因此,您最终得到了一个功能如下的子句:

ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah

你明白了。之所以起作用,是因为SQL Server有效地忽略了by by子句中的空值。这很难维护,因为任何具有SQL基本工作知识的人都可以看到。如果我失去了你们中的任何一个,不要难过。我们花了很长时间才开始使用它,但仍然很难对它进行编辑或创建类似它的新东西。值得庆幸的是,它不需要经常进行更改,否则它将很快变得“不值得麻烦”。

然而它确实起作用了。


我的问题是: 还有更好的方法吗?

除了存储过程解决方案外,我还可以解决其他问题,因为我意识到这可能不是解决之道。最好是,我想知道是否有人可以在存储过程中做得更好,但是如果不能,那么大家如何处理如何让用户使用ASP.NET对数据表进行动态排序(也双向)?

感谢您阅读(或至少浏览)这么长的问题!

PS:很高兴我没有展示存储过程的示例,该示例支持动态排序,动态过滤/列文本搜索,通过ROWNUMBER()OVER进行分页,尝试...捕获错误时的事务回滚... “庞然大物”甚至没有开始描述它们。


更新:

  • 我想避免使用动态SQL。将字符串解析在一起并在其上运行EXEC会破坏许多首先具有存储过程的目的。有时候我想知道,这样做的弊端是否值得,至少在这些特殊的动态排序情况下是不值得的。尽管如此,每当执行这样的动态SQL字符串时,我总是感到肮脏-就像我仍然生活在Classic ASP世界中一样。
  • 我们首先要存储过程的许多原因是出于安全性考虑。我不会打电话给安全问题,只建议解决方案。使用SQL Server 2005,我们可以在架构级别上对单个存储过程设置权限(如果需要,可以基于每个用户),然后直接拒绝对表的任何查询。批评这种方法的优缺点也许是另一个问题,但这并不是我的决定。我只是主要代码猴子。:)

也请参考stackoverflow.com/questions/3659981/…–具有混合数据类型的SQL Server动态ORDER BY
LCJ 2013年

动态SQL是FAR的高级方法... IF [这是一个很大的IF] ..您的数据访问层很严格,而动态SQL是由一个系统生成的,该系统使用以完美形式表示的RDBMS规则进行严格编程。通过算法设计的数据库体系结构
真是

Answers:


97

是的,这很痛苦,您的操作方式与我的操作类似:

order by
case when @SortExpr = 'CustomerName' and @SortDir = 'ASC' 
    then CustomerName end asc, 
case when @SortExpr = 'CustomerName' and @SortDir = 'DESC' 
    then CustomerName end desc,
...

对我来说,这仍然比从代码构建动态SQL更好,后者变成了DBA的可伸缩性和维护噩梦。

我从代码中所做的是重构分页和排序,因此至少在其中没有很多重复,并且填充了@SortExpr和的值@SortDir

就SQL而言,在不同的存储过程之间保持设计和格式相同,因此,当您进行更改时,它至少是整洁且可识别的。


1
究竟。我的目标是避免对大型5000 varchar字符串执行EXEC命令。如果仅为了增加安全性,我们所做的所有事情都必须通过存储过程来完成,因为我们可以在架构级别上对其设置权限。在我们的案例中,可伸缩性和性能提升只是一个优点。
肖恩·汉利

1
为{安全性,可伸缩性,性能}增加可维护性。一旦有3或4个应用程序在数据库上运行了动态SQL,就无所适从,尤其是随着应用程序的老化和开发人员的不断发展,您将无法进行任何更改。Exec和动态sql是邪恶的。
Eric Z Beard

就是这样---从我到达这里开始,我们就已经为所有仍在运行的Classic ASP Web应用程序和许多仍在流通的Access VB应用程序做过。我抽搐了一下,必须在任何时候都要对任何一个进行维护时,都抑制住纠正明显错误的冲动。
肖恩·汉利

1
这也是我的工作,除了我将方向编码到SortExpr中:当sort ='FirstName'THEN FirstName END ASC时按ORDER BY CASE,当sort ='-FirstName'THEN FirstName END DESC时的案例
Michael Bray

这是DBA和软件工程师的噩梦。因此,除了具有动态的但严格的系统来根据您的信息模式生成表达性的SQL语句之外,您还有这种令人讨厌的硬编码乱码。最好的情况是糟糕的编程。
hajikelist,2015年

23

这种方法可防止将可排序的列按顺序重复两次,并且IMO更具可读性:

SELECT
  s.*
FROM
  (SELECT
    CASE @SortCol1
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol1,
    CASE @SortCol2
      WHEN 'Foo' THEN t.Foo
      WHEN 'Bar' THEN t.Bar
      ELSE null
    END as SortCol2,
    t.*
  FROM
    MyTable t) as s
ORDER BY
  CASE WHEN @dir1 = 'ASC'  THEN SortCol1 END ASC,
  CASE WHEN @dir1 = 'DESC' THEN SortCol1 END DESC,
  CASE WHEN @dir2 = 'ASC'  THEN SortCol2 END ASC,
  CASE WHEN @dir2 = 'DESC' THEN SortCol2 END DESC

这似乎是一个很好的答案,但当可排序的列具有不同的数据类型时似乎不起作用
SlimSim


6

我的应用程序经常执行此操作,但是它们都是动态构建SQL。但是,当我处理存储过程时,我这样做:

  1. 使存储过程成为一个返回值表的函数-不排序。
  2. 然后在您的应用程序代码中执行a,select * from dbo.fn_myData() where ... order by ...以便您可以在那里动态指定排序顺序。

然后,至少动态部分在您的应用程序中,但是数据库仍在繁重的工作中。


1
这可能是我在动态SQL和存储过程一起使用时遇到的最好的折衷方案。我喜欢。我可能会在某个时候尝试使用类似的方法,但是这种更改在我们现有的任何正在进行的项目中都是禁止的。
肖恩·汉利

1
您可以使用局部表变量而不是表格函数返回数据来实现相同目的。我发现本地表比函数更灵活,因为您可以输出一些调试信息。
2011年

5

我用来避免某些作业使用动态SQL的存储过程技术(hack?)是具有唯一的排序列。即

SELECT
   name_last,
   name_first,
   CASE @sortCol WHEN 'name_last' THEN [name_last] ELSE 0 END as mySort
FROM
   table
ORDER BY 
    mySort

这很容易提交-您可以在mySort列中合并字段,使用数学或日期函数反转顺序等。

不过,最好是,我将asp.net gridviews或其他对象与内置排序一起使用,以便在从Sql-Server检索数据之后为我进行排序。或者即使它不是内置的-例如,asp.net中的数据表等。


4

您可以通过几种不同的方式来破解它。

先决条件:

  1. sp中只有一个SELECT语句
  2. 不进行任何排序(或使用默认设置)

然后插入到临时表中:

create table #temp ( your columns )

insert #temp
exec foobar

select * from #temp order by whatever

方法2:将链接服务器设置回自身,然后使用openquery从中选择:http : //www.sommarskog.se/share_data.html#OPENQUERY


4

可能还有第三种选择,因为您的服务器有很多空闲周期-使用帮助程序通过临时表进行排序。就像是

create procedure uspCallAndSort
(
    @sql varchar(2048),        --exec dbo.uspSomeProcedure arg1,'arg2',etc.
    @sortClause varchar(512)    --comma-delimited field list
)
AS
insert into #tmp EXEC(@sql)
declare @msql varchar(3000)
set @msql = 'select * from #tmp order by ' + @sortClause
EXEC(@msql)
drop table #tmp
GO

警告:我还没有测试过,但是它在SQL Server 2005中“应该”工作(它将从结果集中创建一个临时表而无需事先指定列。)


2

在某个时候,离开存储过程而仅仅使用参数化查询来避免这种黑客行为是否值得?


1
在某些情况下,它们可能是大锤,但是我们经常想直接在存储过程上设置权限(尤其是EXECUTE),并禁止直接对表(甚至是SELECT)进行任何SQL查询。我也不太喜欢骇客,但安全并不是我的要求。
肖恩·汉利

1
这就是为什么这么多人转向对象关系映射的原因。不必要的往返行程,相同的巨大CASE块,实际上仅需要更新的情况下对无数列的无意义更新,等等。对于存储过程而言,仍然存在的一个获胜论点是安全性。
匹兹堡DBA

我正在从ORM(EF)迁移到存储过程,因为ORM不支持全文搜索。
罗尼·奥弗比

@RonnieOverby全文搜索通常可以通过专用解决方案(例如Lucene)得到更好的服务。
汉克·盖伊

@HankGay我感到奇怪的是,实体框架也不支持Lucene。
罗尼·奥弗比

2

我同意,请使用客户端。但这似乎不是您想要听到的答案。

因此,它是完美的方式。我不知道您为什么要更改它,甚至不问“有没有更好的方法”。确实,它应该称为“方式”。此外,它似乎可以很好地满足项目的需求,并且可能在未来几年内具有足够的可扩展性。由于您的数据库无需收税,而且排序确实非常容易,因此应该在未来几年保持这种状态。

我不会流汗。


我在使用Windows应用程序时没有遇到客户端问题。但是网络应用程序呢?我发现没有什么JavaScript解决方案足够灵活。是的,它确实像我们所说的那样起作用,但这是SQL的噩梦。我当然想知道是否有更好的方法。
肖恩·汉利

它内置于更新的(2.0及更高版本).NET控件中。或者,您可以创建自己的并将其应用于数据视图。 msdn.microsoft.com/zh-CN/library/hwf94875(VS.80).aspx
DS

2
那么我的问题是可伸缩性和性能之一。进行客户端或Web服务器端排序需要加载所有数据,而不是一次加载要在页面上显示的10或15个数据。从长远来看,这是非常昂贵的,而数据库排序则没有。
肖恩·汉利

2

分页排序结果时,动态SQL是一个不错的选择。如果您对SQL注入抱有偏执,可以使用列号代替列名。在使用负值进行降序之前,我已经完成了此操作。像这样

declare @o int;
set @o = -1;

declare @sql nvarchar(2000);
set @sql = N'select * from table order by ' + 
    cast(abs(@o) as varchar) + case when @o < 0 then ' desc' else ' asc' end + ';'

exec sp_executesql @sql

然后,您只需要确保数字在1到#列之内即可。您甚至可以将其扩展为一个列号列表,并使用类似这样的函数将其解析为一个整数表。然后,您将像这样构建order by子句...

declare @cols varchar(100);
set @cols = '1 -2 3 6';

declare @order_by varchar(200)

select @order_by = isnull(@order_by + ', ', '') + 
        cast(abs(number) as varchar) + 
        case when number < 0 then ' desc' else '' end
from dbo.iter_intlist_to_tbl(@cols) order by listpos

print @order_by

一个缺点是您必须记住客户端上每个列的顺序。特别是,当您不显示所有列或以不同顺序显示它们时。当客户想要排序时,您可以将列名映射到列顺序并生成整数列表。


我们使用sp_executesql构建动态报告查询。非常有效的。无法从应用程序中构建SQL,但是参数只是在需要的地方插入并照常执行。
Josh Smeaton

2

反对在客户端进行排序的观点是大容量数据和分页。一旦您的行数超出了您可以轻松显示的范围,您通常就将其作为跳过/获取的一部分进行排序,您可能希望在SQL中运行它。

对于Entity Framework,您可以使用存储过程来处理文本搜索。如果遇到相同的排序问题,我看到的解决方案是使用存储的proc进行搜索,仅返回匹配的ID键集。接下来,使用列表(包含)中的ID对数据库重新查询(排序)。即使ID设置很大,EF也可以很好地处理。是的,这是两次往返,但是它使您始终可以将排序保留在数据库中,这在某些情况下可能很重要,并防止您在存储过程中编写大量的逻辑。


1

如何对显示结果的东西(网格,报表等)而不是对SQL进行排序?

编辑:

为了弄清楚自从这个答案较早被否决以来的事情,我将详细说明一下...

您说过您了解客户端排序,但想避免这种情况。那是你的电话。

不过,我想指出的是,通过在客户端进行操作,您可以一次提取数据,然后根据需要使用它-而不是每次往返服务器多次排序改变了。

您的SQL Server现在无需收税,这太棒了。不应该这样 但是,仅仅因为它没有过载,并不意味着它将永远保持这种状态。

如果您使用任何较新的ASP.NET东西来在Web上显示,则其中很多东西已经被使用。

值得为每个存储过程添加太多代码以处理排序吗?同样,您的电话。

我不是最终负责支持它的人。但是,请考虑一下在存储过程使用的各种数据集中添加/删除列(需要对CASE语句进行修改)或突然而不是按两列进行排序时,用户决定需要三列时会涉及的内容-现在要求您更新使用此方法的每个存储过程。

对我而言,获得一个可行的客户端解决方案并将其应用于少数面向用户的数据显示并完成它是值得的。如果添加了新列,则已经处理完毕。如果用户要按多列排序,则可以按两列或二十列进行排序。


那是正确的方法,但不被认为是“更好的方法”
DS

因为那时我仍在用C#或JavaScript编写自己的排序,因此看起来应该在SQL中变得更加轻松快捷。因此,我的问题。我是否只是遗漏了一些明显的东西,还是我们坚持使用我们的每一个该死的应用程序编写自己的自定义排序(使用C#或JavaScript)?
肖恩·汉利

3
等一下,那有成千上万行的结果集呢?您无法将所有这些数据返回给客户端。您必须对数据库进行分页和排序。
Eric Z Beard

Yadyn,了解。但是,一旦有了用于网格的通用分类器,就可以将其用于所有东西。
凯文·费尔柴尔德

Eric,是的。。。在这种情况下,您确实需要额外的处理,也许在SQL中是有意义的。这不是一个对与错的问题。在某些情况下,对于客户端上的SQL和某些情况而言,这是有意义的。
凯文·费尔柴尔德

1

抱歉,我参加聚会很晚,但是对于那些确实想避免使用动态SQL但又希望它提供灵活性的人来说,这是另一种选择:

无需动态生成SQL,而是编写代码为每种可能的变化生成唯一的proc。然后,您可以在代码中编写一个方法来查看搜索选项,并让它选择适当的proc进行调用。

如果只有几个变体,那么您可以手动创建proc。但是,如果您有很多变体,则不必维护所有变体,而只需维护proc生成器即可让它重新创建它们。

另外一个好处是,您也将通过这种方式获得更好的SQL计划以提高性能。


-1

我不知道这种解决方案可能仅适用于.NET。

我使用SQL order by子句中的初始排序顺序将数据提取到C#中,然后将数据放入DataView中,将其缓存在Session变量中,然后使用它来构建页面。

当用户单击列标题进行排序(或页面或过滤器)时,我不会返回数据库。相反,我返回到缓存的DataView,并将其“ Sort”属性设置为动态构建的表达式,就像动态SQL一样。(我使用“ RowFilter”属性以相同的方式进行过滤)。

您可以在http://ifdefined.com/btnet/bugs.aspx的我的应用程序BugTracker.NET演示中看到/感觉它能正常工作。


甜!Bug tracker.NET令人震惊!
digiguru

-7

除非必要,否则应避免对SQL Server进行排序。为什么不在应用服务器或客户端上排序?.NET泛型也进行出色的排序


6
由于可伸缩性。几千行就可以了,但是我不想减少一万并进行排序。或者更多。另外,分页又如何呢?我通常只想展示我需要显示的内容。在事实之后对24056的第21-30行进行排序是不正确的。
肖恩·汉利
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.