在存储过程中插入 SQL WHERE 子句作为参数,并仅使用非空子句及其参数值

Insert SQL WHERE clauses as parameter in a stored procedure and use only the non-null clauses and their parameter value

我的问题有点复杂,但我会尽量简化它。我的基本想法是声明 3 个条件(条件是 where 子句)和 3 个参数(与条件一起使用)。这些是为存储过程声明的 运行 动态 SQL.

我想要的是创建一个 SELECT * FROM TableName 并仅使用不可为空的条件和参数对过滤 table。

虚拟 table:

CREATE TABLE test 
(
     id_number NVARCHAR(50) NOT NULL,
     number_of_products INT,
     username TEXT
);

INSERT INTO test (id_number, number_of_products, username)
VALUES (1000077004, 3, 'Jhon Smith'),
       (1000077005, 4, 'Nick Smith'),
       (1000077006, 4, 'Dale Smith'),
       (1000077007, 5, 'Diana Smith'),
       (1000077008, 5, 'Alice Smith'),
       (1000077009, 6, 'Antony Smith'),
       (1000077010, NULL, 'Bruce Smith');

SELECT * FROM test

例如,如果用户仅指定 (condition1=' >', parameter1='3') 和 (condition2=' <', parameter2='6') 那么 SQL 查询将是:

SELECT * FROM test
WHERE number_of_products condiction1 parameter1 AND condition2 paramater2 

但如果用户仅指定 (condition1=' >', parameter1='5') 那么 SQL 查询将为:

SELECT * FROM test
WHERE number_of_products condiction1 parameter1

为此,我创建了以下存储过程(不完整):

CREATE OR ALTER PROCEDURE [dbo].[dynamicquery1] (
    @TableName NVARCHAR(50),
    @Field NVARCHAR(100) = NULL,
    @Criterion1 NVARCHAR(100) = NULL,
    @Parameter1 NVARCHAR(100) = NULL,
    @Criterion2 NVARCHAR(100) = NULL,
    @Parameter2 NVARCHAR(100) = NULL,
    @Criterion3 NVARCHAR(100) = NULL,
    @Parameter3 NVARCHAR(100) = NULL,
    @All VARCHAR(2) = '-1'
)
AS          
BEGIN
PRINT('Starting the procedure')

    SET NOCOUNT ON;  
 
    DECLARE 
        @SQL NVARCHAR(MAX),
        @SQL_WHERE NVARCHAR(MAX),
        @ParameterDef NVARCHAR(500);

    SET @ParameterDef = '@Parameter NVARCHAR(100)'
    SET @SQL = 'SELECT * FROM ' + @TableName;
    SET @SQL_WHERE = '';

    /* BUILD THE WHERE CLAUSE IF @Field IS PRESENT */

    IF NULLIF ( @Field, '' ) IS NOT NULL 
    
    BEGIN
        -- Field value
        SET @SQL_WHERE = ' WHERE ' + @Field;

        -- Set @Parameter value
        SET @Parameter1 =
            CASE WHEN NULLIF ( @Parameter1, '' ) IS NOT NULL
                    THEN @Parameter1
                 ELSE @All
            END;

        SET @Parameter2 =
            CASE WHEN NULLIF ( @Parameter2, '' ) IS NOT NULL
                    THEN @Parameter2
                 ELSE @All
            END;

        -- Field Comparison value
        IF @Field LIKE '%[0-9]%'
            PRINT('Column is numeric')
            BEGIN
                SET @SQL_WHERE += CASE @Criterion1
                    WHEN 'greater than' THEN ' >' + @Parameter1
                    WHEN 'greater than or equal' THEN ' >=' + @Parameter1
                    WHEN 'less than' THEN ' <' + @Parameter1
                    WHEN 'less than or equal' THEN ' <=' + @Parameter1
                    WHEN 'not equal' THEN ' <>' + @Parameter1
                    WHEN 'equal' THEN ' =' + @Parameter1
                    ELSE ''
                END;

                PRINT('Column is still numeric')
            END;

        IF @Field NOT LIKE '%[0-9]%'
            PRINT('Column is text')
            BEGIN
                SET @SQL_WHERE += CASE @Criterion1 
                WHEN 'start with' THEN ' LIKE ' + ''''+ @Parameter1 + '&'''
                WHEN 'end with' THEN ' LIKE ' + '''&' + @Parameter1 + ''''
                WHEN 'in any position' THEN ' LIKE ' + '''%' + @Parameter1 + '%'''
                WHEN 'in second position' THEN ' LIKE ' + '''_' + @Parameter1 + '%'''
                WHEN 'specific character and at least 2 characters in length' THEN ' LIKE ' + @Parameter1 + '_%'''
                WHEN 'specific character and at least 3 characters in length' THEN ' LIKE ' + @Parameter1 + '__%'''
                ELSE ''
            END;
        END;
    END;

    -- Finish SQL statement.
    SET @SQL = @SQL + ISNULL ( @SQL_WHERE, '' ) + ';';

    -- Execute the dynamic statement.
    PRINT(@SQL)
    PRINT(@ParameterDef)
    PRINT(@Parameter1)
    PRINT(@Criterion1)
    PRINT(@Parameter2)
    PRINT(@Criterion2)
    EXEC sp_executesql @SQL, @ParameterDef, @Parameter1=@Parameter1, @Parameter2=Parameter2;
    
END
GO

对于如何更改 Criterion1 和 Parameter1 的 SET 语句以包括 Criterion2、3 和 Parameters2、3 对,我将非常感激。但是只有当它们不为空时才将它们包含在查询中,因此例如,

EXEC [dynamicquery1] @TableName='test', @Field='number_of_products', @Criterion1='greater than', @Parameter1 = '3', @Criterion2='less than or equal', @Parameter2 = '6'

上面的EXEC会return产品数量大于3小于等于6的行

正如我在评论中所说,我真的不建议这样做,但如果您要这样做,您需要在脚本中更改一些内容。比如你判断是否使用数字操作数:

IF @Field LIKE '%[0-9]%'
        PRINT('Column is numeric')

这取决于您在所有数字列名称中都输入一个数字,并且不在任何 non-numeric 中输入一个数字,您最好在系统目录视图中查找实际类型:

SELECT  t.name
FROM    sys.columns AS c
        INNER JOIN sys.types AS t
            ON t.user_type_id = c.user_type_id
            AND t.system_type_id = c.system_type_id
WHERE   c.Name = @Field
AND     c.object_id = OBJECT_ID(@TableName, 'U');

这还有一个额外的好处,即确保参数 @TableName@Field 分别是有效的 table 和列名,因此为您的查询提供了一些额外的验证。

如果我要这样做,我会创建一个 table 来存储您要使用的操作数列表,以及 table:[=33 中的显示名称=]

CREATE TABLE dbo.Operands
(
        OperandID INT IDENTITY(1, 1) NOT NULL,
        Name VARCHAR(255) NOT NULL,
        SqlExpression VARCHAR(50) NOT NULL,
    CONSTRAINT PK_Operands__OperandID PRIMARY KEY (OperandID)
);
INSERT dbo.Operands(Name, SqlExpression)
VALUES 
    ('greater than' , ' > %s'),
    ('greater than or equal', ' >= %s'),
    ('less than', ' < %s');

然后您可以将此 table 映射到有效的数据类型,这也将有助于验证传递的参数(例如,如果有人通过“开始于”比较但针对的是日期时间列)。在我的演示中,我跳过了创建实际的 table,而只是使用 table 值构造函数来创建它,使用您在问题中使用的数字和 non-numeric 值。

SELECT  *
FROM    (VALUES
            (1, 'greater than' , ' > {{parameter}}'),
            (1, 'greater than or equal', ' >= {{parameter}}'),
            (1, 'less than', ' < {{parameter}}'),
            (1, 'less than or equal', ' <= {{parameter}}'),
            (1, 'not equal', ' <> {{parameter}}'),
            (1, 'equal', ' = {{parameter}}'),
            (0, 'equal', ' = {{parameter}}'),
            (0, 'start with', ' LIKE CONCAT({{parameter}}, ''%'')'),
            (0, 'end with', ' LIKE CONCAT({{parameter}}, ''%'')'),
            (0, 'in any position', ' LIKE CONCAT(''%'', {{parameter}}, ''%'')'),
            (0, 'in second position', ' LIKE CONCAT(''_'', {{parameter}}, ''%'')'),
            (0, 'specific character and at least 2 characters in length', ' LIKE CONCAT({{parameter}}, ''_%'')'),
            (0, 'specific character and at least 3 characters in length', ' LIKE CONCAT({{parameter}}, ''__%'')')
        ) op (NumericField, Criterion, Operator);

正如我所说,还有改进的余地,但是增加了映射的复杂性,从简单的 numeric/non-numeric 到考虑其他类型。

我在表达式中使用 {{parameter}} 的原因是稍后我将用实际参数名称替换它,可能还有一个表达式,所以它现在只是一个占位符。一个简单的例子是:

SELECT  CONCAT(p.ColumnName, REPLACE(Operand, '{{parameter}}', p.ParameterName))
FROM    (VALUES
            (' = {{parameter}}', 'Column1', '@Parameter1'),
            (' >= {{parameter}}', 'Column2', '@Parameter2'),
            (' LIKE CONCAT(''%'', {{parameter}}, ''%'')', 'Column3', '@Parameter3')
        ) p (Operand, ColumnName, ParameterName);

哪个returns

Column1 = @Parameter1
Column2 >= @Parameter2
Column3 LIKE CONCAT('%', @Parameter3, '%')

在我的演示中,我只将类型限制为文本或数字类型,因为这似乎是您想要做的,因此下一节将检索传入的列的实际类型,并且将设置一个字段来表示它是否为数字。这将与操作数 table 结合使用以确定 field/criterion 是否为有效组合:

    DECLARE @IsNumericField BIT, @TypeName SYSNAME;
    SELECT  @IsNumericField = CASE WHEN t.name IN ('sysname', 'nvarchar', 'nchar', 'char', 'text', 'varchar', 'ntext') THEN 0 ELSE 1 END,
            @TypeName = t.name
    FROM    sys.columns AS c
            INNER JOIN sys.types AS t
                ON t.user_type_id = c.user_type_id
                AND t.system_type_id = c.system_type_id
    WHERE   c.Name = @Field
    AND     c.object_id = OBJECT_ID(@TableName, 'U')
    AND     t.name IN ('sysname', 'nvarchar', 'nchar', 'char', 'text', 'varchar', 'ntext', 'tinyint',
                     'smallint', 'int', 'real', 'money', 'float', 'decimal', 'numeric', 'smallmoney', 'bigint');

下一部分是将所有这些结合起来以实际构建您的 where 子句:

DECLARE @Criterion1 NVARCHAR(100) = 'greater than',
        @Parameter1 NVARCHAR(100) = '1',
        @Criterion2 NVARCHAR(100) = 'less than or equal',
        @Parameter2 NVARCHAR(100) = 5,
        @Criterion3 NVARCHAR(100) = NULL,
        @Parameter3 NVARCHAR(100) = NULL;

SELECT  CONCAT('AND ', 
            QUOTENAME(@Field), 
            REPLACE(op.Operator, '{{parameter}}', CONCAT('TRY_CONVERT(', @TypeName, ', ', p.ParameterName, ')')))
FROM    (VALUES
            (1, 'greater than' , ' > {{parameter}}'),
            (1, 'greater than or equal', ' >= {{parameter}}'),
            (1, 'less than', ' < {{parameter}}'),
            (1, 'less than or equal', ' <= {{parameter}}'),
            (1, 'not equal', ' <> {{parameter}}'),
            (1, 'equal', ' = {{parameter}}'),
            (0, 'equal', ' = {{parameter}}'),
            (0, 'start with', ' LIKE CONCAT({{parameter}}, ''%'')'),
            (0, 'end with', ' LIKE CONCAT({{parameter}}, ''%'')'),
            (0, 'in any position', ' LIKE CONCAT(''%'', {{parameter}}, ''%'')'),
            (0, 'in second position', ' LIKE CONCAT(''_'', {{parameter}}, ''%'')'),
            (0, 'specific character and at least 2 characters in length', ' LIKE CONCAT({{parameter}}, ''_%'')'),
            (0, 'specific character and at least 3 characters in length', ' LIKE CONCAT({{parameter}}, ''__%'')')
        ) op (NumericField, Criterion, Operator)
        INNER JOIN
        (VALUES
            (@Criterion1, @Parameter1, '@Parameter1'),
            (@Criterion2, @Parameter2, '@Parameter2'),
            (@Criterion3, @Parameter3, '@Parameter3')
        ) p (Criterion, ParameterValue, ParameterName)
            ON p.Criterion = op.Criterion
WHERE   op.NumericField = @IsNumericField;

由于我只传递了 parameter1 和 parameter2 的值,因此 returns 两行:

AND [number_of_products] > TRY_CONVERT(int, @Parameter1)
AND [number_of_products] <= TRY_CONVERT(int, @Parameter2)

我已经使用 TRY_CONVERT() 以及之前检索到的类型名称来更优雅地处理无效数据,因此如果有人为数字列传递参数值“String”,您将不会获得转换错误,你将得不到任何结果。如果您想抛出错误,只需使用 CONVERT() 即可。

在完整的工作演示中,我使用 STRING_AGG() 将其连接成一个变量。

最后,当调用你的SQL时,你不需要动态声明参数,如果它们没有出现在SQL中,它们将不会被使用,所以声明并通过所有 3 个将不是问题:

    EXECUTE sp_executesql 
                @SQL, 
                N'@Parameter1 NVARCHAR(100), @Parameter2 NVARCHAR(100), @Parameter3 NVARCHAR(100)',
                @Parameter1,
                @Parameter2,
                @Parameter3;

然后您需要做的就是将所有这些整合到一个存储过程中。

Example on DB<>Fiddle

CREATE OR ALTER PROCEDURE [dbo].[dynamicquery1] (
    @TableName sysname,
    @Field NVARCHAR(100) = NULL,
    @Criterion1 NVARCHAR(100) = NULL,
    @Parameter1 NVARCHAR(100) = NULL,
    @Criterion2 NVARCHAR(100) = NULL,
    @Parameter2 NVARCHAR(100) = NULL,
    @Criterion3 NVARCHAR(100) = NULL,
    @Parameter3 NVARCHAR(100) = NULL,
    @All VARCHAR(2) = '-1'
)
AS
BEGIN

    -- VALIDATE TABLE EXISTS
    IF NOT EXISTS (SELECT 1 FROM sys.tables AS t WHERE t.object_id = OBJECT_ID(@TableName))
    BEGIN
        RAISERROR ('Invalid table', 16, 1);
        RETURN;
    END

    DECLARE @SQL NVARCHAR(MAX) = CONCAT('SELECT * FROM ', @TableName);
    IF @Field IS NULL
    BEGIN
        EXECUTE sp_executesql @SQL;
        RETURN;
    END

    -- USE SYSTEM CATALOGUE VIEWS TO VALIDATE @FIELD AND GET THE CORRECT TYPE
    DECLARE @IsNumericField BIT, @TypeName SYSNAME;
    SELECT  @IsNumericField = CASE WHEN t.name IN ('sysname', 'nvarchar', 'nchar', 'char', 'text', 'varchar', 'ntext') THEN 0 ELSE 1 END,
            @TypeName = t.name
    FROM    sys.columns AS c
            INNER JOIN sys.types AS t
                ON t.user_type_id = c.user_type_id
                AND t.system_type_id = c.system_type_id
    WHERE   c.Name = @Field
    AND     c.object_id = OBJECT_ID(@TableName, 'U')
    AND     t.name IN ('sysname', 'nvarchar', 'nchar', 'char', 'text', 'varchar', 'ntext', 'tinyint',
                     'smallint', 'int', 'real', 'money', 'float', 'decimal', 'numeric', 'smallmoney', 'bigint');


    IF @IsNumericField IS NULL
    BEGIN
        -- If @Numeric field was not set it means the column doesn't exist, 
        -- or the type of the column is not numeric or text
        RAISERROR ('Invalid column or column is not queryable type', 16, 1);
        RETURN;
    END

    -- DECLARE THE WHERE CLAUSE FOR THE DYNAMIX SQL
    DECLARE @SQLWhere NVARCHAR(MAX) = ' WHERE 1 = 1 ';

    -- BUILD UP THE WHERE CLAUSE BASED ON THE CRITERIA AND PARAMETERS PASSED
    SELECT  @SQLWhere += STRING_AGG(CONCAT('AND ', 
                                    QUOTENAME(@Field), 
                                    REPLACE(op.Operator, '{{parameter}}', CONCAT('TRY_CONVERT(', @TypeName, ', ', p.ParameterName, ')'))),
                                 ' ')
    FROM    (VALUES
                (1, 'greater than' , ' > {{parameter}}'),
                (1, 'greater than or equal', ' >= {{parameter}}'),
                (1, 'less than', ' < {{parameter}}'),
                (1, 'less than or equal', ' <= {{parameter}}'),
                (1, 'not equal', ' <> {{parameter}}'),
                (1, 'equal', ' = {{parameter}}'),
                (0, 'equal', ' = {{parameter}}'),
                (0, 'start with', ' LIKE CONCAT({{parameter}}, ''%'')'),
                (0, 'end with', ' LIKE CONCAT({{parameter}}, ''%'')'),
                (0, 'in any position', ' LIKE CONCAT(''%'', {{parameter}}, ''%'')'),
                (0, 'in second position', ' LIKE CONCAT(''_'', {{parameter}}, ''%'')'),
                (0, 'specific character and at least 2 characters in length', ' LIKE CONCAT({{parameter}}, ''_%'')'),
                (0, 'specific character and at least 3 characters in length', ' LIKE CONCAT({{parameter}}, ''__%'')')
            ) op (NumericField, Criterion, Operator)
            INNER JOIN
            (VALUES
                (@Criterion1, @Parameter1, '@Parameter1'),
                (@Criterion2, @Parameter2, '@Parameter2'),
                (@Criterion3, @Parameter3, '@Parameter3')
            ) p (Criterion, ParameterValue, ParameterName)
                ON p.Criterion = op.Criterion
    WHERE   op.NumericField = @IsNumericField;

    SET @SQL += @SQLWhere;

    PRINT @SQL;
    EXECUTE sp_executesql 
                @SQL, 
                N'@Parameter1 NVARCHAR(100), @Parameter2 NVARCHAR(100), @Parameter3 NVARCHAR(100)',
                @Parameter1,
                @Parameter2,
                @Parameter3;
END

附录

如果要传递多个字段,添加新参数(@Field1、@Field2 等),然后调整 table 值构造函数,为字段名称添加另一列:

INNER JOIN
(VALUES
    (@Field1, @Criterion1, @Parameter1, '@Parameter1'),
    (@Field2, @Criterion2, @Parameter2, '@Parameter2'),
    (@Field3, @Criterion3, @Parameter3, '@Parameter3')
) p (ColumnName, Criterion, ParameterValue, ParameterName)
    ON p.Criterion = op.Criterion

然后使用这个新列来构建您的表达式而不是 @Field:

SELECT  @SQLWhere += STRING_AGG(CONCAT('AND ', 
                                QUOTENAME(p.ColumnName),  
                                REPLACE(op.Operator, '
                                        {{parameter}}', 
                                        CONCAT('TRY_CONVERT(', t.Name, ', ', p.ParameterName, ')'))),
                             ' ')
                             
                             

Example on DB<>Fiddle