ChatGPT解决这个技术问题 Extra ChatGPT

SQL 存储过程中的动态排序

这是我过去花费数小时研究的问题。在我看来,这应该是现代 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,我们可以在架构级别对单个存储过程设置权限(如果需要,基于每个用户),然后直接拒绝对表的任何查询。批评这种方法的利弊也许是另一个问题,但这又不是我的决定。我只是主要的代码猴子。 :)

也请参阅 stackoverflow.com/questions/3659981/… -- 混合数据类型的 SQL Server 动态 ORDER BY
动态 SQL 是 FAR 的最佳方式...如果 [这是一个很大的 IF] ..您的数据访问层是严格的,并且您的动态 SQL 是由一个严格编程的系统生成的,该系统使用以完美形式表达的 RDBMS 规则进行严格编程。算法设计的数据库架构是一件美丽的事情......

V
Vadim Ovchinnikov

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

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 语句,而您却拥有这种令人作呕的硬编码胡言乱语。这是最好的编程。
J
Jason DeFontes

这种方法可以防止可排序的列按顺序重复两次,并且在 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

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

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

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


P
Peter Mortensen

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

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

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


这可能是我见过的同时使用动态 SQL 和存储过程之间的最佳折衷方案。我喜欢。我可能会在某个时候尝试类似的方法,但这样的改变在我们现有的任何正在进行的项目中都是令人望而却步的。
您可以使用本地表变量而不是返回数据的表格函数来实现相同的目的。我发现本地表比函数更灵活,因为您可以输出一些调试信息。
d
dave

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


M
Matt Rogish

有几种不同的方法可以破解它。

先决条件:

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


P
Peter Mortensen

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

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 中工作(它将从结果集中创建一个临时表,而无需提前指定列。)


H
Hank Gay

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


在某些情况下,它们可能是钉子上的大锤,但通常我们希望直接在存储过程上设置权限(特别是 EXECUTE),并禁止直接对表进行任何 SQL 查询,甚至是 SELECT。我也不太喜欢hackery,但安全不是我的使命。
这就是为什么这么多人转向对象关系映射的原因。排序的不必要往返,相同的巨大 CASE 块,当实际上只需要更新一个列时对大量列进行无意义的更新等。仍然存在的存储过程的一个获胜论点是安全性。
我正在从 ORM (EF) 迁移到存储过程,因为 ORM 不支持全文搜索。
@RonnieOverby 全文搜索通常由专用解决方案提供更好的服务,例如 Lucene。
@HankGay 我有一种奇怪的感觉,实体框架也不支持 Lucene。
D
D.S.

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

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

我不会出汗的。


我对客户端没有问题,因为我使用 Windows 应用程序走这条路。但是网络应用程序呢?我没有发现任何 JavaScript 解决方案真的足够灵活。是的,它确实像我所说的那样工作,但它是 SQL 的噩梦。当然我想知道是否有更好的方法。
它内置于较新的(2.0 及更高版本).NET 控件中。或者您可以创建自己的并将其应用于数据视图。 msdn.microsoft.com/en-us/library/hwf94875(VS.80).aspx
那么我的问题是可扩展性和性能之一。进行客户端或 Web 服务器端排序需要加载所有数据,而不是一次只加载 10 或 15 个数据。从长远来看,这是非常昂贵的,而数据库排序则没有。
P
Peter Mortensen

当您对排序结果进行分页时,动态 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 不能从应用程序构建,但参数只是插入到需要的地方并正常执行。
P
Paul Schirf

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

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


K
Kevin Fairchild

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

编辑:

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

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

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

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

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

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

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

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


那将是正确的方式,但不被认为是“更好的方式”
因为那时我仍然在用 C# 或 JavaScript 编写自己的排序,而且它似乎在 SQL 中应该更容易和更快。因此我的问题。我只是遗漏了一些明显的东西,还是我们一直在编写我们自己的自定义排序(在 C# 或 JavaScript 中)我们工作的每个该死的应用程序?
等等,数万行的结果集呢?您无法将所有数据返回给客户端。您必须对数据库进行分页和排序。
亚丁,明白了。但是一旦你有一个通用的网格分类器,你就可以将它用于你所有的东西。
Eric, True... 在这种情况下,您确实需要额外的处理,也许这在 SQL 中是有意义的。这远非正确与错误的问题。在某些情况下,它对 SQL 有意义,而在某些情况下,在客户端。
D
DJo

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

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

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

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


C
Corey Trager

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

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

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

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


甜的!错误跟踪器.NET 摇滚!
S
Saif Khan

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


因为可扩展性。几千行没问题,但我不想下拉一万行并对其进行排序。或者更多。另外,分页呢?我经常只想拉入我需要展示的东西。事后对 24056 的第 21-30 行进行排序是不正确的。