Dynamic SQL, sp_executesql, and rebuilding the dynamic sql statement - Part 1 - tsql

Part 1: In his article, "Dynamic Search Conditions in T-SQL...for SQL 2005 and Earlier", Erland Sommarskog gives an example of how to use dynamic sql with sp_executesql.
http://www.sommarskog.se/dyn-search-2005.html#sp_executesql
SELECT #sql = -- 19
'SELECT o.OrderID, o.OrderDate, od.UnitPrice, od.Quantity, -- 20
c.CustomerID, c.CompanyName, c.Address, c.City, -- 21
c.Region, c.PostalCode, c.Country, c.Phone, -- 22
p.ProductID, p.ProductName, p.UnitsInStock, -- 23
p.UnitsOnOrder -- 24
FROM dbo.Orders o -- 25
JOIN dbo.[Order Details] od ON o.OrderID = od.OrderID -- 26
JOIN dbo.Customers c ON o.CustomerID = c.CustomerID -- 27
JOIN dbo.Products p ON p.ProductID = od.ProductID -- 28
WHERE 1 = 1' -- 29
-- 30
IF #orderid IS NOT NULL -- 31
SELECT #sql = #sql + ' AND o.OrderID = #xorderid' + -- 32
' AND od.OrderID = #xorderid' -- 33
-- 34
IF #fromdate IS NOT NULL -- 35
SELECT #sql = #sql + ' AND o.OrderDate >= #xfromdate' -- 36
etc...
So, as you build your dynamic sql statement, it makes sense if you have to run just one sp_executesql for your #sql variable.
However, let's suppose you've built your #sql, and returned the filtered records you want returned, but you also want a COUNT of the records returned.
What would be the best way to go about doing this?
Would you have to declare another variable, #sql_2, whose build would be nearly identical to #sql, except the SELECT statement in #sql_2 would do a SELECT COUNT(*)... instead of a SELECT col1, col2, col3?
Or is there a better approach to take?

String the SQL statements together separated by semicolons. Here is a working example that returns the tables in your database that start with the letter "A" and the count.
First the simple version. This returns 2 result sets, the second one being the count.
declare #findTables nvarchar(256)
set #findTables = N'A%'
declare #sql nvarchar(max)
set #sql = N'set nocount on; '+
'select * from sys.tables where name like '''+#findTables+''';'+
'select ##RowCount as [RowCount];';
execute sp_executesql #sql
Now a version where a variable gets valued with the count for when you need to use it later in the stored proc.
declare #findTables nvarchar(256)
set #findTables = N'A%'
declare #sql nvarchar(max)
declare #ParmDefinition nvarchar(500);
declare #rowCount int
set #sql = N'set nocount on;
select * from sys.tables where name like #findTablesParm;
select #rowCountParm = ##rowcount;
select #rowCountParm as [RowCount];';
SET #ParmDefinition = N'#findTablesParm nvarchar(256),
#rowCountParm int OUTPUT';
execute sp_executesql #sql,
#ParmDefinition,
#findTablesParm=#findTables,
#rowCountParm=#rowCount OUTPUT
After this runs you should see 2 result sets, the second one will contain the RowCount and the variable #rowCount will also contain the row count.

Related

How to select top 10 rows from information.schema.columns list

SELECT TOP 10
*
FROM
(SELECT
COLUMN_NAME, TABLE_NAME
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
COLUMN_NAME LIKE '%MAIL%')
I am trying to get the top 10 rows from each of the tables within my search. Any ideas? SQL Server 2008 R2
While nscheaffer's answer is valid, I feel obligated to tell you that you can get the exact same functionality without using a cursor, and also while using a query which might be a bit easier to implement.
Just concatenate all of the possible queries together based off the system tables and then execute them simultaneously, like this:
DECLARE #SQL NVARCHAR(MAX)
;
SELECT #SQL =
(
SELECT 'SELECT TOP 10 * FROM ' + OBJECT_NAME(C.Object_ID) + ';' + CHAR(10)
FROM sys.Columns C
INNER JOIN sys.Tables T
ON C.Object_ID = T.Object_ID
AND T.is_ms_shipped = 0
WHERE C.Name LIKE '%Mail%'
GROUP BY C.Object_ID
ORDER BY C.Object_ID
FOR XML PATH('')
)
;
EXEC sp_ExecuteSQL #SQL
;
If you want to check the SQL before it runs, just comment out the EXEC command and replace it with a SELECT, like this:
SELECT #SQL;
--EXEC sp_ExecuteSQL #SQL
;
I would use a cursor to create dynamic SQL and then execute that SQL. Does this work for you?
DECLARE #SQL NVARCHAR (4000) = ''
DECLARE #Table_Name NVARCHAR(50)
DECLARE TableCursor CURSOR FAST_FORWARD READ_ONLY FOR
SELECT DISTINCT TABLE_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE COLUMN_NAME LIKE '%MAIL%'
OPEN TableCursor
FETCH NEXT FROM TableCursor INTO #Table_Name
WHILE ##FETCH_STATUS = 0
BEGIN
SET #SQL = #SQL + '
SELECT TOP 10 * FROM ' + #Table_Name + '
GO
'
FETCH NEXT FROM TableCursor INTO #Table_Name
END
CLOSE TableCursor
DEALLOCATE TableCursor
EXECUTE sp_executeSQL #SQL

Dynamic SQL with nvarchar(max) variable (again)

I have read about a dozen articles here and I am still stumped with this issue.
I am building a dynamic select statement that will update a view on a monthly schedule.
set ansi_nulls on
go
set quoted_identifier on
go
alter procedure [dbo].[Proc_Name_SP]
as
begin
set nocount on
set quoted_identifier off
declare #dbname varchar(10), #schema_id int, #schema_name varchar(10),
#jacro varchar(10), #rec_cnt int, #tot_rec int
declare #SQL_Main nvarchar(max), #SQL_Final nvarchar(max),
#SQL_schema nvarchar(2000), #SQL_Union nvarchar(max)
declare iteration cursor global static for
-- Begin statement for cursor array
select distinct db, code
from linkedserver.db.schema.Directory
where current_stage = 'live'
order by db
-- End statement for cursor array
-- get total number of cursor iterations to know when to stop
-- "union" statements
select #tot_rec = count(*) from (select distinct db, code
from [linkedserver].db.schema.Directory
where current_stage = 'live') as cur
-- begin loop
open iteration
fetch first from iteration into #dbname, #jacro
while ##fetch_status=0
begin
-- the schema used is not consistent. Because of the linked server it was
-- necessary to get the Schema_ID from the sys.tables and then pull the
-- schema name from sys.schema
set #SQL_schema = 'select #sch_id = schema_id from [linkedserver].'+#dbname+'.sys.tables where name = ''Manuscript'''
execute sp_executesql #SQL_schema, N'#sch_id int OUTPUT', #sch_id = #schema_id output
--print #schema_id
set #SQL_schema ='select #sch_name = name from [linkedserver].'+#dbname+'.sys.schemas where schema_id = '+cast(#schema_id as varchar)+''
execute sp_executesql #SQL_schema, N'#sch_name nvarchar(10) OUTPUT', #sch_name = #schema_name output
--print #schema_name
--building Select statement
set #SQL_Main ='
select jcode.Code as BILLING_ACRO
,s.start_dt as BILLING_DATE
,cmpt_ms_nm as MANUSCRIPT
,isnull(jcode.billing_type, ''reviewed'') as Billing_type
from [linkedserver].'+#dbname+'.'+#schema_name+'.Manuscript as m
join [linkedserver].'+#dbname+'.'+#schema_name+'.Step as s on m.ms_id = s.ms_id and m.ms_rev_no = s.ms_rev_no
join (select j_id, Code, billing_type from [linkedserver].db.schema.Directory where db = '''+#dbname+''') as jcode on jcode.j_id = m.j_id
where jcode.Code = '''+#jacro+'''
and m.ms_rev_no = 0
and s.stage_id = 190
and isnull(cmpt_ms_nm, '''') <> ''''
and s.step_id = (select min(s2.step_id)
from [linkedserver].'+#dbname+'.'+#schema_name+'.Step as s2
where s2.stage_id = 190
and s2.ms_id = m.ms_id
and s2.ms_rev_no = m.ms_rev_no)
'
set #rec_cnt = isnull(#rec_cnt, 0) + 1
if #SQL_Union is null
begin
set #SQL_Union = #SQL_Main
end
else if #tot_rec <> #rec_cnt
begin
set #SQL_Union = #SQL_Union + ' union ' + #SQL_Main
end
else
begin
set #SQL_Union = #SQL_Union + #SQL_Main
end
--print #rec_cnt
fetch next from iteration into #dbname, #jacro --next database
end -- while ##FETCH_STATUS=0
close iteration
deallocate iteration
-- build new view
print len(#SQL_Union)
set #SQL_Final = '
ALTER VIEW [dbo].[View_Name_VW]
AS
'+#SQL_Union+'
'
execute sp_executesql #SQL_Final
--grab string variables to table for troubleshooting
insert into Output_SQL(SQL_Final, SQL_Final_Len, SQL_Union, SQL_Union_Len)
select #SQL_Final, LEN(#SQL_Final), #SQL_Union, LEN(#SQL_Union)
set nocount off
end
go
I have read that others have had problems with this type of truncation and I have tried multiple suggestions but in the end the I am getting capped at 68274 in this code with nvarchar(max). For troubleshooting, I am saving the results of the variables and the len of these variables to a table to eliminate the SSMS cap on the display of strings.
I have tried cast(#varible as nvarchar(max)) on the right side of the = sign. I have changed the data type lengths (as the select that is being built is not that large, it is just large after it has been union for each unique customer)
I am open to any suggestions as I have tried many variations of datatype declarations for these variables.

Need help in creating a stored procedure to iterate tables in a database, then run a SQL statement on each table

Our application does not delete data as we retain it for a period of time, instead we have a column "deleted" (bit) in most tables of the database that store data which we mark 1 when deleted, otherwise the default is 0.
I'd like to create a stored procedure that iterates all tables in the database, checks for the existence of a column named "deleted" and if it exists, I run a check against the LastUpdatedUtc column (datetime2) and if the date is over 6 months old and deleted = 1 then we delete the row.
This application is under continuous development so tables could be added which is why I want to create a script that iterates tables instead of having to add a line for each table and remember to add them as new tables are added.
Any help in a SQL Server 2008 R2 stored procedure to this would be a great help.
Thank you.
EDIT (thank you Omaer) here is what I've come up with so far. Anyone that knows a better way let me know.
IF OBJECT_ID('tempdb..#tmpTables') IS NOT NULL DROP TABLE #tmpTables
GO
CREATE TABLE #tmpTables
(
ID INT,
TableName NVARCHAR(100) NOT NULL
)
GO
SET NOCOUNT ON
GO
INSERT #tmpTables
SELECT [object_id], [name] FROM sys.all_objects WHERE type_desc = 'USER_TABLE' ORDER BY [name]
DECLARE #TN NVARCHAR(100)
DECLARE #SQL NVARCHAR(max)
DECLARE #PurgeDate VARCHAR(50)
SET #PurgeDate = DATEADD(MONTH, -6, GETUTCDATE())
WHILE (SELECT COUNT(*) FROM #tmpTables) > 0
BEGIN
SELECT TOP 1 #TN = TableName FROM #tmpTables
IF EXISTS(SELECT * FROM sys.columns WHERE name = 'deleted' AND OBJECT_ID = OBJECT_ID(#TN))
BEGIN
IF EXISTS(SELECT * FROM sys.columns WHERE name = 'LastUpdatedUtc' AND OBJECT_ID = OBJECT_ID(#TN))
BEGIN
SET #SQL = 'SELECT Count(*) As Counter FROM ' + #TN + ' WHERE [deleted] = 1 AND [LastUpdatedUtc] < ''' + #PurgeDate + '''' -- this will be the delete line when the code is final, just outputting results for now
EXEC(#SQL)
END
END
DELETE #tmpTables WHERE TableName=#TN
END
DROP TABLE #tmpTables
This is my first attempt, not tested it so there might be some typos/syntax errors but this should get you started:
declare #date6MonthsBack varchar(50)
select #date6MonthsBack = dateadd(month, -6, getdate());
declare c cursor for
select 'delete from ' + quotename(name) + ' where [deleted] = 1 and [LastUpdatedUtc] <= ''' + #date6MonthsBack + '''' from sys.tables
where object_id in (select object_id from sys.columns where name = 'deleted')
and object_id in (select object_id from sys.columns where name = 'LastUpdatedUtc')
declare #sql varchar(max)
open c; fetch next from c into #sql
while (##fetch_status = 0) begin
print(#sql)
--exec(#sql) --uncomment this line to do the actual deleting once you have verified the commands.
fetch next from c into #sql; end
close c; deallocate c
You could use undocummented sp_MSforeactable procedure instead of loop or cursor. Something like code below. I created procedure that runs your code and is executed with sp_MSforeachtable. The disadvantage is - the procedure is undocumented and may not be supported in next SQL Server releases
IF OBJECT_ID('dbo.usp_cleanup') IS NULL
EXEC ('CREATE PROCEDURE dbo.usp_cleanup AS SELECT 1')
GO
ALTER PROCEDURE dbo.usp_cleanup
#sTblName VARCHAR(200)
AS
BEGIN
-- your variables
DECLARE #PurgeDate VARCHAR(50)
DECLARE #SQL VARCHAR(MAX)
SET #PurgeDate = DATEADD(MONTH, -6, GETUTCDATE())
-- we can check columns existence in one condition
IF
EXISTS(SELECT * FROM sys.columns WHERE name = 'deleted' AND OBJECT_ID = OBJECT_ID(#sTblName))
AND EXISTS(SELECT * FROM sys.columns WHERE name = 'LastUpdatedUtc' AND OBJECT_ID = OBJECT_ID(#sTblName))
BEGIN
SET #SQL = 'SQL CODE GOES HERE' -- this will be the delete line when the code is final, just outputting results for now
PRINT #SQL
--EXEC(#SQL) -- uncomment for execution
END
ELSE
-- for debugging
BEGIN
PRINT #sTblName + ' has no [delete] and [LastUpdatedUtc] columns'
END
END
EXEC sp_MSforeachtable 'exec usp_cleanup ''?'''
GO

Enabling or disabling part of TSQL select statement (columns)

Let's say I have a table with five columns. Full select statement would be :
Select col1, col2, col3, col4, col5
from tbl
But I need this query to be "dynamic", every column must be possible to enable/disable. So, as far as I understand I need five tags (bool let's say).
If i have tag1=0, tag2=1, tag3=1, tag4=0, tag5 =0 then the select statement should be like this:
Select col2, col3
from tbl
If i have tag1=1, tag2=0, tag3=0, tag4=0, tag5 =0 then the select statement should be like this:
Select col1
from tbl
So is there a possibility to do so in TSQL? I intend to create SP and execute it from php.
P.s. I understand that there's a solution to make a bunch of IF statements with all possible tag1...tag5 variations, but this is not very effective when the number of columns (and enable tags) is high.
create procedure GetTblList
#Tag1 bit,
#Tag2 bit,
#Tag3 bit,
#Tag4 bit,
#Tag5 bit
as
declare #SQL nvarchar(max)
set #SQL = 'select '
if #Tag1 = 1 set #SQL = #SQL + 'Col1,'
if #Tag2 = 1 set #SQL = #SQL + 'Col2,'
if #Tag3 = 1 set #SQL = #SQL + 'Col3,'
if #Tag4 = 1 set #SQL = #SQL + 'Col4,'
if #Tag5 = 1 set #SQL = #SQL + 'Col5,'
if #SQL = 'select ' set #SQL = #SQL + 'null as NoColumnSelected'
set #SQL = stuff(#SQL, len(#SQL), 1, ' from TbL')
exec (#SQL)
Even tough it is not a recommended practice the only possible way seems to be creating the sql statement as a string and executing it.
If you build the sql statement string in your application layer you can send it to db layer for execution as a single string. This one should be clear.
On the other hand if you want build the sql statement string at DB level (in an SP for example) you can use sp_executesql stored procedure to execute it. Sample:
DECLARE #sql nvarchar(MAX);
SET #sql = 'SELECT col1'
IF #tag1 = 1 SET #sql = #sql + ', dynamic_col_1';
IF #tag2 = 1 SET #sql = #sql + ', dynamic_col_2';
IF #tag3 = 1 SET #sql = #sql + ', dynamic_col_3';
SET #sql = #sql + ' FROM myTable'
EXEC sp_executesql #sql;
Either case you need to send all your tags as parameters or you can aggregate them into a single 5 digit string parameter where each digit represents a flag.

How to get row count from EXEC() in a TSQL SPROC?

I have a TSQL sproc that builds a query as and executes it as follows:
EXEC (#sqlTop + #sqlBody + #sqlBottom)
#sqlTop contains something like SELECT TOP(x) col1, col2, col3...
TOP(x) will limit the rows returned, so later I want to know what the actual number of rows in the table is that match the query.
I then replace #sqlTop with something like:
EXEC ('SELECT #ActualNumberOfResults = COUNT(*) ' + #sqlBody)
I can see why this is not working, and why a value not declared error occurs, but I think it adequately describes what I'm trying to accomplish.
Any ideas?
use sp_executesql and an output parameter
example
DECLARE #sqlBody VARCHAR(500),#TableCount INT, #SQL NVARCHAR(1000)
SELECT #sqlBody = 'from sysobjects'
SELECT #SQL = N'SELECT #TableCount = COUNT(*) ' + #sqlBody
EXEC sp_executesql #SQL, N'#TableCount INT OUTPUT', #TableCount OUTPUT
SELECT #TableCount
GO
You could instead have the dynamic query return the result as a row set, which you would then insert into a table variable (could be a temporary or ordinary table as well) using the INSERT ... EXEC syntax. Afterwards you can just read the saved value into a variable using SELECT #var = ...:
DECLARE #rowcount TABLE (Value int);
INSERT INTO #rowcount
EXEC('SELECT COUNT(*) ' + #sqlBody);
SELECT #ActualNumberOfResults = Value FROM #rowcount;
Late in the day, but I found this method much simpler:
-- test setup
DECLARE #sqlBody nvarchar(max) = N'SELECT MyField FROM dbo.MyTable WHERE MyOtherField = ''x''';
DECLARE #ActualNumberOfResults int;
-- the goods
EXEC sp_executesql #sqlBody;
SET #ActualNumberOfResults = ##ROWCOUNT;
SELECT #ActualNumberOfResults;
After executing your actual query store the result of ##ROWCOUNT in any variable which you can use later.
EXEC sp_executesql 'SELECT TOP 10 FROM ABX'
SET #TotRecord = ##ROWCOUNT into your variable for later use.
Keep in mind that dynamic SQL has its own scope. Any variable declared/modified there will go out of scope after your EXEC or your sp_executesql.
Suggest writing to a temp table, which will be in scope to your dynamic SQL statement, and outside.
Perhaps put it in your sqlBottom:
CREATE TABLE ##tempCounter(MyNum int);
EXEC('SELECT #ActualNumberOfResults = COUNT(*) ' + #sqlBody +
'; INSERT INTO ##tempCounter(MyNum) VALUES(#ActualNumberOfResults);');
SELECT MyNum FROM ##tempCounter;
You can use output variable in SP_EXECUTESQL
DECLARE #SQL NVARCHAR(MAX);
DECLARE #ParamDefinition NVARCHAR(100) = '#ROW_SQL INT OUTPUT'
DECLARE #AFFECTED_ROWS INT;
SELECT
#SQL = N'SELECT 1 UNION ALL SELECT 2'
SELECT #SQL += 'SELECT #ROW_SQL = ##ROWCOUNT;';
EXEC SP_EXECUTESQL #SQL, #ParamDefinition, #ROW_SQL=#AFFECTED_ROWS OUTPUT;
PRINT 'Number of affected rows: ' + CAST(#AFFECTED_ROWS AS VARCHAR(20));
Ouput:
SQL2.sql: Number of affected rows: 2
Thanks Jesus Fernandez!
The only problem with the answers that create temporary tables (either using "DECLARE #rowcount TABLE" or "CREATE TABLE ##tempCounter(MyNum int)") is that you're having to read all the affected records off disk into memory. If you're expecting a large number of records this may take some time.
So if the answer is likely to be large the "use sp_executesql and an output parameter" solution is a more efficient answer. And it does appear to work.