SQL 存储过程中的动态排序
Posted
技术标签:
【中文标题】SQL 存储过程中的动态排序【英文标题】:Dynamic Sorting within SQL Stored Procedures 【发布时间】:2010-09-14 01:04:06 【问题描述】:这是我过去花费数小时研究的问题。在我看来,现代RDBMS 解决方案应该解决的问题,但到目前为止,我还没有找到任何真正解决我认为在任何具有数据库后端的 Web 或 Windows 应用程序中令人难以置信的普遍需求的东西.
我说的是动态排序。在我的幻想世界中,它应该像这样简单:
ORDER BY @sortCol1, @sortCol2
这是新手 SQL 和Stored Procedure 开发人员在 Internet 上的各个论坛上给出的典型示例。 “为什么这不可能?”他们问。总是有人最终过来向他们讲授存储过程的编译特性、一般的执行计划,以及为什么不能将参数直接放入 ORDER BY
子句的各种其他原因。
我知道你们中的一些人已经在想什么:“那就让客户来进行分类吧。”自然地,这会从您的数据库中卸载工作。不过,在我们的案例中,我们的数据库服务器在 99% 的时间里都没有出过汗,它们甚至还不是多核的,也不是每 6 个月对系统架构进行的任何其他无数改进。仅出于这个原因,让我们的数据库处理排序不会有问题。此外,数据库非常擅长排序。他们针对它进行了优化,并且多年来一直在做正确的事情,执行它的语言非常灵活、直观和简单,最重要的是任何初学者 SQL 编写者都知道如何去做,更重要的是他们知道如何编辑它,进行更改、进行维护等。当您的数据库远没有被征税并且您只想简化(并缩短!)开发时间时,这似乎是一个显而易见的选择。
然后是网络问题。我玩过 javascript 可以对 html 表进行客户端排序,但它们不可避免地不够灵活,无法满足我的需求,而且,因为我的数据库没有过多的负担,并且真的可以进行排序真的 很容易,我很难证明重写或滚动我自己的 JavaScript 排序器所花费的时间。服务器端排序通常也是如此,尽管它可能已经比 JavaScript 更受欢迎。我不是特别喜欢 DataSet 开销的人,所以请告我。
但这又带回了一点,那就是这是不可能的——或者更确切地说,不容易。在以前的系统中,我已经完成了一种令人难以置信的动态排序方式。它既不漂亮,也不直观、简单或灵活,并且初学者 SQL 编写者会在几秒钟内迷失方向。这已经不是一个“解决方案”,而是一个“并发症”。
以下示例并非旨在展示任何类型的最佳实践或良好的编码风格或任何内容,也不代表我作为 T-SQL 程序员的能力。它们就是它们,我完全承认它们令人困惑,形式不好,而且只是普通的hack。
我们将一个整数值作为参数传递给存储过程(让我们将参数称为“排序”),并从中确定一堆其他变量。例如...假设排序为 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
显然,这是一个非常精简的示例。真正的东西,因为我们通常有四到五列来支持排序,除此之外,每列可能还有第二列甚至第三列(例如日期降序,然后按名称升序排序),并且每列都支持双列定向排序,有效地使案例数量增加一倍。是的……它很快就长毛了。
这个想法是,人们可以“轻松”更改排序情况,以便在 storagedatetime 之前对车辆 ID 进行排序......但伪灵活性,至少在这个简单的示例中,真正到此为止。本质上,每个未通过测试的案例(因为我们的排序方法这次不适用于它)呈现一个 NULL 值。因此,您最终会得到一个功能如下的子句:
ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah
你明白了。它之所以有效,是因为 SQL Server 按子句的顺序有效地忽略了空值。这很难维护,任何对 SQL 有任何基本工作知识的人都可能看到。如果我失去了你们中的任何一个,请不要难过。我们花了很长时间才让它工作,但我们仍然对尝试编辑它或创建类似的新东西感到困惑。值得庆幸的是它不需要经常更换,否则很快就会变得“不值得麻烦”。
然而它确实起作用了。
那么我的问题是:有更好的方法吗?
我可以接受存储过程以外的解决方案,因为我意识到这可能不是可行的方法。最好,我想知道是否有人可以在存储过程中做得更好,但如果没有,你们都如何处理让用户使用 ASP.NET 动态排序数据表(也是双向的)?
感谢您阅读(或至少略读)这么长的问题!
PS:很高兴我没有展示支持动态排序、动态过滤/列文本搜索、通过 ROWNUMBER() OVER 进行分页、AND 尝试的存储过程示例。 .catch 与错误的事务回滚......“庞然大物”甚至没有开始描述它们。
更新:
我想避免使用动态 SQL。将字符串解析在一起并在其上运行 EXEC 会首先破坏存储过程的许多目的。有时我想知道这样做的缺点是否不值得,至少在这些特殊的动态排序情况下。尽管如此,每当我做这样的动态 SQL 字符串时,我总是觉得很脏——就像我仍然生活在经典的 ASP 世界中一样。 我们首先需要存储过程的很多原因是为了安全。我不能就安全问题打电话,只建议解决方案。使用 SQL Server 2005,我们可以在架构级别对单个存储过程设置权限(如果需要,基于每个用户),然后直接拒绝对表的任何查询。批评这种方法的利弊也许是另一个问题,但这又不是我的决定。我只是主要的代码猴子。 :)【问题讨论】:
也参考***.com/questions/3659981/… -- 混合数据类型的 SQL Server 动态 ORDER BY 动态 SQL 是 FAR 的最佳方式...如果 [这是一个很大的 IF] ..您的数据访问层是严格的,您的动态 SQL 是由使用 RDBMS 严格编程的系统生成的规则以完美的形式表达。算法设计的数据库架构是一件美丽的事情...... 【参考方案1】:是的,这很痛苦,而且你的做法和我的做法很相似:
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而言,保持不同存储过程之间的设计和格式相同,这样至少在你进行更改时整洁且易于识别。
【讨论】:
没错。我的目标是避免对 5000 个大 varchar 字符串执行 EXEC 命令。如果只是为了增加安全性,我们所做的一切都必须通过存储过程来完成,因为我们可以在模式级别对它们设置权限。在我们的案例中,可扩展性和性能提升只是一个优势。 将可维护性添加到安全性、可扩展性、性能。一旦你有 3 或 4 个应用程序在你的数据库上运行动态 SQL,你就完蛋了,你无法改变任何东西,尤其是随着应用程序的老化和开发人员的不断发展。 exec 和动态 sql 都是邪恶的。 就是这样 --- 在我来到这里之前,我们已经为所有仍在运行的 Classic ASP Web 应用程序和许多仍在流通的 Access VB 应用程序做了。每当我必须对其中的任何一个进行维护时,我都会抽搐并且不得不抑制修复明显错误的冲动。 这也是我所做的,除了我将方向编码到 SortExpr 中: ORDER BY CASE WHEN sort = 'FirstName' THEN FirstName END ASC, CASE WHEN sort = '-FirstName' THEN FirstName END DESC 这是 DBA 和软件工程师的噩梦。因此,您无法拥有动态但严格的系统来根据您的信息模式生成富有表现力的 SQL 语句,而您却拥有这种令人作呕的硬编码胡言乱语。这是最好的编程。【参考方案2】:这种方法可以防止可排序的列在 order by 中重复两次,并且在 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
【讨论】:
这似乎是一个很好的答案,但当可排序的列具有不同的数据类型时似乎不起作用【参考方案3】:动态 SQL 仍然是一个选项。您只需要确定该选项是否比您当前的选项更可口。
这里有一篇文章显示:http://www.4guysfromrolla.com/webtech/010704-1.shtml。
【讨论】:
【参考方案4】:我的应用程序经常这样做,但它们都在动态构建 SQL。但是,当我处理存储过程时,我会这样做:
-
使存储过程成为返回值表的函数 - 不排序。
然后在您的应用程序代码中执行
select * from dbo.fn_myData() where ... order by ...
,以便您可以在那里动态指定排序顺序。
那么至少动态部分在您的应用程序中,但数据库仍在做繁重的工作。
【讨论】:
这可能是我见过的将动态 SQL 和存储过程结合使用的最佳折衷方案。我喜欢。我可能会在某个时候尝试类似的方法,但这样的改变在我们现有的任何正在进行的项目中都是令人望而却步的。 您可以使用本地表变量而不是返回数据的表格函数来实现相同的目的。我发现本地表比函数更灵活,因为您可以输出一些调试信息。【参考方案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 列中连接字段,使用数学或日期函数反转顺序等。
不过,在从 Sql-Server 检索数据之后,我最好使用我的 asp.net gridviews 或其他具有内置排序功能的对象来为我进行排序。或者即使它不是内置的——例如 asp.net 中的数据表等。
【讨论】:
【参考方案6】:有几种不同的方法可以破解它。
先决条件:
-
只有一个 SELECT 语句在
sp
省略任何排序(或
默认)
然后插入到临时表中:
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
【讨论】:
【参考方案7】:可能还有第三种选择,因为您的服务器有很多空闲周期 - 使用帮助程序通过临时表进行排序。类似的东西
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 中工作(它将根据结果集创建一个临时表,而无需提前指定列。)
【讨论】:
【参考方案8】:在某些时候,是否值得放弃存储过程而只使用参数化查询来避免这种黑客行为?
【讨论】:
在某些情况下,它们可能是钉子上的大锤,但我们通常希望直接在存储过程上设置权限(特别是 EXECUTE),并禁止直接对表进行任何 SQL 查询,甚至是 SELECT。我也不太喜欢骇客,但安全不是我的使命。 这就是为什么这么多人转向对象关系映射的原因。排序的不必要往返,相同的巨大 CASE 块,当实际上只需要更新一个列时对大量列进行毫无意义的更新等等。仍然存在的存储过程的一个成功论点是安全性。 我正在从 ORM (EF) 迁移到存储过程,因为 ORM 不支持全文搜索。 @RonnieOverby 全文搜索通常由专用解决方案提供更好的服务,例如 Lucene。 @HankGay 我有一种奇怪的感觉,实体框架也不支持Lucene。【参考方案9】:我同意,使用客户端。但这似乎不是你想听到的答案。
所以,它是完美的方式。我不知道你为什么要改变它,甚至问“有没有更好的方法”。真的,它应该被称为“道”。此外,它似乎可以很好地满足项目的需求,并且可能在未来几年内具有足够的可扩展性。由于您的数据库没有征税并且排序真的很容易它应该在未来几年保持这种状态。
我不会出汗的。
【讨论】:
我对客户端没有问题,因为我使用 Windows 应用程序走这条路。但是网络应用程序呢?我没有发现任何 JavaScript 解决方案真的足够灵活。是的,它确实像我所说的那样工作,但它是 SQL 的噩梦。当然我想知道是否有更好的方法。 它内置于较新的(2.0 及更高版本).NET 控件中。或者您可以创建自己的并将其应用于数据视图。 msdn.microsoft.com/en-us/library/hwf94875(VS.80).aspx 我的问题是可扩展性和性能之一。进行客户端或 Web 服务器端排序需要加载所有数据,而不是一次只加载 10 或 15 个数据。从长远来看,这是非常昂贵的,而数据库排序则没有。【参考方案10】:当您对排序结果进行分页时,动态 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 到 # 列之间。您甚至可以将其扩展为列号列表,并使用this 之类的函数将其解析为整数表。然后你会像这样构建 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 不能从应用程序构建,但参数只是插入到需要的地方并正常执行。【参考方案11】:反对在客户端进行排序的一个论点是大量数据和分页。一旦您的行数超出了您可以轻松显示的范围,您通常会将排序作为跳过/拍摄的一部分,您可能希望在 SQL 中运行。
对于实体框架,您可以使用存储过程来处理您的文本搜索。如果您遇到相同的排序问题,我看到的解决方案是使用存储过程进行搜索,只返回匹配的 id 键集。接下来,使用列表(包含)中的 id 对数据库重新查询(使用排序)。即使 ID 集非常大,EF 也能很好地处理这个问题。是的,这是两次往返,但它允许您始终将排序保留在数据库中,这在某些情况下可能很重要,并阻止您在存储过程中编写大量逻辑。
【讨论】:
【参考方案12】:如何对显示结果的内容(网格、报告等)而不是 SQL 进行排序?
编辑:
为了澄清一些事情,因为这个答案早些时候被否决了,我会详细说明一下......
您说您知道客户端排序,但想避开它。当然,那是你的决定。
不过,我想指出的是,通过在客户端执行此操作,您可以一次提取数据,然后根据需要使用它——而不是多次往返于每次更改排序时的服务器。
您的 SQL Server 现在不会被征税,这太棒了。不应该。但仅仅因为它没有超载并不意味着它会永远保持这种状态。
如果您正在使用任何较新的 ASP.NET 内容在 Web 上显示,那么其中的很多内容已经融入其中。
是否值得为每个存储过程添加这么多代码来处理排序?再次,您的电话。
我不是最终负责支持它的人。但是考虑一下当在存储过程使用的各种数据集中添加/删除列时会涉及什么(需要修改 CASE 语句),或者当用户突然而不是按两列排序时,用户决定他们需要三个——要求您现在更新使用此方法的每个存储过程。
对我来说,获得一个有效的客户端解决方案并将其应用于少数面向用户的数据显示并完成它是值得的。如果添加了新列,则它已被处理。如果用户想按多列排序,可以按其中的两列或二十列排序。
【讨论】:
这将是正确的方式,但不被认为是“更好的方式” 因为那时我仍在用 C# 或 JavaScript 编写自己的排序,而且似乎在 SQL 中它应该更容易和更快。因此我的问题。我只是遗漏了一些明显的东西,还是我们一直在编写我们自己的自定义排序(在 C# 或 JavaScript 中)我们工作的每个该死的应用程序? 等等,数万行的结果集呢?您无法将所有数据返回给客户端。您必须对数据库进行分页和排序。 亚丁,明白了。但是一旦你为你的网格有了一个通用的排序器,你就可以用它来处理你所有的东西。 Eric, True... 在这种情况下,您确实需要额外的处理,也许这在 SQL 中是有意义的。这远非正确与错误的问题。在某些情况下,它对 SQL 和客户端有意义。【参考方案13】:抱歉,我迟到了,但对于那些真正想要避免使用动态 SQL,但又想要它提供的灵活性的人来说,这里有另一种选择:
与其动态生成 SQL,不如编写代码为每个可能的变化生成一个唯一的过程。然后你可以在代码中编写一个方法来查看搜索选项并让它选择合适的过程来调用。
如果您只有一些变化,那么您可以手动创建 proc。但是,如果您有很多变体,那么不必维护它们,您只需维护您的 proc 生成器,而不是让它重新创建它们。
另外一个好处是,通过这种方式,您可以获得更好的 SQL 计划以获得更好的性能。
【讨论】:
【参考方案14】:这个解决方案可能只适用于 .NET,我不知道。
我使用 SQL order by 子句中的初始排序顺序将数据提取到 C# 中,将该数据放入 DataView 中,将其缓存在 Session 变量中,并使用它来构建页面。
当用户单击列标题进行排序(或页面或过滤器)时,我不会返回数据库。相反,我回到缓存的 DataView 并将其“排序”属性设置为我动态构建的表达式,就像我使用动态 SQL 一样。 (我使用“RowFilter”属性以同样的方式进行过滤)。
您可以在我的应用程序 BugTracker.NET 的演示中看到/感觉到它在 http://ifdefined.com/btnet/bugs.aspx 上工作
【讨论】:
甜! Bug tracker.NET 摇滚!【参考方案15】:除非必要,否则应避免 SQL Server 排序。为什么不在应用服务器或客户端排序? .NET Generics 也进行了特殊的排序
【讨论】:
因为可扩展性。几千行没问题,但我不想拉下一万行并对其进行排序。或者更多。另外,分页呢?我经常只想拉入我需要展示的东西。事后对 24056 的第 21-30 行进行排序是不正确的。以上是关于SQL 存储过程中的动态排序的主要内容,如果未能解决你的问题,请参考以下文章