【问题标题】:Stored procedure with dynamic SQL and ORDER BY具有动态 SQL 和 ORDER BY 的存储过程
【发布时间】:2019-01-24 13:41:33
【问题描述】:

我构建了一个存储过程,旨在识别表中的重复项并以有意义的顺序显示重复的行。它看起来像这样:

CREATE PROCEDURE [dbo].[spFindDuplicates] 
    @tableName nvarchar(255), 
    @field1 nvarchar(255), 
    @field2 nvarchar(255) = '1', 
    @field3 nvarchar(255) = '2', 
    @field4 nvarchar(255) = '3', 
    @field5 nvarchar(255) = '4'

AS

BEGIN

DECLARE @query AS nvarchar(MAX);

SET @query = '
SELECT *
FROM ' + @tableName + '
WHERE CAST(' + @field1 + ' AS nvarchar(255)) + CAST(' + @field2 + ' AS nvarchar(255)) + CAST(' + @field3 + ' AS nvarchar(255)) + CAST(' + @field4 + ' AS nvarchar(255)) + CAST(' + @field5 + ' AS nvarchar(255)) 
IN 
(
    SELECT CAST(' + @field1 + ' AS nvarchar(255)) + CAST(' + @field2 + ' AS nvarchar(255)) + CAST(' + @field3 + ' AS nvarchar(255)) + CAST(' + @field4 + ' AS nvarchar(255)) + CAST(' + @field5 + ' AS nvarchar(255))
    FROM ' + @tableName + '
    GROUP BY CAST(' + @field1 + ' AS nvarchar(255)) + CAST(' + @field2 + ' AS nvarchar(255)) + CAST(' + @field3 + ' AS nvarchar(255)) + CAST(' + @field4 + ' AS nvarchar(255)) + CAST(' + @field5 + ' AS nvarchar(255))
    HAVING COUNT(*) > 1
)
ORDER BY ' + @field1 + ', ' + @field2 + ', ' + @field3 + ', ' + @field4 + ', ' + @field5

EXECUTE(@query);

END

GO

--Example:

EXEC spFindDuplicates @tableName = 'someRandomTable', @field1 = 'firstField', @field2 = 'secondField', @field3 = 'thirdField'

如您所见,我最多可以使用 5 个不同的字段进行连接,以便获得用于确定我们是否有重复项的键。请注意,我使用 CAST 函数能够连接具有各种数据类型(varchar、int、日期等)的字段。

当我使用 5 个不同的字段执行上述存储过程时,它工作正常。但我希望能够使用可变数量的字段(从 1 到 5)运行它,这就是我为 @field2 提供默认值到 @field5 的原因。

但是当我用上面的例子执行它时(提供了 3 个字段),我收到以下错误消息:

在 order by 列表中多次指定了一个列。 order by 列表中的列必须是唯一的。

问题:我怎样才能在不出错的情况下继续订购结果表?

奖励问题:如果您找到一种动态方式来使用具有任意数量字段(4、17 或其他)的存储过程,那对我来说会更有用。 p>

【问题讨论】:

  • 你可以动态地做到这一点,通过使用单列参数,你将传递逗号分隔的列或表类型
  • 你那里有一个巨大的安全漏洞;它对 SQL 注入非常开放。您应该使用sp_executesql 参数化您的查询并使用QUOTENAME 引用您的对象。现在你只是有一个漏洞等待被利用。
  • “Larnu 让我们考虑一下我不介意 SQL 注入” 不可能。你做。如果您不这样做,请重新考虑,改变主意,然后解决问题。
  • 如果这不是xy problem 我不知道是什么。
  • 那么,您希望在许多表中允许重复行,但还需要定期识别这些重复行?你确定你正在解决正确的问题吗?因为通常情况下,它表明您需要一些独特的约束来阻止重复项的存在。

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


【解决方案1】:

就像我在 cmets 中所说,注入在这里是一个大问题,您需要考虑它。说“让我们考虑一下我不介意注射”是幼稚的,你需要改变这种态度。始终确保您的 SQL 安全;那么您的应用程序就没有任何借口和机会被泄露。

正如你所追求的,我怀疑这可以实现目标。此处子查询无需使用IN 扫描您的表,您可以在CTE 中使用COUNTOVER 子句。

CREATE PROCEDURE [dbo].[FindDuplicates] --I've removed te sp prefix, as sp_ is reserved by MS
    @tableName sysname, 
    @field1 sysname, 
    @field2 sysname = NULL, 
    @field3 sysname = NULL, 
    @field4 sysname = NULL, 
    @field5 sysname = NULL

AS BEGIN

    DECLARE @query AS nvarchar(MAX);

    SET @query = N'WITH CTE AS(' + NCHAR(10) +
                 N'    SELECT *' + NCHAR(10) + 
                 N'           COUNT(*) OVER (PARTITION BY ' + STUFF(CONCAT(N',' + QUOTENAME(@field1),N',' + QUOTENAME(@field2),N',' + QUOTENAME(@field3),N',' + QUOTENAME(@field4),N',' + QUOTENAME(@field5)),1,1,N'') + N' AS RowCount' + NCHAR(10) +
                 N'    FROM ' + QUOTENAME(@tableName) + N')' + NCHAR(10) +
                 N'SELECT *' + NCHAR(10) +
                 N'FROM CTE' + NCHAR(10) +
                 N'WHERE RowCount > 1' + NCHAR(10) + 
                 N'ORDER BY ' + STUFF(CONCAT(N',' + QUOTENAME(@field1),N',' + QUOTENAME(@field2),N',' + QUOTENAME(@field3),N',' + QUOTENAME(@field4),N',' + QUOTENAME(@field5)),1,1,N'') + N';';

    PRINT @query;
    --EXEC sys.sp_executesql @query; --Uncomment to rrun the actual query
END
GO

对于你给我们EXEC dbo.FindDuplicates @tableName = 'someRandomTable', @field1 = 'firstField', @field2 = 'secondField', @field3 = 'thirdField';的命令,这会返回SQL:

WITH CTE AS(
    SELECT *
           COUNT(*) OVER (PARTITION BY [firstField],[secondField],[thirdField] AS RowCount
    FROM [someRandomTable])
SELECT *
FROM CTE
WHERE RowCount > 1
ORDER BY [firstField],[secondField],[thirdField];

相信会给你你所追求的行为。

【讨论】:

  • 干得好。花了我一秒钟来弄清楚你是如何处理 NULL 的,但这很聪明。
  • CONCATSTUFF 为 NULLable 参数和类似的 @SeanLange 实现了梦想。为了向其他人解释(也许我应该输入答案),CONCAT(与正常的字符串连接不同)在其中一个表达式的值为NULL 时不会返回NULL。所以'a' + NULL 将是NULL,但是CONCAT('a',NULL, NULL, 'b') 将是'ab'。我将其用于“我的”优势,这意味着在构建 PARTITION BYORDER BY 子句时,NULL 值参数被有效地“丢弃”
  • 是的,我只是从未考虑过这样使用。很酷。
【解决方案2】:

编辑代码以检查列列表是否存在于那里的 sys.columns 中,确保我们只获得合适的列。

CREATE FUNCTION dbo.fn_SplitString
(
   @List       NVARCHAR(MAX),
   @Delimiter  NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
   RETURN 
   (  
      SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
      FROM 
      ( 
        SELECT x = CONVERT(XML, '<i>' 
          + REPLACE(@List, @Delimiter, '</i><i>') 
          + '</i>').query('.')
      ) AS a CROSS APPLY x.nodes('i') AS y(i)
   );
GO
ALTER PROCEDURE [dbo].[spFindDuplicates] 
    @tableName nvarchar(255), 
    @columnlist nvarchar(max)  

AS

BEGIN

DECLARE @query AS nvarchar(MAX);

SET @columnlist = (SELECT STUFF((SELECT ','+'['+[name]+']'
FROM SYS.columns
WHERE object_id = object_id(@tableName)
AND [Name] IN
(
   SELECT Item
   FROM dbo.fn_SplitString(@columnlist,',')
)
FOR XML PATH('')
)
,1,1,''))

PRINT @columnlist

SET @query = 'SELECT * FROM (SELECT '+CAST(@columnlist AS NVARCHAR(MAX))+'
FROM '+CAST(@tableName AS nvarchar(MAX))+'
GROUP BY '+CAST(@columnlist AS NVARCHAR(MAX))+'
HAVING COUNT(*) > 1)Res1
ORDER BY '+@columnlist


EXEC SP_EXECUTESQL @query;

END

GO

【讨论】:

  • 这远不能解决注入问题。事实上,ORDER BY '+@columnlist 让情况变得更糟;因为那是不可能“确保安全”的。
  • @Dheerendra 您的建议几乎不错,但“SELECT *”对我来说很重要,这就是我需要使用子查询并连接字段的原因。在这种情况下,您的代码在连接时会失败
  • @Guillaume - 我已经编辑了代码。我认为要消除 sql 注入,我们可以从列列表中拆分列并与 sys.columns 的列匹配,并仅考虑匹配的列并更新逗号分隔列表作为对过程的增强
  • 编辑不会使这免受 sql 注入的影响。它是敞开的。
  • 使用 while 循环来分割字符串实在是太糟糕了。我意识到你复制并粘贴了这个。 Here 是处理拆分字符串的几种更好的方法。一个更好的方法是使用表值参数,这样你就不必解析它了。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-10-26
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多