SQL Server – How to Use Parameterized Dynamic T-SQL

dynamic-sqlparametersql server

We have a situation where effectively the same report is requested by different customers but they:

  1. Don't want all the columns
  2. Want the columns in a different order than we naturally have them
  3. Want them called something different than how we have is stored ("Customer No" vs "Customer Number" for example)

The intent is to ease the amount of effort needed accommodate these customization requests. We are currently in a position with a couple hundred instances of these basically identical reports (excluding these superficial differences). I am looking to see if I needed once instance of these base Dynamic Queries per Parameter set or if I could handle all possible parameter sets via 1 Stored Procedure. The hope is to also not have to have a bunch of specific instances of some kind of SSRS RDL file or an SSIS DTSX package to handle these changes. That the data would come out of the Stored Procedure as we need it to be displayed/presented.

Lets assume I build out a Dynamic SQL Command where the output looks something like:

SELECT
Col1 AS 'Alias1',
Col2 AS 'Alias2',
Col3 AS 'Alias3'
FROM View
WHERE DateCol >= @StartDate
AND DateCol < @EndDate

It is built from a couple different parts using a couple of tables. Below table structures are more pseudo code to get the ideas across, so please ignore things like there are no Primary Keys declared, etc…

CREATE TABLE [report].[ReportTemplate]
(
    ID INT NOT NULL, --(Primary Key)
    ReportName VarChar(100) NOT NULL,
    ReportTypeID INT NOT NULL --(FK To report.ReportTemplateType.ID)
)

CREATE TABLE [report].[ReportTemplateType]
(
    ID INT NOT NULL, --(Primary Key)
    Name VarChar(50), --(Unique Constraint)
    BaseCommand VarChar(2000), --Holds FROM and JOIN clauses
    WhereCommand VarChar(2000), --Holds WHERE Clause
    WhereCommandParameters VarChar(2000), --Holds declaration of the parameters
)

CREATE TABLE [report].[ReportTemplateColumnDetails]
(
    ID INT NOT NULL, --(Primary Key)
    ReportTemplateID INT NOT NULL, --(FK to report.ReportTemplate.ID)
    ColumnName VarChar(256) NOT NULL,
    ColumnAlias VarChar(256) NULL, --Have logic handle blank vs NULL values
    ColumnOrder SmallInt NOT NULL
)
+----+-------------------+--------------+
| ID |    ReportName     | ReportTypeID |
+----+-------------------+--------------+
|  1 | Customer 1 Status |            1 |
|  2 | Customer 1 Sales  |            2 |
+----+-------------------+--------------+


+----+--------+-----------------+------------------------------------------------------------------------------+-------------------------------------------------------------------+
| ID |  Name  |   BaseCommand   |                                WhereCondition                                |                     WhereConditionParameters                      |
+----+--------+-----------------+------------------------------------------------------------------------------+-------------------------------------------------------------------+
|  1 | Status | FROM StatusView | WHERE DateCol >= @StartDate AND DateCol < @EndDate                           | @StartDate DATEIME, @EndDate DateTime                             |
|  2 | Sales  | FROM SalesView  | WHERE DateCol >= @StartDate AND DateCol < @EndDate AND Col4 = @TypeParameter | @StartDate DATEIME, @EndDate DateTime, @TypeParameter VarChar(20) |
+----+--------+-----------------+------------------------------------------------------------------------------+-------------------------------------------------------------------+

+----+------------------+------------+-------------+-------------+
| ID | ReportTemplateID | ColumnName | ColumnAlias | ColumnOrder |
+----+------------------+------------+-------------+-------------+
|  1 |                1 | Col1       | Alias1      |           1 |
|  2 |                1 | Col2       | Alias2      |           2 |
|  3 |                1 | Col3       | Alias3      |           3 |
|  4 |                2 | Col4       | Alias1      |           1 |
|  5 |                2 | Col5       | Alias2      |           2 |
|  6 |                2 | Col6       | Alias3      |           3 |
+----+------------------+------------+-------------+-------------+

The command is built using code below:

CREATE PROCEDURE [report].[ExecuteReportTemplate] (@ReportName VarChar(50))
AS
BEGIN
    DECLARE @SQLCommand VarChar(MAX) = 'SELECT ',
            @FirstColumnAdded BIT = 0,
            @BaseCommand VarChar(2000),
            @WhereCondition VarChar(2000),
            @WhereConditionParameters VarChar(2000)

    SELECT @BaseCommand = RTT.BaseCommand,
    @WhereCondition = RTT.WhereCommand, 
    @WhereConditionParameters = RTT.WhereCommandParameters 
    FROM [report].[ReportTemplateType] RTT
        INNER JOIN [report].[ReportTemplate] RT
            ON RTT.ID = RT.ReportTypeID
    WHERE RT.Name = @ReportName

    DECLARE @ColumnName VarChar(256),
            @ColumnAlias VarChar(256)

    DECLARE ColumnCursor CURSOR FOR
    SELECT ColumnName,
    ColumnAlias
    FROM [report].[ReportTemplateColumnDetails]
    ORDER BY ColumnOrder

    FETCH NEXT FROM ColumnCursor INTO @ColumnName, @ColumnAlias

    WHILE (@@FETCH_STATUS = 0)
    BEGIN
        --Add a comma inbetween columns, does not happen on the first one
        IF(@FirstColumnAdded = 1)
        BEGIN
            SET @SQLCommand = @SQLCommand + ', '
        END
        ELSE
        BEGIN
            SET @FirstColumnAdded = 1
        END

        --Adds the column into the list
        SET @SQLCommand = @SQLCommand + @ColumnName

        --If we have been provided an alias, set the alias
        IF(@ColumnAlias IS NULL OR LTRIM(RTRIM(@ColumnAlias)) = '')
        BEGIN
            @SQLCommand = @SQLCommand + 'AS ''' + @ColumnAlias + ''' '
        END
    END

    CLOSE ColumnCursor
    DEALLOCATE ColumnCursor

    --Now Add The Base Command
    SELECT @SQLCommand = @SQLCommand + ' ' + @BaseCommand + ' ' + @WhereCommand

    EXECUTE sp_executesql @sqlCommand, @WhereConditionParameters
        @StartDate = '2019-01-01', 
        @EndDate = GETDATE()
END

Is there a way to dynamical change the parameters that are configured and passed in without having to build a separate command?

I would like to be able to populate [report].[ReportTemplateType].[WhereCondition] and [report].[ReportTemplateType].[WhereCondition] with a different WHERE and Parameters. For example adding a 3rd column in the WHERE condition something like Col4 = @TypeParameter. The only way I know to solve this is to create a different Stored Procedure where everything is identical to the above Stored Procedure but we would change the last piece to:

EXECUTE sp_executesql @sqlCommand, @WhereConditionParameters
        @StartDate = '2019-01-01', 
        @EndDate = GETDATE(),
        @TypeParameter = 'SomeStringValue'

Is there a way to dynamical change the parameters that are configured and passed in without having to build a separate command?

Best Answer

Too long for a comment, so I've dumped this down to an answer.

My personal preference is that Dynamic SQL (DSQL) should never be used by production code that is not administrative in nature. I use DSQL all the time when it comes to managing environments, but none of these truly require any significant level of performance. The moment you start pushing Dynamic SQL out as production code you will inevitably run into performance issues. At that moment, the wheels come off because DSQL is notorious to troubleshoot. Obviously, that's just an opinion and you're free to do what you want, but I would heavily discourage the use of DSQL in any code you push to production.

Before you go any further down this road, I would recommend reading, what I feel is the definitive article on Dynamic SQL, by Erland Sommarskog: The Curse and Blessings of Dynamic SQL

It's a really good read; just be warned it will take a little time to digest it all.

I'll jump off my soapbox now....

As for your question:

Is there a way to dynamical change the parameters that are configured and passed in without having to build a separate command?

Yes, you may need to nest your DSQL or add additional report tables accordingly, but only your requirements and approach will dictate the appropriate path. Should you is the more pressing question, and obviously I would say no.