【问题标题】:EXEC dynamic SQL built from query从查询构建的 EXEC 动态 SQL
【发布时间】:2016-03-02 03:17:12
【问题描述】:

我已经进行了一些搜索,但发现有点无法满足我的需求。

我受到编码标准的限制,无法使用 NVARCHAR(MAX) 或 VARCHAR(MAX),这是我的痛点。

我需要创建一个很可能超过 8000 个字符的动态 SQL 语句。发生这种情况的原因是因为要求使用一个巨大的 CASE WHEN 语句。我的方法是将每个 CASE WHEN 作为记录插入到表中。然后我在动态 SQL 语句中使用 XML PATH('') 来创建另一个动态 SQL 语句。

这是我正在使用的代码示例:

CREATE TABLE #TempTest (
ID INT
,Data VARCHAR(50)
,Data2 VARCHAR(50)
,Flag INT
)

CREATE TABLE #TempCase (
ID INT
,CaseS VARCHAR(50)
)

INSERT INTO #TempCase (
ID
,CaseS
)
VALUES
(1,'CASE WHEN Flag = 1 THEN Data ELSE Data2 END')
,(2,'CASE WHEN Flag = 2 THEN ID ELSE 0 END')

INSERT INTO #TempTest (
ID
,Data
,Data2
,Flag
)
VALUES
(1,'Hobo','Jim',1)
,(2,'Hobo Again','Jane',2)

EXEC('SELECT TOP 1 ''SELECT '' + STUFF((SELECT N'', '' + CaseS
    FROM #TempCase TC
    ORDER BY TC.ID
    FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''nvarchar(max)''), 1, 2, N'''') + '' FROM #TempCase''
FROM #TempCase TC
GROUP BY TC.ID') 

这个EXEC语句的结果是——

SELECT CASE WHEN Flag = 1 THEN Data ELSE Data2 END, CASE WHEN Flag = 2 THEN ID ELSE 0 END FROM #TempCase

查询的结果是我必须运行的动态 SQL。我试图将我当前的 EXEC 语句嵌套到另一个 EXEC 语句中,但这只会创建两个具有相同结果的数据集。大多数人处理此问题的方式是通过将初始 EXEC 结果设置为 VARCHAR(MAX) 并将其分配给第二个 EXEC 语句进行一些搜索,由于我公司的编码标准,我无法这样做。

任何建议将不胜感激。

【问题讨论】:

  • 为什么不使用EXEC sp_executesql @sql,那么@sql可以是NVARCHAR(MAX)
  • 限制是有原因的 - 使用动态 SQL 很糟糕,因为它会使您暴露于 sql 注入,并且如果您获得单个字符,则 非常 难以维护和调试错误的。超过 8000 个字符的动态 SQL 语句非常不好,因为维护它几乎是不可能的。性能也可能非常糟糕,因为优化器将很难为如此复杂的查询制定计划。为什么不用 CASE 语句创建一个或多个视图并且只选择您想要的字段?或者将大查询分解为多个查询,然后与 UNION ALL 结合使用?
  • 因为我不能使用 NVARCHAR(MAX) - @sql 仍然必须定义为 NVARCHAR(MAX)。
  • 我了解 SQL 注入的问题,但是该过程没有用户前端,因此不是真正的问题。我已经只选择了我需要的字段,但是根据记录中返回的内容确定输出,因此需要 CASE WHEN 语句。信息来自不同的服务器,因此我只想查询一次数据。
  • 但是在通过 FOR XML 连接之后,在转换从 xml 输出中提取的值时,您使用 nvarchar(max) 作为 sql 类型,因此即使您可以执行 exec(exec) 它仍然是违反编码标准

标签: tsql sql-server-2012


【解决方案1】:

假设您确实有权创建临时表,并且您已经具备将各种 CASE 语句放入表中的逻辑(以及其他一些可能有效也可能无效但仍然隐含但此处缺乏明确描述的假设) ,下面的方法怎么样:

CREATE TABLE #TempTest
  (
       ID    INT,
       Data  VARCHAR(50),
       Data2 VARCHAR(50),
       Flag  INT
  )

CREATE TABLE #TempCase
  (
       ID    INT,
       CaseS VARCHAR(50),
       CaseSDataType VARCHAR(20),
       HasProcessed BIT DEFAULT(0)
  )

INSERT INTO #TempCase
            (ID,
             CaseS,
             CaseSDataType)
     VALUES (1,
             'CASE WHEN Flag = 1 THEN Data ELSE Data2 END',
             'VARCHAR(50)'),
            (2,
             'CASE WHEN Flag = 2 THEN ID ELSE 0 END',
             'VARCHAR(50)')

INSERT INTO #TempTest
            (ID,
             Data,
             Data2,
             Flag)
     VALUES (1,
             'Hobo',
             'Jim',
             1),
            (2,
             'Hobo Again',
             'Jane',
             2) 

CREATE TABLE #Computed ( ID INT )
INSERT INTO #Computed ( ID ) SELECT ID FROM #TempTest

DECLARE @caseID INT
DECLARE @caseSQL VARCHAR(100)
DECLARE @colSQL VARCHAR(100)
DECLARE @updateSQL VARCHAR(255)
WHILE EXISTS( SELECT * FROM #TempCase WHERE HasProcessed = 0 )
BEGIN
    SELECT TOP 1 
        @caseID = ID, 
        @caseSQL = REPLACE(REPLACE('ALTER TABLE #TempTest ADD CaseStmt{id} AS ({stmt})', '{id}', CAST(ID AS VARCHAR)), '{stmt}', CaseS),
        @colSQL = REPLACE(REPLACE('ALTER TABLE #Computed ADD CaseStmt{id} {datatype}', '{id}', CAST(ID AS VARCHAR)), '{datatype}', CaseSDataType),
        @updateSQL = REPLACE('UPDATE t2 SET t2.CaseStmt{id} = t1.CaseStmt{id} FROM #TempTest t1 INNER JOIN #Computed t2 ON t1.ID = t2.ID', '{id}', ID)
    FROM #TempCase 
    WHERE HasProcessed = 0

    -- Add the columns
    EXEC(@caseSQL)
    EXEC(@colSQL)

    -- Update the column
    EXEC(@updateSQL)

    -- Next
    UPDATE #TempCase
       SET HasProcessed = 1
    WHERE ID = @caseID
END

-- Return your results (all of #Computed)
SELECT * FROM #Computed

实际上,#Computed 的结构变成了结果,就好像各种 CASE 语句都在单个 SELECT 语句中链接在一起。

我们可以通过简单地将 CASE 语句直接插入 UPDATE sql 来缩短计算列的创建,但这可能需要对 CASE 语句进行一些操作以确保表前缀正确。

【讨论】:

  • 我想过这一点,但是随着 CASE WHEN 语句数量的增加,我的数据来自另一台服务器这一事实会增加运行时间。这就是我尝试同时查询所有语句的原因。
【解决方案2】:

以暴露于 SQL 注入的方式构建动态 SQL!

无论如何,您可以将第一个 EXEC 的结果插入临时表并执行它:

CREATE TABLE #final(result VARCHAR(8000));

INSERT INTO #final(result)
EXEC(
'SELECT TOP 1 ''SELECT '' + STUFF((SELECT N'', '' + CaseS
    FROM #TempCase TC
    ORDER BY TC.ID
    FOR XML PATH(N''''), TYPE).value(N''.[1]'', N''nvarchar(max)''), 1, 2, N'''') 
        + '' FROM #TempTest''   
FROM #TempCase TC
GROUP BY TC.ID');

DECLARE @sql VARCHAR(8000);

SELECT @sql = result
FROM #final;

EXEC(@sql);

LiveDemo

输出:

╔══════╦══════╗
║ col1 ║ col2 ║
╠══════╬══════╣
║ Hobo ║    0 ║
║ Jane ║    2 ║
╚══════╩══════╝

【讨论】:

  • 但是如果 SQL 的结果超过 8000 个字符,字符串就会被截断。
猜你喜欢
  • 2018-08-04
  • 1970-01-01
  • 1970-01-01
  • 2013-09-24
  • 1970-01-01
  • 1970-01-01
  • 2012-11-23
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多