【问题标题】:Optional Order By in stored procedure?存储过程中的可选顺序?
【发布时间】:2014-01-22 18:14:09
【问题描述】:

我有一个有点复杂的存储过程。我想让用户选择要排序的列。他们应该能够根据需要选择任意多或少的列。

有没有办法在存储过程中实现这一点?如何将列名传递到过程中,然后将这些列名反映到 order by 子句中?注意会有可变数量的列。

我了解如何传递参数,只是不知道这是否可以在存储过程中动态构建order by子句

【问题讨论】:

  • 是的,通过使用动态 SQL - 在过程中构建 SQL 语句,有或没有 ORDER BY 子句 - 然后使用 exec sp_executesql .... 执行该 SQL 语句
  • 创建动态查询并使用 sp_executesql 执行
  • ...并确保防止 SQL 注入。
  • 或者,您可以在客户端对行进行排序。

标签: sql-server stored-procedures sql-order-by


【解决方案1】:

动态构建ORDER BY 非常简单。我假设您传递的参数如下:

@OrderByCol1 NVARCHAR(255),
@OrderByCol2 NVARCHAR(255),
...etc...

这些可能也可能不包括方向,例如N'MyColumn DESC'。因此,您可以按如下方式一起构建它:

DECLARE @sql NVARCHAR(MAX);

SELECT @sql = N'SELECT ... 
  FROM ...  
  WHERE ... 
  ORDER BY NULL'
  + COALESCE(',' + @OrderByCol1, '')
  + COALESCE(',' + @OrderByCol2, '')
  ...etc...;

PRINT @sql;
--EXEC sp_executesql @sql;

由于我们显然需要在每次回答甚至提到动态 SQL 时回顾整个 SQL 注入对话,所以我将添加一些示例。

如果它们只能升序排序,那么您可以通过简单地将参数值包装在QUOTENAME() 中来防止 SQL 注入。

  + COALESCE(',' + QUOTENAME(@OrderByCol1), '')
  + COALESCE(',' + QUOTENAME(@OrderByCol2), '')

否则,您还可以将参数按空格分开(假设您的列名不包含空格,它们不应该包含空格!),并验证左侧始终存在于 sys.columns 中。

IF @OrderByCol1 IS NOT NULL AND EXISTS
(
   SELECT 1 FROM sys.columns 
     WHERE [object_id] = OBJECT_ID('dbo.MyTable')
     AND name = LTRIM(LEFT(@OrderByCol1, CHARINDEX(' ', @OrderByCol1)))
)
BEGIN
  SET @sql += ',' + @OrderByCol1;
END

您可能还希望在那里进行检查,以防它们没有将任何内容传递给任何参数,或者只将值传递给参数 #4 等。上面就是这样做的。

使用 TVP 传递这些可能会更好,这样您就不必对他们可以选择的列数进行任意和人为的限制。这是一个三列 TVP 的示例,它允许您按列传递一组顺序,指示它们的应用顺序,并指示每个列的排序顺序。这也使得检查每一列是否真的是一列变得稍微容易一些(嘿,如果你将一列命名为[1;truncate table dbo.something],那么你应该得到你所得到的......)。

首先,在您的数据库中创建以下用户定义的表类型:

CREATE TYPE dbo.OrderByColumns AS TABLE
(
  [Sequence] TINYINT PRIMARY KEY,
  ColumnName SYSNAME NOT NULL,
  Direction VARCHAR(4) NOT NULL DEFAULT 'ASC'
);

然后:

DECLARE @x dbo.OrderByColumns;

INSERT @x SELECT 1, N'name', 'ASC';
INSERT @x SELECT 2, N'ID', 'DESC';
INSERT @x SELECT 3, N'1;truncate table dbo.whatever', 'DESC';

-- the above could be a parameter to your stored procedure
-- and could be populated in a DataTable in your application

DECLARE @sql NVARCHAR(MAX) = N'SELECT ... FROM ...
  WHERE ... ORDER BY NULL';

SELECT @sql += ',' + QUOTENAME(x.ColumnName) + ' ' + x.Direction
FROM sys.columns AS c
INNER JOIN @x AS x
ON c.name = x.ColumnName
AND c.[object_id] = OBJECT_ID('dbo.MyTable')
ORDER BY x.[Sequence] OPTION (MAXDOP 1);

PRINT @sql;

虽然您可以使用CASE 执行此操作,但动态生成ORDER BY(尤其是当它影响计划选择时)实际上可以提高性能。使用静态查询,您可以先获得@order_column 的计划,然后即使不同的排序列可能导致不同的、更有效的计划,它也会被重用。不同的计划可能使用不同的ORDER BY 子句,因为这些子句需要不同的 SORT 运算符。您可以使用OPTION (RECOMPILE) 在一定程度上解决这个问题,这可以确保您每次都生成一个新计划,但现在您每次都需要支付编译成本,即使总是或几乎总是使用相同的 order by。

当您使用动态 SQL 时,每个版本的查询都会单独优化。计划缓存膨胀是一个被optimize for ad hoc workloads 服务器设置所抵消的问题。这可以防止 SQL Server 缓存查询的特定变体的整个计划,直到该特定变体被使用两次。

【讨论】:

  • set @orderbycol1='1; truncate table dbo.ReallyImportantTable'
  • @Ben 是的,你有没有看到我有一整段关于如何阻止 SQL 注入?
  • 我的错。在编写 cmets 时将其归结为多任务处理!
  • @AaronBertrand 不错的答案,我写过类似的解决方案。后来我添加了一个“记录器”来查看用户实际请求的类型。我总共有 64 个选项,但其中 3 个选项负责 95%+,所以在得出结论之后,我在程序开始时放置了一个开关,直接跳转到 sub_procedure[1..3] 用于这三个 95%+ 排序.程序的其余部分保持不变。从性能的角度来看,这是我的救命稻草。
  • @Paul 这是 2000 年的吗?自 SQL Server 2005 以来,我们已经进行了语句级别的重新编译,因此将某些内容改组到不同的过程中应该不会真正改变计划缓存方面的任何内容。
【解决方案2】:

您可以动态组装 SQL 并使用sp_executesql 执行它。但是,这会使您失去使用非动态参数化存储过程所获得的一些性能和安全收益。

如果可能的 ORDER BY 列是有限列表,则可以在 ORDER BY 子句中使用 CASE WHEN 来根据传入的参数更改排序。例如。如果你传入了一个名为@order_column 的参数,你可以这样做

ORDER BY
CASE WHEN @order_column='ColumnA'
THEN ColumnA END
CASE WHEN @order_column='ColumnB'
THEN ColumnB END

【讨论】:

  • 嗯,实际上,动态生成ORDER BY - 特别是当它影响计划选择时 - 实际上可以更好地 提高性能。使用静态查询,您可以先获得@order_column 的计划,然后即使不同的排序列可能导致不同的、更有效的计划,它也会被重用。当您使用动态 SQL 时,每个版本的查询都会单独进行优化。计划缓存膨胀是一个被optimize for ad hoc workloads 抵消的问题。
  • 投了赞成票,因为您不依赖动态 sql,但不幸的是 Aaron 是对的。如果 OP 不是分页服务器端,我建议由客户端订购。
  • @JacobMattison 你会如何用不止一列来写它?
猜你喜欢
  • 2015-11-22
  • 2015-10-29
  • 1970-01-01
  • 2018-07-16
  • 2010-10-31
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2010-09-10
相关资源
最近更新 更多