SQL 存储过程中的动态排序

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

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

ORDER BY @sortCol1, @sortCol2

这是互联网上所有论坛上新手 SQL 和 存储过程开发人员给出的典型示例。“为什么这不可能?”他们会问。最终总会有人来教他们存储过程的编译本质,执行计划的一般性,以及为什么不可能直接将参数放入 ORDER BY子句的各种其他原因。


我知道你们中的一些人已经在想: “那么,让客户来排序吧。”自然地,这会从数据库中卸载工作。然而,在我们的例子中,我们的数据库服务器99% 的时间都没有出汗,它们甚至还没有多核,或者其他每6个月发生一次的无数系统架构改进。仅仅出于这个原因,让我们的数据库处理排序不成问题。此外,数据库在排序方面是 非常。他们为此进行了优化,并且花了很多年才把它做好,这种语言非常灵活,直观,简单,最重要的是任何初学者 SQL 编写者都知道如何做,更重要的是他们知道如何编辑它,进行修改,进行维护,等等。当您的数据库远远没有被征税,您只是想简化(和缩短!)开发时间这似乎是一个显而易见的选择。

还有网络问题。我已经尝试过用 JavaScript 对 HTML 表格进行客户端排序,但它们不可避免地不够灵活,不能满足我的需要。同样,由于我的数据库不需要太多的负担,而且能够轻松地对 真的进行排序,我很难证明重写或者滚动我自己的 JavaScript 排序器所需要的时间是合理的。服务器端排序通常也是如此,尽管它可能已经比 JavaScript 更受欢迎。我不是一个特别喜欢数据集开销的人,所以告我吧。

但是这又回到了一点: 这是不可能的,或者更确切地说,不容易。我已经做了,以前的系统,一个令人难以置信的黑客方式得到动态排序。它不漂亮,不直观,不简单,也不灵活,初学者 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

显然,这是一个非常简单的例子。真正的内容,因为我们通常有四到五列来支持排序,每一列都可能有第二列甚至第三列来进行排序(例如,日期降序然后按名称升序进行第二次排序) ,每一列都支持双向排序,这有效地使案例数量翻了一番。是啊,很快就会变得毛茸茸的。

这个想法是人们可以“容易地”改变排序情况,比如 vehicle leid 在存储时间之前被排序... 但是伪灵活性,至少在这个简单的例子中,真的到此为止了。实际上,每个测试失败的情况(因为这次我们的排序方法不适用于它)都呈现一个 NULL 值。这样你就得到了一个子句,它的功能如下:

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

你懂的。它之所以能够工作,是因为 SQLServer 有效地忽略了按子句顺序排列的空值。这是非常难以维护的,任何具有 SQL 基本工作知识的人都可以看到这一点。如果我失去了你们中的任何一个,不要难过。我们花了很长时间才让它工作,我们仍然感到困惑,试图编辑或创建新的喜欢它。谢天谢地,它不需要经常改变,否则很快就会变得“不值得麻烦。”

是的还是有用的。


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

我对存储过程以外的解决方案没有意见,因为我意识到这可能不是正确的方法。最好,我想知道是否有人可以在存储过程中做得更好,但如果没有,如何处理让用户使用 ASP.NET 对数据表(也是双向的)进行动态排序?

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

PS: 很高兴我没有展示一个支持动态排序、动态过滤/列的文本搜索、 ROWNUMBER () OVER 分页、 还有 try 分页的存储过程示例... ... 错误的事务回滚捕获... ... “庞然大物般大小”甚至都不足以描述它们。


更新:

  • 我想要 避免使用动态 SQL。一起解析一个字符串并在其上运行 EXEC 违背了首先使用存储过程的许多目的。有时候我会想,这样做的弊端是否值得,至少在这些特殊的动态排序情况下是这样的。尽管如此,每当我使用动态 SQL 字符串时,我总是感觉很脏,就像我仍然生活在经典 ASP 世界中一样。
  • 我们首先需要存储过程的很多原因是为了 保安。我不能就安全问题发号施令,只能提出解决方案。使用 SQLServer2005,我们可以在模式级别上设置单个存储过程的权限(如果需要的话,以每个用户为基础) ,然后直接拒绝对表的任何查询。批评这种方法的优点和缺点也许是另一个问题,但这又不是我的决定。我只是个负责编程的猴子。:)
76968 次浏览

如何处理显示结果(网格、报告等)的内容上的排序,而不是在 SQL 上?

编辑:

为了澄清一些事情,因为这个答案早些时候被否决了,我将详细说明一下..。

你说你知道客户端分类,但想避开它。这是你的决定,当然。

不过,我想指出的是,通过在客户端执行此操作,您可以将数据一次性提取出来,然后按照自己的意愿使用它——而不是每次更改排序时都要来回多次访问服务器。

您的 SQLServer 现在没有被征税,这很棒。不应该是这样的。但是仅仅因为它还没有超载并不意味着它将永远保持这样。

如果你正在使用任何更新的 ASP.NET 的东西在网络上显示,很多东西已经在内置的权利。

为了处理排序,是否值得向每个存储过程添加这么多代码。

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

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

在某种程度上,从存储过程转移到仅仅使用参数化查询来避免这种黑客行为,难道不值得吗?

动态 SQL 仍然是一个选项。你只需要决定这个选项是否比你现在拥有的更加美味。

这里有一篇文章显示: https://web.archive.org/web/20211029044050/https://www.4guysfromrolla.com/webtech/010704-1.shtml

是啊,很痛苦,而且你做事的方式和我很像:

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 而言,在不同的存储过程之间保持设计和格式相同,这样在进行更改时,它至少是整洁和可识别的。

可能还有第三种选择,因为您的服务器有很多空闲周期——使用一个 helper 过程通过一个临时表进行排序。差不多

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

警告: 我还没有测试过这个,但是它“应该”可以在 SQLServer2005中工作(它将从结果集中创建一个临时表,而不需要事先指定列)

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

所以,现在这样很完美。我不知道你为什么想要改变它,或者甚至问“有没有更好的办法?”真的,应该叫做“方式”。此外,它看起来工作正常,适合项目的需要,并且可能在未来几年内具有足够的可扩展性。因为你的数据库没有被征税,排序是 真的很简单,所以它应该在未来几年保持这种状态。

我不会担心的。

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

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

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

有几种不同的方法可以黑进去。

先决条件:

  1. 中只有一个 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

这个解决方案可能只适用于.NET,我不知道。

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

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

你可以在我的应用程序 BugTracker.NET 的 http://ifdefined.com/btnet/bugs.aspx演示中看到/感受到它的工作

除非必要,否则应避免 SQLServer 排序。为什么不在应用服务器端或客户端进行排序?还有。NET 泛型执行异常排序

这种方法使可排序的列不会按顺序重复两次,而且 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

对排序后的结果进行分页时,动态 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到 # 之间。您甚至可以将其扩展为一个列号列表,并使用类似 这个的函数将其解析为一个 int 表。然后你可以像这样根据条款建立订单..。

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

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

存储过程技术(黑客攻击?)我用来避免动态 SQL 的某些作业是有一个唯一的排序列。也就是说,

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

这一点很容易做到——您可以在 mySort 列中连接字段,使用数学函数或日期函数反转顺序,等等。

不过,在从 Sql-Server 检索数据之后,我最好使用 asp.net 网格视图或其他具有内置排序功能的对象来为我进行排序。或者即使它不是内置的——例如,在 asp.net 中的数据表等。

反对在客户端进行排序的一个参数是大容量数据和分页。一旦行数超出了可以轻松显示的范围,通常就会作为略过/获取的一部分进行排序,这可能需要在 SQL 中运行。

对于实体框架,可以使用存储过程来处理文本搜索。如果遇到同样的排序问题,我看到的解决方案是使用存储过程进行搜索,只返回匹配的 id 键集。接下来,使用列表(包含)中的 id 对数据库进行重新查询(使用排序)。EF 可以很好地处理这个问题,即使 ID 集非常大。是的,这是两个往返过程,但是它允许您始终将排序保留在 DB 中,这在某些情况下可能很重要,并且阻止您在存储过程中编写大量逻辑。

对不起,我来晚了,但是对于那些真正想避免使用动态 SQL,但又想要它所提供的灵活性的人来说,这里有另一个选择:

不要动态地动态生成 SQL,而是编写代码为每个可能的变化生成一个惟一的过程。然后,您可以在代码中编写一个方法来查看搜索选项,并让它选择要调用的适当的过程。

如果你只有一些变化,那么你可以只是手工创建触发器。但是如果您有很多变体,那么不必维护所有变体,您只需维护 proc 生成器,而是让它重新创建它们。

作为一个额外的好处,您还可以通过这种方式获得更好的 SQL 计划,从而获得更好的性能。