这是我过去花费数小时研究的问题。在我看来,这应该是现代 RDBMS 解决方案应该解决的问题,但到目前为止,我还没有发现任何东西可以真正解决我认为在任何具有数据库后端的 Web 或 Windows 应用程序中令人难以置信的普遍需求.
我说的是动态排序。在我的幻想世界中,它应该像这样简单:
ORDER BY @sortCol1, @sortCol2
这是 SQL 新手和 Stored Procedure 开发人员在 Internet 上的各个论坛上给出的典型示例。 “为什么这不可能?”他们问。总是有人最终过来向他们讲授存储过程的编译性质、一般的执行计划以及不能将参数直接放入 ORDER BY
子句的各种其他原因。
我知道你们中的一些人已经在想什么:“那么让客户进行分类吧。”自然地,这会从您的数据库中卸载工作。不过,在我们的例子中,我们的数据库服务器甚至在 99% 的时间里都没有出过汗,它们甚至还不是多核的,也不是每 6 个月对系统架构进行的任何其他无数改进。仅出于这个原因,让我们的数据库处理排序不会有问题。此外,数据库非常擅长排序。他们已经针对它进行了优化,并且多年来一直在做正确的事情,执行它的语言非常灵活、直观和简单,最重要的是,任何初学者 SQL 编写者都知道如何去做,更重要的是他们知道如何编辑它,进行更改,进行维护等。当您的数据库远没有被征税并且您只想简化(并缩短!)开发时间时,这似乎是一个明显的选择。
然后是网络问题。我玩过 JavaScript 可以对 HTML 表进行客户端排序,但它们不可避免地不够灵活,无法满足我的需要很难证明重写或滚动我自己的 JavaScript 排序器所花费的时间是合理的。服务器端排序通常也是如此,尽管它可能已经比 JavaScript 更受欢迎。我不是特别喜欢 DataSets 开销的人,所以起诉我。
但这又带回了不可能的观点——或者更确切地说,不容易。在以前的系统中,我已经完成了一种令人难以置信的动态排序方式。它既不漂亮,也不直观、简单或灵活,并且初学者 SQL 编写者会在几秒钟内迷失方向。这已经不是一个“解决方案”,而是一个“并发症”。
以下示例并不意味着展示任何最佳实践或良好的编码风格或任何东西,也不代表我作为 T-SQL 程序员的能力。它们就是它们,我完全承认它们令人困惑,形式不好,而且只是简单的黑客攻击。
我们将一个整数值作为参数传递给存储过程(让我们将参数称为“排序”),并从中确定一堆其他变量。例如...假设 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
显然,这是一个非常精简的例子。真正的东西,因为我们通常有四到五列来支持排序,除此之外,每列可能还有第二列甚至第三列(例如日期降序,然后按名称升序排序),并且每列都支持双列定向排序,有效地使案例数量增加一倍。是的……它很快就长毛了。
这个想法是,人们可以“轻松地”更改排序情况,以便在 storagedatetime 之前对车辆 ID 进行排序......但伪灵活性,至少在这个简单的例子中,真的到此为止。本质上,每个未通过测试的案例(因为我们的排序方法这次不适用于它)呈现一个 NULL 值。因此,您最终会得到一个功能如下的子句:
ORDER BY NULL DESC, NULL, [storagedatetime] DESC, blah blah
你明白了。它之所以有效,是因为 SQL Server 按子句的顺序有效地忽略了空值。这很难维护,任何对 SQL 有任何基本工作知识的人都可能看到。如果我失去了你们中的任何一个,请不要难过。我们花了很长时间才让它工作,但我们仍然对尝试编辑它或创建类似的新东西感到困惑。值得庆幸的是,它不需要经常更改,否则它很快就会变得“不值得麻烦”。
然而它确实奏效了。
我的问题是:有没有更好的方法?
我对存储过程以外的解决方案没有意见,因为我意识到这可能不是要走的路。最好,我想知道是否有人可以在存储过程中做得更好,但如果没有,你们都如何处理让用户使用 ASP.NET 动态排序数据表(也可以双向)?
感谢您阅读(或至少略读)这么长的问题!
PS:很高兴我没有展示我的存储过程示例,该示例支持动态排序、动态过滤/列的文本搜索、通过 ROWNUMBER() OVER 进行分页,并尝试...捕获错误时的事务回滚... “庞然大物”甚至还没有开始描述它们。
更新:
我想避免使用动态 SQL。将字符串解析在一起并在其上运行 EXEC 会首先破坏存储过程的许多目的。有时我想知道这样做的缺点是否不值得,至少在这些特殊的动态排序情况下。尽管如此,每当我做这样的动态 SQL 字符串时,我总是觉得很脏——就像我仍然生活在经典的 ASP 世界中一样。
我们首先需要存储过程的很多原因是为了安全。我不能就安全问题打电话,只建议解决方案。使用 SQL Server 2005,我们可以在架构级别对单个存储过程设置权限(如果需要,基于每个用户),然后直接拒绝对表的任何查询。批评这种方法的利弊也许是另一个问题,但这又不是我的决定。我只是主要的代码猴子。 :)
是的,这很痛苦,而且您的操作方式与我的操作方式相似:
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 而言,保持不同存储过程之间的设计和格式相同,这样至少在您进行更改时它是整洁和可识别的。
这种方法可以防止可排序的列按顺序重复两次,并且在 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 仍然是一种选择。您只需要决定该选项是否比您目前拥有的更可口。
这里有一篇文章显示:http://www.4guysfromrolla.com/webtech/010704-1.shtml。
我的应用程序经常这样做,但它们都在动态构建 SQL。但是,当我处理存储过程时,我会这样做:
使存储过程成为返回值表的函数 - 无排序。然后在您的应用程序代码中执行 select * from dbo.fn_myData() where ... order by ... 这样您就可以在那里动态指定排序顺序。
然后至少动态部分在您的应用程序中,但数据库仍在做繁重的工作。
我用来避免某些作业的动态 SQL 的存储过程技术(hack?)是拥有一个唯一的排序列。 IE,
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 中的数据表等。
有几种不同的方法可以破解它。
先决条件:
sp 中只有一个 SELECT 语句 忽略任何排序(或具有默认值)
然后插入临时表:
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
可能还有第三种选择,因为您的服务器有很多空闲周期 - 使用帮助程序通过临时表进行排序。就像是
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 中工作(它将从结果集中创建一个临时表,而无需提前指定列。)
在某些时候,离开存储过程而只使用参数化查询来避免这种黑客行为难道不值得吗?
我同意,使用客户端。但这似乎不是您想听到的答案。
所以,它是完美的方式。我不知道你为什么要改变它,甚至问“有没有更好的方法”。真的,它应该被称为“道”。此外,它似乎可以很好地满足项目的需求,并且可能在未来几年内具有足够的可扩展性。由于您的数据库没有征税并且排序非常容易,因此在未来几年内应该保持这种状态。
我不会出汗的。
当您对排序结果进行分页时,动态 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
一个缺点是您必须记住客户端每列的顺序。特别是当您不显示所有列或以不同的顺序显示它们时。当客户端想要排序时,您将列名映射到列顺序并生成整数列表。
反对在客户端进行排序的一个论点是大量数据和分页。一旦您的行数超出了您可以轻松显示的范围,您通常会作为跳过/拍摄的一部分进行排序,您可能希望在 SQL 中运行。
对于实体框架,您可以使用存储过程来处理您的文本搜索。如果您遇到相同的排序问题,我看到的解决方案是使用存储过程进行搜索,只返回匹配的 id 键集。接下来,使用列表(包含)中的 id 对数据库重新查询(使用排序)。即使 ID 集非常大,EF 也能很好地处理这个问题。是的,这是两次往返,但它允许您始终将排序保留在数据库中,这在某些情况下可能很重要,并阻止您在存储过程中编写大量逻辑。
如何处理显示结果的东西(网格、报告等)而不是 SQL 的排序?
编辑:
自从这个答案早些时候被否决以来,为了澄清事情,我将详细说明......
你说你知道客户端排序,但想避开它。当然,那是你的决定。
不过,我想指出的是,通过在客户端执行此操作,您可以一次提取数据,然后根据需要使用它——而不是每次来回多次往返于服务器排序发生了变化。
您的 SQL Server 现在没有被征税,这太棒了。不应该。但仅仅因为它没有超载并不意味着它会永远保持这种状态。
如果您正在使用任何较新的 ASP.NET 内容在 Web 上显示,那么其中的很多内容已经融入其中。
是否值得为每个存储过程添加这么多代码来处理排序?再次,您的电话。
我不是最终负责支持它的人。但是考虑一下当在存储过程使用的各种数据集中添加/删除列时会涉及什么(需要修改 CASE 语句),或者当用户突然而不是按两列排序时,用户决定他们需要三个——要求您现在更新使用此方法的每个存储过程。
对我来说,获得一个有效的客户端解决方案并将其应用于少数面向用户的数据显示并完成它是值得的。如果添加了新列,则它已被处理。如果用户想按多列排序,他们可以按其中的两列或二十列排序。
抱歉,我迟到了,但对于那些真正想要避免使用动态 SQL,但又想要它提供的灵活性的人来说,这是另一种选择:
与其动态生成 SQL,不如编写代码为每个可能的变化生成一个唯一的过程。然后您可以在代码中编写一个方法来查看搜索选项并让它选择适当的过程来调用。
如果您只有一些变化,那么您可以手动创建 proc。但是,如果您有很多变体,那么不必维护它们,您只需维护您的 proc 生成器,而不是让它重新创建它们。
作为一个额外的好处,您将获得更好的 SQL 计划,从而获得更好的性能。
这个解决方案可能只适用于.NET,我不知道。
我使用 SQL order by 子句中的初始排序顺序将数据提取到 C# 中,将该数据放入 DataView 中,将其缓存在 Session 变量中,并使用它来构建页面。
当用户单击列标题进行排序(或分页或过滤)时,我不会返回数据库。相反,我返回缓存的 DataView 并将其“排序”属性设置为我动态构建的表达式,就像我使用动态 SQL 一样。 (我使用“RowFilter”属性以相同的方式进行过滤)。
您可以在 http://ifdefined.com/btnet/bugs.aspx 上的我的应用程序 BugTracker.NET 的演示中看到/感觉到它在工作
除非必要,否则应避免 SQL Server 排序。为什么不在应用服务器或客户端排序? .NET Generics 也进行了特殊的排序