I'm trying to build the from clause in a query. I need to obtain the database name and append a portion of the name to a cross database query. (select * from ('x' + database.table))
select '[' + LEFT (DB_NAME(), CHARINDEX('-', DB_NAME())) + 'DatabaseName.Table]'
The statement above returns the value. I had thought I could use this to populate a dynamic query but it's not working.
DECLARE #sql NVARCHAR(MAX)
DECLARE #dynamicSql NVARCHAR(MAX)
SET #sql= 'select '[' + LEFT (DB_NAME(), CHARINDEX('-', DB_NAME())) + 'DatabaseName.Table]''
SET #dynamicSql ='SELECT t.*
FROM
(
'+#sql+'
) t'
EXECUTE sp_executesql #dynamicSql
If you want to dynamically construct a qualified table name, fill in the strings directly into the dynamic sql string, i.e. you do not need to use a ( SELECT ...) construct in the from clause.
Something like this:
DECLARE #sql NVARCHAR(MAX)
DECLARE #dynamicSql NVARCHAR(MAX)
SET #sql= '[' + LEFT (DB_NAME(), CHARINDEX('-', DB_NAME())) + 'DatabaseName.Table]'
SET #dynamicSql ='SELECT t.*
FROM '+#sql+'
'
print #dynamicSql
/*
Output in database 'Book-Collection':
SELECT t.*
FROM [Book-DatabaseName.Table]
*/
Mind that the database name, schema name and table name can each be quoted with square brackets, but putting the whole of the qualified identifier within brackets will make Sql Server read it as a 1 long non-qualified table name.
Also do not forget to put the schema name between the database name and the table name.
Related
I'm trying to fetch the data in a specific table name by passing tableName as a parameter to the stored procedure.
CREATE PROCEDURE schemaName.spDynamicTableName
#tableName NVARCHAR(100)
AS
BEGIN
DECLARE #sql nvarchar(max)
SET #sql = 'SELECT * FROM ' + #tableName
EXECUTE sp_executesql #sql
END;
--> EXEC schemaName.spDynamicTableName 'Employee';
Now, how can I pass list of table names to a procedure so that procedure will iterate over the list of table names and fetch the data from all the tables?
Ok, let's start off with the problems you have in your current set up. Firstly it sounds like you have a design flaw here. Most likely you are using a table's name to infer information that should be in a column. For example perhaps you have different tables for each client. In such a scenario the client's name should be a column in a singular table. This makes querying your data significantly easier and allows for good use for key constraints as well.
Next, your procedure. This is a huge security hole. The value of your dynamic object is not sanitised nor validated meaning that someone (malicious) has almost 100 characters to mess with your instance and inject SQL into it. There are many articles out there that explain how to inject securely (including by myself), and I'm going to cover a couple of processes here.
Note that, as per my original paragraph, you likely really have a design flaw, and so that is the real solution here. We can't address that in the answers here though, as we have no details of the data you are dealing with.
Fixing the injection
Injecting Securely
The basic's of injecting a dynamic object name is to make it secure. You do that by using QUOTENAME; it both delimit identifies the object name and escapes any needed characters. For example QUOTENAME(N'MyTable') would return an nvarchar with the value [MyTable] and QUOTENAME(N'My Alias"; SELECT * FROM sys.tables','"') would return the nvarchar value "My Alias""; SELECT U FROM sys.tables".
Validating the value
You can easily validate a value by checking that the object actually exists. I prefer to do this with the sys objects, so something like this would work:
SELECT #SchemaName = s.[name],
#TableName = t.[name]
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
WHERE s.[name] = #Schema --This is a parameter
AND t.[name] = #Table; --This is a parameter
As a result, if the FROM returns no values, then the 2 variables in the SELECT won't have a value assigned and no SQL will be run (as {String} + NULL = NULL).
The Solution
Table Type Parameter
So, to allow for multiple tables, we need a table type parameter. I would create one with both the schema and table name in the columns, but we can default the schema name.
CREATE TYPE dbo.Objects AS table (SchemaName sysname DEFAULT N'dbo',
TableName sysname); --sysname is a sysnonym for nvarchar(128) NOT NULL
And you can DECLARE and INSERT into the TYPE as follows:
DECLARE #Objects dbo.Objects;
INSERT INTO #Objects (TableName)
VALUES(N'test');
Creating the dynamic statement
Assuming you are using a supported version of SQL Server, you'll have access to STRING_AGG; this removes any kind of looping from the procedure, which is great for performance. If you're using a version only in extended support, then use the "old" FOR XML PATH method.
This means you can take the values and create a dynamic statement along the lines of the below:
SET #SQL = (SELECT STRING_AGG(N'SELECT * FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';',' ')
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN #Objects O ON s.name = O.SchemaName
AND t.name = O.TableName);
The Stored Proecure
Putting all this together, this will give you a procedure that would look like this:
CREATE PROC schemaName.spDynamicTableName #Objects dbo.Objects AS
BEGIN
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SET #SQL = (SELECT STRING_AGG(N'SELECT N' + QUOTENAME(t.[name],'''') + N',* FROM ' + QUOTENAME(s.[name]) + N'.' + QUOTENAME(t.[name]) + N';',#CRLF) --I also inject the table's name as a column
FROM sys.schemas s
JOIN sys.tables t ON s.schema_id = t.schema_id
JOIN #Objects O ON s.name = O.SchemaName
AND t.name = O.TableName);
EXEC sys.sp_executesql #SQL;
END;
And then you would execute it along the lines of:
DECLARE #Objects dbo.Objects;
INSERT INTO #Objects (SchemaName,TableName)
VALUES(N'dbo',N'MyTable'),
(N'dbo',N'AnotherTable');
EXEC schemaName.spDynamicTableName #Objects;
This one accepts a comma delimited list of tables and guards against SQL injection with a simple QUOTENAME escape (not sure if this is quite enough though):
IF OBJECT_ID('dbo.spDynamicTableName') IS NOT NULL DROP PROC dbo.spDynamicTableName
GO
/*
EXEC dbo.spDynamicTableName 'Students,Robert--
DROP TABLE Students'
*/
CREATE PROC dbo.spDynamicTableName
#tableName NVARCHAR(100)
AS
BEGIN
DECLARE #sql nvarchar(max)
SELECT #sql = STRING_AGG('SELECT * FROM ' + QUOTENAME(value), ';')
FROM STRING_SPLIT(#tableName, ',')
--PRINT #sql
EXEC dbo.sp_executesql #sql
END;
GO
There are two ways you can do this: use a string that contains the names you want and are separated by a special character as:
Table1, Table2, Table3
and split it in the stored procedure (check this)
The second method: make a typo as follows:
CREATE TYPE [dbo].[StringList] AS TABLE
(
[TableName] [NVARCHAR(50)] NULL
)
Add a parameter for your stored procedure as StringList:
CREATE PROCEDURE schemaName.spDynamicTableName
#TableNames [dbo].[StringList] READONLY,
AS
BEGIN
END;
Then measure its length using the following code and make a repeat loop::
DECLARE #Counter INT
DECLARE #TableCount INT
SELECT #TableCount = Count(*), #Counter = 0 FROM #TableNames
WHILE #Counter < #TableCount
BEGIN
SELECT #TableName = Name
FROM #TableNames
ORDER BY Name
OFFSET #Counter ROWS FETCH NEXT 1 ROWS ONLY
SET #sql = 'SELECT * FROM ' + #TableName
EXECUTE sp_executesql #sql
SET #Counter = #Counter + 1
END
We have a View in SQL Server that constantly evolving.
We want to show it in a report as it is (If we add/remove a field in the View, we don't have to modify the report and add/remove manuelly the field).
A sort of a table/matrix that is refreshing by itself.
Thank you by advance for your help.
Without using a table or matrix? Why not? You can't really do this without using one of these controls...
Ignoring that for a moment though, the problem you will face is that the dataset query has to always return the same structure every time it is run so you can't point it directly at a query that is constantly changing.
The only way you might be able to do this is to write a query that unpivots your table/view into another structure and then report on that. By using a matrix, you could reconstruct the table in the report.
There are drawbacks to this approach. All value data needs to be cast to a constant datatype so if each row has a mix of text and numeric values, they would all have to be converted to text.
This approach also assumes there is a key column on the table/view.
Below is a simple example of the kind of thing I mean. This is based on the sample 'AdventureWorksDW2016' database in case you want to test it.
DECLARE #Schema sysname = 'dbo' -- Schema where table/view resides
DECLARE #Table sysname = 'DimGeography' -- name of table or view to read from
DECLARE #KeyColumn sysname = 'GeographyKey' -- name of keycolumn, assumed to be INT in this exmaple
SELECT
COLUMN_NAME, ORDINAL_POSITION, DATA_TYPE
into #t
FROM INFORMATION_SCHEMA.COLUMNS t
WHERE TABLE_SCHEMA = #Schema AND TABLE_NAME = #Table
AND COLUMN_NAME != #KeyColumn
DECLARE #OrdPos int
DECLARE #ColName sysname
DECLARE #sql varchar(max) = ''
CREATE TABLE #result (KeyID int, ColumnName sysname, ColumnPosition int, ColumnValue varchar(75)) -- <= Update 75 to suit maximum column length
WHILE EXISTS(SELECT * FROM #t)
BEGIN
SELECT TOP 1 #OrdPos = ORDINAL_POSITION, #ColName = COLUMN_NAME FROM #t
SET #SQL = 'INSERT INTO #result SELECT ' + #KeyColumn + ', ''' + #ColName + ''', ' + CAST(#OrdPos as varchar(10)) + ', CAST(' + #ColName + ' as varchar(200)) FROM ' + QUOTENAME(#Schema) + '.' + QUOTENAME(#Table)
EXEC (#sql)
DELETE FROM #t WHERE ORDINAL_POSITION = #OrdPos
END
SELECT * FROM #result
If we take a look at the results (just for 2 keyid vales for simplicity) we can see we have a consistent structure.
SELECT * FROM #result where keyid in (207,208) order by KeyID, ColumnPosition
Now, you can build a simple report using a Matrix, have a row group that groups by KeyID and have a column group that groups by ColumnName. The column group sorting can be set to ColumnPosition and the matrix 'data' cell set to ColumnValue.
This whole process will effectively recreate the table/view and be dynamic.
Using SQL Server 2012, I've found that trying to build up a string based on an nvarchar(max) column in a table doesn't seem to work correctly. It seems to overwrite, instead of append. Arbitrary Example:
DECLARE #sql nvarchar(max);
SELECT #sql = N'';
SELECT #sql += [definition] + N'
GO
'
FROM sys.sql_modules
WHERE OBJECT_NAME(object_id) LIKE 'dt%'
ORDER BY OBJECT_NAME(object_id);
PRINT #sql;
This SHOULD print out all the SQL module definitions for all the various dt_ tables in the database, separated by GO, as a script that could then be run. However... this prints out only the LAST module definition, not the sum of all of them. It's behaving as if the "+=" were just an "=".
If you change it just slightly... cast [definition] to an nvarchar(4000) for example, it suddenly works as expected. Also, if you choose any other column that is NOT an nvarchar(max) or varchar(max) type, it works as expected. Example:
DECLARE #sql nvarchar(max);
SELECT #sql = N'';
SELECT #sql += CAST([definition] AS nvarchar(4000)) + '
GO
'
FROM sys.sql_modules
WHERE OBJECT_NAME(object_id) LIKE 'dt%'
ORDER BY OBJECT_NAME(object_id);
PRINT #sql;
Is this a known bug? Or is this working as expected? Am I doing something wrong? Is there any way for me to make this work correctly? I've tried a dozen different things, including ensuring every expression in the concatenation is the same nvarchar(max) type, including the string literal.
NOTE: The example is just an example that shows the problem, and not exactly what I'm trying to do in real life. If your database doesn't have the "dt*" tables defined, you can change the WHERE clause to specify any group of tables or stored procedures in any database you want, you'll get the same result... only the last one shows up in the #sql string, as if you just did "=" instead of "+=". Also, explicitly stating "#sql = #sql + " behaves the same way... works correctly with every string type EXCEPT nvarchar(max) or varchar(max).
I've verified that none of the [definition] values is NULL as well, so there are no NULL shenanigans going on.
The += operator only applies to numeric data types in SQL Server. Microsoft documentation here
For string concatenation, you need to write the assignment and concatenation separately.
DECLARE #sql nvarchar(max);
SELECT #sql = N'';
SELECT #sql = #sql + [definition] + N'
GO
'
FROM sys.sql_modules
WHERE OBJECT_NAME(object_id) LIKE 'dt%'
ORDER BY OBJECT_NAME(object_id);
PRINT #sql;
Also, if you are running this query in Management Studio, keep in mind that there is a limit to the size of the data that it will return (including in a print statement). So if the definitions of your modules exceed this limit, they will be truncated in the output.
To add on to #HABO's comment, the behavior of aggregate string concatenation is undefined and results are plan dependent. Use FOR XML to provide deterministic results and honor the ORDER BY clause:
DECLARE #sql nvarchar(max);
SET #sql =
(SELECT [definition] + N'
GO
'
FROM sys.sql_modules
WHERE OBJECT_NAME(object_id) LIKE 'dt%'
ORDER BY OBJECT_NAME(object_id)
FOR XML PATH(''), TYPE).value('(./text())[1]', 'nvarchar(MAX)');
PRINT #sql; --SSMS will truncate long strings
It works as expected:
DECLARE #sql nvarchar(max);
SELECT #sql =COALESCE(#sql + N' GO ','')+[definition]
FROM sys.sql_modules
Print #sql;
The following code demonstrates taking a column value from a set of ordered rows and, using for xml, creating an aggregated string with separators, including line breaks, between values.
-- Carriage return and linefeed characters.
declare #CRLF as NVarChar(16) = NChar( 13 ) + NChar( 10 );
-- Carriage return and linefeed characters XML encoded.
declare #EncodedCRLF as NVarChar(16) = N'
';
-- Separator to insert between source rows in the accumulated string.
declare #Separator as NVarChar(64) = ';' + #EncodedCRLF + 'go;' + #EncodedCRLF + 'do something with ';
-- Characters in #Separator to remove from the first element in #Result .
declare #SkipCount as Int = 4 + 2 * Len( #EncodedCRLF );
-- The result.
declare #Result as NVarChar( max ) = '';
-- Do it.
select #Result =
Replace(
Stuff(
-- Specify the sorting order of the XML elements in the following select statement.
( select #Separator + name from sys.tables order by name desc for XML path(''), type).value('.[1]', 'VarChar(max)' ),
1, #SkipCount, '' ),
#EncodedCRLF, #CRLF );
-- Output the result.
select #Result as [Result];
-- Output the result with line breaks shown.
print #Result; -- Switch to Messages tab in SSMS to see result.
I´m pretty sure the PRINT command is the reason for the troubles.
When I try:
DECLARE #SQL NVARCHAR(MAX) = N'';
SELECT #sql += SUBSTRING( [definition], 1, 100 ) + CHAR(13) + CHAR(10) + 'GO' + CHAR(13) + CHAR(10)
FROM SYS.SQL_MODULES
PRINT #sql;
I get all my procedures (the first 100 chars ...). So it seems to be a limitation to the PRINT command.
Second try, another database, without "substring":
DECLARE #SQL NVARCHAR(MAX) = N'';
SELECT #sql += [definition] + CHAR(13) + CHAR(10) + 'GO' + CHAR(13) + CHAR(10)
FROM SYS.SQL_MODULES
PRINT #sql;
Now I got 1,5 procedures: The 1st one complete, the second stopped in the middle of on line in the proc. There is a limit in PRINT
Third try, to proof my assumption:
DECLARE #SQL NVARCHAR(MAX) = N'';
SELECT #sql += [definition] + 'GO' FROM SYS.SQL_MODULES
PRINT #sql;
Now I got one line of code more! So for me, this is the proof, that PRINT has somehow a limit.
A last attempt:
DECLARE #SQL NVARCHAR(MAX) = N'';
SELECT #sql += [definition] + N'GO' FROM SYS.SQL_MODULES
PRINT SUBSTRING(#SQL,1,4000)
PRINT SUBSTRING(#SQL,4001,4000)
The result: Now I got more then 2 procedures, although there now is a line break after 4K, this may occur on ... bad places.
I'm trying to compare two resultsets from queries that are stored in variables.
I've tried the following:
DECLARE #sql1 varchar(8000) = 'SELECT * FROM table1'
DECLARE #sql2 varchar(8000) = 'SELECT Col2, Col1 FROM table1'
IF EXISTS(
(EXEC sp_executesql #sql1
EXCEPT
EXEC sp_executesql #sql2)
UNION ALL
(EXEC sp_executesql #sql2
EXCEPT
EXEC sp_executesql #sql1))
This approach has two problems: the if statement doesn't like EXEC statements (EXCEPT UNION ALL EXCEPT structure works when you use the actual queries instead of the variables).
The second problem is that, even if you use the actual queries, the order of the columns of both queries have to be the same or the resulsets will not match. For my purposes however, I need those resultsets to match. I think I need a way to order the columns but I'm not sure if that's even possible.
EDIT:
I have no control over the incoming queries because this is code for an application that's meant to check an answer query of a student against the teacher's query. I can't choose to not use *.
What you try to achieve is something that is not meant to be done in SQL Server. A rather clean way would be to have a code that executes each query and compares both resulting data sets, by metadata and value by value.
I do not recommend you to use the following approach.
For testing purposes I created this stored procedure:
IF EXISTS (SELECT * FROM sys.objects WHERE type = 'P' AND name = 'SP_CompareQueryResults')
DROP PROCEDURE SP_CompareQueryResults
GO
CREATE PROCEDURE SP_CompareQueryResults
(
#sql1 NVARCHAR(4000)
, #sql2 NVARCHAR(4000)
)
AS
BEGIN
DECLARE #q1 NVARCHAR(MAX) = #sql1
, #q2 NVARCHAR(MAX) = #sql2
IF OBJECT_ID('tempdb..##q1') IS NOT NULL
DROP TABLE ##q1
IF OBJECT_ID('tempdb..##q2') IS NOT NULL
DROP TABLE ##q2
SET #q1 = 'SELECT * INTO ##q1 FROM (' + #q1 + ') r'
SET #q2 = 'SELECT * INTO ##q2 FROM (' + #q2 + ') r'
BEGIN TRY
EXEC (#q1)
EXEC (#q2)
END TRY
BEGIN CATCH
SELECT 'One of the source queries are not valid.'
RETURN
END CATCH
DECLARE #r NVARCHAR(MAX)
SELECT #r = COALESCE(#r + ', ', ' ') + COLUMN_NAME
FROM (
SELECT COLUMN_NAME
FROM tempdb.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '##q1'
INTERSECT
SELECT COLUMN_NAME
FROM tempdb.INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = '##q2'
) r
SET #r = 'SELECT 1 as SourceQuery, * FROM (SELECT ' + #r + ' FROM ##q1 EXCEPT SELECT' + #r + ' FROM ##q2) r'
+ ' UNION ALL SELECT 2 as SourceQuery, * FROM (SELECT ' + #r + ' FROM ##q2 EXCEPT SELECT' + #r + ' FROM ##q1) r'
BEGIN TRY
EXEC(#r)
END TRY
BEGIN CATCH
SELECT 'Queries have not matching metadata.'
RETURN
END CATCH
IF OBJECT_ID('tempdb..##q1') IS NOT NULL
DROP TABLE ##q1
IF OBJECT_ID('tempdb..##q2') IS NOT NULL
DROP TABLE ##q2
END
GO
It finds columns with the same names from both queries and compares each of the queries results, returning rows from query 1 not included in query 2 and the other way around.
Let's say you have two queries with following results:
and another one:
As you can see the second query has an additional column, columns are not in the same order and only one tuple is in both queries.
Executing the above SP like this:
EXEC SP_CompareQueryResults
#sql1 = N'
SELECT 1 AS ID
, ''test'' AS Value
, CAST(1 as BIT) AS Valid
UNION ALL
SELECT 2, ''test2'', 0',
#sql2 = N'
SELECT 1 AS ID
, CAST(1 as BIT) AS Valid
, ''test'' AS Value
, ''test'' AS AnotherValue
UNION ALL
SELECT 2, 1, ''test2'', ''whatever'''
gives you the none matching tuples from both queries:
If both queries yield the same results, the SP_CompareQueryResults will not return any rows, so you could say they have same values for the columns with matching names. Same thing happens if both queries have no resulting rows, giving a false positive. You can tweak the above procedure for your needs.
DO NOT USE this code in a production environment as it can be SQL injected and I have not tested this. Generally avoid dynamic sql, if not necessary.
As first stated, try to write for example a c# code is sql injection safe and compares the results.
I have a language table and I want to select aliases from that table according to the specified language.
ALTER PROCEDURE sp_executesql
(#parameter1 NVARCHAR(MAX)
,#parameter2 NVARCHAR(MAX)
,#code NVARCHAR(MAX),#language NVARCHAR(MAX))
DECLARE #sql NVARCHAR(MAX)
SET #sql = 'SELECT '+#parameter1+' AS (SELECT #language FROM Languages WHERE code=somecolumn) '+#paramter2+' AS (SELECT #language FROM Languages WHERE code='+#code+') FROM mytable'
EDIT:
in Stored Procedure, I need something like that.
Thanks for answers..
You cannot use a subquery to build an alias in that way, you would need to use dynamic sql to do this.
DECLARE #language NVARCHAR(255) -- or whatever type your field is
SELECT #language=language FROM Languages WHERE code=#code
DECLARE #sql NVARCHAR(MAX) = 'SELECT ' + #parameter1 + ' AS ' + QUOTENAME(#language) + ' FROM MyTable'
EXEC sp_executesql #sql
(Note the inclusion of QUOTENAME around the alias - this is a safety feature in case of your alias names having invalid characters.)
You can repeat the code above for the second parameter inside your stored procedure.
Try this:
CREATE PROCEDURE sp_NameOfSP
(#parameter1 NVARCHAR(MAX)
,#parameter2 NVARCHAR(MAX)
,#code NVARCHAR(MAX)
,#language NVARCHAR(MAX))
AS
BEGIN
DECLARE #sql NVARCHAR(MAX)
SELECT TOP(1) #language=LanguageColumn FROM Languages WHERE code=somecolumn
SET #sql = 'SELECT '+#parameter1+' AS '+#language+', '
SELECT TOP(1) #language=LanguageColumn FROM Languages WHERE code=#code
SET #sql=#sql+#paramter2+' AS '+#language+' FROM mytable'
EXEC(#SQL)
END
Replace LanguageColumn with proper column name from Languages table