SQL Server Dynamic SQL Based off Query Results

a11xaf1n  于 11个月前  发布在  其他
关注(0)|答案(3)|浏览(71)

I have a number of vendors, call then V1, V2, V3, etc. that provide data to my team. That data is provided on a monthly basis, and each time we ingest it, the tables are read in as the same format: VENDOR_YYYY_MM (e.g., V1 for 2023-10 data would be V1_2023_10).

I've written out a query (see below) that gives me the names of each table that are the most recent:

use mydb
go

select 
    name
    , ROW_NUMBER() over (partition by 1 order by name desc) as row_num
from sys.Tables
inner join (
    select max(cast(cast(right(name, 4) as varchar(4)) + cast(SUBSTRING(name, len(name) - 6, 2) as varchar(2)) as numeric)) as max_yyyy_mm
    from sys.Tables
    where (name like 'V1_%' OR name like 'V2_%' OR name like 'V3_%' OR
           name like 'V4_%' OR name like 'V5_%' OR name like 'V6_%'OR 
           name like 'V7_%')
    ) t
on cast(cast(right(name, 4) as varchar(4)) + cast(SUBSTRING(name, len(name) - 6, 2) as varchar(2)) as numeric) = t.max_yyyy_mm
WHERE (name like 'V1_%'
OR name like 'V2_%'
OR name like 'V3_%'
OR name like 'V4_%'
OR name like 'V5_%'
OR name like 'V6_%'
OR name like 'V7_%')

Note that name is the table name, based on sys.tables . This gives me the following table:

namerow_num
V1_2023101
V2_2023102
......

I need to dynamically take the value from the first row, where row_num = 1, and use that as the basis for a select statement, so I can select * from V1_202310 , then select * from V2_202310 , etc. I am unsure of how to accomplish this, and am slightly afraid of dynamic SQL. Any tips would be appreciated! My guess is I need to declare some sort of table_name variable, but I'm unsure of how to do that in a situation like this.

qnakjoqk

qnakjoqk1#

Hope this can help give an idea.

First we will create a temporary table to hold most recent table names.

USE mydb;
GO

CREATE TABLE #MostRecentTables (
    TableName VARCHAR(255),
    RowNum INT
);

Second step is to insert the recent table names into that temp table.

INSERT INTO #MostRecentTables (TableName, RowNum)
SELECT 
    name,
    ROW_NUMBER() OVER (ORDER BY name DESC) as row_num
FROM sys.Tables
WHERE name LIKE 'V[1-7]_%'
AND RIGHT(name, 7) = (
    SELECT TOP 1 RIGHT(name, 7)
    FROM sys.Tables
    WHERE name LIKE 'V[1-7]_%'
    ORDER BY CAST(SUBSTRING(name, LEN(name)-6, 4) + SUBSTRING(name, LEN(name)-2, 2) AS INT) DESC
);

The third step would be then to declare a cursor, which you will use to iterate over the table names storede in the temp table.

DECLARE @TableName VARCHAR(255);
DECLARE table_cursor CURSOR FOR 
SELECT TableName 
FROM #MostRecentTables 
ORDER BY RowNum;

And the last step is you would be constructing your dynamic sql statement for each table name within the cursor loop, while executing them with the loop.

OPEN table_cursor;

FETCH NEXT FROM table_cursor INTO @TableName;

WHILE @@FETCH_STATUS = 0
BEGIN

    DECLARE @SQLCommand NVARCHAR(MAX) = N'SELECT * FROM ' + QUOTENAME(@TableName) + ';';
    
    EXEC sp_executesql @SQLCommand;
    
    FETCH NEXT FROM table_cursor INTO @TableName;
END;

Do not forget to close and deallocate the cursor, and also clean the temp table

CLOSE table_cursor;
DEALLOCATE table_cursor;

DROP TABLE #MostRecentTables;
mbyulnm0

mbyulnm02#

First, let's make perfectly clear: There is no way to read a table with dynamic name without dynamic SQL.

I will not cover your answer functionally, but I'll focus on your caution on dynamic-SQL. Know that if you take the following 2 precautions, you will be perfectly safe from its dangers, i.e. injection.

  1. For object names (like table names), all you have to do is use the quotename(@yourVarHere) function. See, Kreetchy's answer already did that. That means no hacker can write something "smart" that bypasses your code - quotename makes sure its input gets appropriate brackets to stay just a simple object name
  2. In somewhat of the same fashion, you should make sure that values are passed as parameters in sp_executesql . So let's say you want to do a select * from @yourTableNameVar where column1=@col1Value . You should NOT do a direct string concatenation like that:
DECLARE @SQLCommand NVARCHAR(MAX) = N'SELECT * FROM ' + QUOTENAME(@TableName) + ' where column1 = ' + @col1Value
     EXEC sp_executesql @SQLCommand

Instead, you use sp_executesql 's second argument to define your parameter(s) and then you pass them to the procedure directly:

DECLARE @stmt NVARCHAR(MAX) = N'SELECT * FROM ' + QUOTENAME(@TableName) + ' where column1 = @col1Value' -- notice that the name of the variable is used, not its value
    DECLARE @params = N'@col1Value int'
    EXEC sp_executesql @stmt,@params,@col1Value = @col1Value

Do these two things, and you have no reason to fear it.

mcvgt66p

mcvgt66p3#

Not a complete answer, so just community wiki, but one thing I would do is convert this:

(name like 'V1_%'
OR name like 'V2_%'
OR name like 'V3_%'
OR name like 'V4_%'
OR name like 'V5_%'
OR name like 'V6_%'
OR name like 'V7_%')

To use a table-value constructor like this:

INNER JOIN (
        VALUES ('V2_%'), ('V3_%'), ('V4_%'), ('V5_%'), ('V6_%'),('V7_%') 
    ) t(key) ON name LIKE t.key

or without the wildcards in the data, like this:

INNER JOIN (
        VALUES ('V2_'), ('V3_'), ('V4_'), ('V5_'), ('V6_'),('V7_') 
    ) t(key) ON name LIKE t.key + '%'

This sets you up to also move these values to a real table and eventually be fully data-driven for this.

Note the _ character also has special meaning for the LIKE predicate, but not in a way that's likely to really matter here.

相关问题