132

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

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

ORDER BY @sortCol1, @sortCol2

这是新手 SQL 和存储过程开发人员在 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,我们可以在架构级别对单个存储过程设置权限(如果需要,基于每个用户),然后直接拒绝对表的任何查询。批评这种方法的利弊也许是另一个问题,但这又不是我的决定。我只是主要的代码猴子。:)
4

15 回答 15

103

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

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

于 2008-09-29T16:42:13.197 回答
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
于 2008-09-30T04:35:39.333 回答
6

动态 SQL 仍然是一种选择。您只需要决定该选项是否比您目前拥有的更可口。

这是一篇文章,显示:http ://www.4guysfromrolla.com/webtech/010704-1.shtml 。

于 2008-09-29T16:28:43.043 回答
6

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

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

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

于 2008-09-29T19:08:38.103 回答
6

我用来避免某些作业的动态 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 中的数据表等。

于 2010-08-02T17:17:19.353 回答
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 中工作(它将从结果集中创建一个临时表,而无需提前指定列。)

于 2008-09-29T16:49:52.617 回答
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

于 2008-09-29T22:17:55.760 回答
2

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

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

我不会出汗的。

于 2008-09-29T18:00:51.887 回答
2

在某些时候,离开存储过程而只使用参数化查询来避免这种黑客行为难道不值得吗?

于 2008-09-29T16:27:11.690 回答
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

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

于 2009-05-07T20:13:11.347 回答
2

反对在客户端进行排序的一个论点是大量数据和分页。一旦您的行数超出了您可以轻松显示的范围,您通常会作为跳过/拍摄的一部分进行排序,您可能希望在 SQL 中运行。

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

于 2014-01-07T18:56:25.250 回答
1

如何处理显示结果的东西(网格、报告等)而不是 SQL 的排序?

编辑:

自从这个答案早些时候被否决以来,为了澄清事情,我将详细说明......

你说你知道客户端排序,但想避开它。当然,那是你的决定。

不过,我想指出的是,通过在客户端执行此操作,您可以一次提取数据,然后根据需要使用它——而不是每次来回多次往返于服务器排序发生了变化。

您的 SQL Server 现在没有被征税,这太棒了。不应该。但仅仅因为它没有超载并不意味着它会永远保持这种状态。

如果您正在使用任何较新的 ASP.NET 内容在 Web 上显示,那么其中的很多内容已经融入其中。

是否值得为每个存储过程添加这么多代码来处理排序?再次,您的电话。

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

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

于 2008-09-29T16:06:57.477 回答
1

抱歉,我迟到了,但对于那些真正想要避免使用动态 SQL,但又想要它提供的灵活性的人来说,这是另一种选择:

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

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

作为一个额外的好处,您将获得更好的 SQL 计划,从而获得更好的性能。

于 2018-12-19T22:46:31.723 回答
-1

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

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

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

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

于 2008-09-30T04:11:00.500 回答
-8

除非必要,否则应避免 SQL Server 排序。为什么不在应用服务器或客户端排序?.NET Generics 也进行了特殊的排序

于 2008-09-30T04:16:48.020 回答