I am using cursor to randomly copy data from one table(Family_tree) to two other tables(Family_tree1 + #time, Family_tree2 + #time). Code executes successfully, but no row is updated. There IS actually what to copy from a table.
I am using Microsoft SQL Server Management Studio. Here's the part of script with cursor:
---creating two tables beforehand
DECLARE #random int
DECLARE
#first_name nvarchar(20),
#last_name AS nvarchar(20),
#date_of_birth AS nchar(10),
#date_of_death AS nchar(10),
#place_of_death AS nvarchar(30),
#credit_card AS nchar(16),
#ID_member AS int,
#FK_gender AS nchar(3),
DECLARE curs CURSOR FOR SELECT first_name, last_name, date_of_birth, date_of_death, place_of_death, credit_card, ID_member, FK_gender, FROM Family_tree
OPEN curs
FETCH NEXT FROM curs INTO #first_name, #last_name, #date_of_birth, #date_of_death, #place_of_death, #credit_card, #ID_member, #FK_gender,
WHILE ##FETCH_STATUS = 0
BEGIN
SET #random = RAND() * 1000000
IF #random % 2 = 1
BEGIN
SET #sqlString = 'INSERT INTO [Family_tree1' + #time + '] (first_name, last_name, date_of_birth, date_of_death, place_of_death, credit_card, ID_member, FK_gender)
VALUES ('
+ #first_name + ',' + #last_name + ',' + #date_of_birth + ',' + #date_of_death + ',' + #place_of_death + ',' + #credit_card + ','
+ CAST(#ID_member AS nvarchar) +','+ #FK_gender + ')'
END
ELSE
BEGIN
SET #sqlString = 'INSERT INTO [Family_tree2' + #time + '] (first_name, last_name, date_of_birth, date_of_death, place_of_death, credit_card, ID_member, FK_gender)
VALUES (' + #first_name + ',' + #last_name + ',' + #date_of_birth + ',' + #date_of_death + ',' + #place_of_death + ',' + #credit_card + ','
+ CAST(#ID_member AS nvarchar) +','+ #FK_gender + ')'
END
EXECUTE(#sqlString)
FETCH NEXT FROM curs INTO #first_name, #last_name, #date_of_birth, #date_of_death, #place_of_death, #credit_card, #ID_member, #FK_gender
END
CLOSE curs
DEALLOCATE curs
END;
I am new to T-SQL and will appreciate any advice!
Thank You in advance (:
If you "have" to do it in the cursor, then you need to watch out for nulls. You also need to watch out for the fact that you're using strings and these should be quoted when inserted into a VALUES clause.
Instead of
VALUES (' + #first_name + ',
You need something like:
VALUES (' + COALESCE('''' + REPLACE(#first_name,'''','''''') + '''','NULL') + ',
And so on for the rest of your values. This replaces any single quotes within the value with doubled-up quotes, then wraps the whole string in single quotes. NULLs survive through all of that processing so we then also use COALESCE to replace the NULL with a NULL literal in the eventual string1.
Before running the cursor in anger, I'd suggest you do this for one row and print the string rather than executing it, to check that it "looks right".
I'd also suggest you look into using better data types - dates of birth/death would be much better dealt with as actual date values rather than strings.
1Guido suggested ISNULL in the comments, which is similar to COALESCE but has some odd limitations and I'd usually recommend against using. They also suggested that the replacement should be an empty string, but here that would result in VALUES(... ,, ...) at the position of the NULL value which would generate an error.
Related
Im trying to use a stored procedure to insert some string values into a remote DB with Openquery. The argument #val_a can sometimes be some random string, but sometimes it can be NULL.
Following syntax works if #val_a is NULL, but not if it's a string 'asdf'.
DECLARE #TSQL nvarchar(4000);
SELECT #TSQL =
'UPDATE
OPENQUERY(TEST,''SELECT * FROM test_db WHERE id = ' + convert(VARCHAR(MAX), #id) +''')
SET
val_a = ' + ISNULL(convert(VARCHAR(MAX), #val_a), 'NULL') + ';'
EXEC (#TSQL)
But, in order to work with the string 'asdf', the syntax has look like this:
DECLARE #TSQL nvarchar(4000);
SELECT #TSQL =
'UPDATE
OPENQUERY(TEST,''SELECT * FROM test_db WHERE id = ' + convert(VARCHAR(MAX), #id) +''')
SET
val_a = ''' + ISNULL(convert(VARCHAR(MAX), #val_a), 'NULL') + ''';'
EXEC (#TSQL)
But here, NULLs are inserted as the string 'NULL', not as a NULL value.
Is there a way to write the TSQL query in such form that both NULL and 'asdf' are inserted correctly in the table?
You may use like that:
val_a = ' + ISNULL(convert(VARCHAR(MAX), '''' + #val_a + ''''), 'NULL') + ';'
I'm working with Dynamic SQL (still in the learning phase) and I'm stuck at a part where I need to use a WHILE loop:
SET #tableName = (SELECT DISTINCT TableName FROM #dataStructure)
Here basically I want to make sure that the operations inside the while loop should occur for all the tables in the #tableName (defined above). I don't know how I can give this condition as an input for the while loop.
WHILE() #HOW CAN I PUT THE CONDITION HERE????
BEGIN
SET #str = ''
SET #sqlstr = ''
SELECT #table = TableName FROM #dataStructure
SET #str = 'UPDATE a0' + char(13) + char(10)
+ ' SET a0.Mjolnir_Source_ID = CONCAT( '
SELECT #str = #str + IIF(ReferenceTable IS NULL, 'a0.' + columnName , alias + '.Mjolnir_Source_ID') + ','
FROM #dataStructure
WHERE TableName = #tableName AND ReferenceTable IS NOT NULL
ORDER BY columnName
SELECT #str = #str + ') FROM ' + #table + ' a0'
SELECT #sqlstr = #sqlstr + +
+ ' INNER JOIN ' + QUOTENAME(#U4SM_db_name) + '.dbo.' + QUOTENAME(ReferenceTable) + ' ' + alias + char(13) + char(10)
+ ' ON a0.' + columnName + ' = ' + alias + '.' + ReferenceColumn + char(13) + char(10)
FROM #dataStructure
WHERE TableName = #tableName AND ReferenceTable IS NOT NULL
ORDER BY columnPosition
select #str + #sqlstr
select #sqlstr
SET #tableName = #tableName + 1
END
Can anyone please help me out here?
Here's an example of a WHILE loop. Basically, you get the first TableName, then if it's NOT NULL, you do your functions. Then get the next table name, and repeat as necessary.
DECLARE #CurrentTableName nvarchar(100)
DECLARE #CustomSQL nvarchar(4000)
SET #CurrentTableName = (SELECT TOP 1 TableName FROM #dataStructure ORDER BY TableName)
WHILE #CurrentTableName IS NOT NULL
BEGIN
SET #CustomSQL = 'SELECT TOP 10 * FROM ' + #CurrentTableName
EXEC (#CustomSQL)
SET #CurrentTableName = (SELECT TOP 1 TableName FROM #dataStructure WHERE TableName > #CurrentTableName ORDER BY TableName)
END
Note that SQL commands often cannot contain variable names in key spots (e.g., SELECT * FROM #tableName). Instead, you save it as an SQL string (what I've called #CustomSQL above) and then EXEC it (put brackets around the variable name though).
Edit: Do this on a test site first before production, and know where the 'cancel query' button is. It's not often, but it's also not unknown, that the 'getting the next row' part isn't properly written and it just runs in a perpetual loop.
FETCH CURSOR with WHILE. Example:
DECLARE myCursor CURSOR FOR
SELECT DISTINCT TableName FROM #dataStructure;
OPEN myCursor;
FETCH NEXT FROM myCursor INTO #table:Name;
WHILE ##FETCH_STATUS = 0
BEGIN
Print ' ' + #TableName
FETCH NEXT FROM myCursor INTO #TableName;
END;
CLOSE myCursor;
DEALLOCATE myCursor;
GO
Don't recreate the wheel unless you need a better wheel:
sp_MSforeachtable
https://www.sqlshack.com/an-introduction-to-sp_msforeachtable-run-commands-iteratively-through-all-tables-in-a-database/
If you are worried about using an undocumented procedure in production that might change in the future, simply script it out and create your own custom named version.
I have built a stored procedure that aims to identify duplicates in a table and to display the duplicated rows in a meaningful order. It looks like this:
CREATE PROCEDURE [dbo].[spFindDuplicates]
#tableName nvarchar(255),
#field1 nvarchar(255),
#field2 nvarchar(255) = '1',
#field3 nvarchar(255) = '2',
#field4 nvarchar(255) = '3',
#field5 nvarchar(255) = '4'
AS
BEGIN
DECLARE #query AS nvarchar(MAX);
SET #query = '
SELECT *
FROM ' + #tableName + '
WHERE CAST(' + #field1 + ' AS nvarchar(255)) + CAST(' + #field2 + ' AS nvarchar(255)) + CAST(' + #field3 + ' AS nvarchar(255)) + CAST(' + #field4 + ' AS nvarchar(255)) + CAST(' + #field5 + ' AS nvarchar(255))
IN
(
SELECT CAST(' + #field1 + ' AS nvarchar(255)) + CAST(' + #field2 + ' AS nvarchar(255)) + CAST(' + #field3 + ' AS nvarchar(255)) + CAST(' + #field4 + ' AS nvarchar(255)) + CAST(' + #field5 + ' AS nvarchar(255))
FROM ' + #tableName + '
GROUP BY CAST(' + #field1 + ' AS nvarchar(255)) + CAST(' + #field2 + ' AS nvarchar(255)) + CAST(' + #field3 + ' AS nvarchar(255)) + CAST(' + #field4 + ' AS nvarchar(255)) + CAST(' + #field5 + ' AS nvarchar(255))
HAVING COUNT(*) > 1
)
ORDER BY ' + #field1 + ', ' + #field2 + ', ' + #field3 + ', ' + #field4 + ', ' + #field5
EXECUTE(#query);
END
GO
--Example:
EXEC spFindDuplicates #tableName = 'someRandomTable', #field1 = 'firstField', #field2 = 'secondField', #field3 = 'thirdField'
As you can see, I can use at most 5 different fields that I concatenate in order for me to get a key used to determine whether we have a duplicate or not. Please note that I use the CAST function to be able to concatenate fields with various datatypes (varchar, int, dates, etc.).
When I execute the above stored procedure with 5 different fields, it works fine. But I would like to be able to run it with a variable number of fields (from 1 to 5), which is why I provided default values for #field2 to #field5.
But when I execute it with the above example (3 fields provided), I get the following error message:
A column has been specified more than once in the order by list. Columns in the order by list must be unique.
QUESTION: How can I keep ordering the resulting table without getting an error?
BONUS QUESTION: If you find a dynamic way to use that stored procedure with any number of fields (4, 17, or whatever), that'd be even more useful to me.
Like I said in the comments, injection is a huge problem here, and you need to consider it. Saying "Let's consider I don't mind about injection" is naïve and you need to change that attitude. Always make your SQL safe; then there are no excuses and chances for your application being compromised.
As what you are after, I suspect this achieves the goal. There's no need for the subquery to scan your table with an IN here, you can make use of COUNT and the OVER clause within a CTE.
CREATE PROCEDURE [dbo].[FindDuplicates] --I've removed te sp prefix, as sp_ is reserved by MS
#tableName sysname,
#field1 sysname,
#field2 sysname = NULL,
#field3 sysname = NULL,
#field4 sysname = NULL,
#field5 sysname = NULL
AS BEGIN
DECLARE #query AS nvarchar(MAX);
SET #query = N'WITH CTE AS(' + NCHAR(10) +
N' SELECT *' + NCHAR(10) +
N' COUNT(*) OVER (PARTITION BY ' + STUFF(CONCAT(N',' + QUOTENAME(#field1),N',' + QUOTENAME(#field2),N',' + QUOTENAME(#field3),N',' + QUOTENAME(#field4),N',' + QUOTENAME(#field5)),1,1,N'') + N' AS RowCount' + NCHAR(10) +
N' FROM ' + QUOTENAME(#tableName) + N')' + NCHAR(10) +
N'SELECT *' + NCHAR(10) +
N'FROM CTE' + NCHAR(10) +
N'WHERE RowCount > 1' + NCHAR(10) +
N'ORDER BY ' + STUFF(CONCAT(N',' + QUOTENAME(#field1),N',' + QUOTENAME(#field2),N',' + QUOTENAME(#field3),N',' + QUOTENAME(#field4),N',' + QUOTENAME(#field5)),1,1,N'') + N';';
PRINT #query;
--EXEC sys.sp_executesql #query; --Uncomment to rrun the actual query
END
GO
For the command you gave us EXEC dbo.FindDuplicates #tableName = 'someRandomTable', #field1 = 'firstField', #field2 = 'secondField', #field3 = 'thirdField';, this returns the SQL:
WITH CTE AS(
SELECT *
COUNT(*) OVER (PARTITION BY [firstField],[secondField],[thirdField] AS RowCount
FROM [someRandomTable])
SELECT *
FROM CTE
WHERE RowCount > 1
ORDER BY [firstField],[secondField],[thirdField];
Which, I believe gives you the behaviour you are after.
Edited the code to check if the column list exists on the sys.columns there by making sure we get only the columns which are appropriate.
CREATE FUNCTION dbo.fn_SplitString
(
#List NVARCHAR(MAX),
#Delimiter NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#List, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
ALTER PROCEDURE [dbo].[spFindDuplicates]
#tableName nvarchar(255),
#columnlist nvarchar(max)
AS
BEGIN
DECLARE #query AS nvarchar(MAX);
SET #columnlist = (SELECT STUFF((SELECT ','+'['+[name]+']'
FROM SYS.columns
WHERE object_id = object_id(#tableName)
AND [Name] IN
(
SELECT Item
FROM dbo.fn_SplitString(#columnlist,',')
)
FOR XML PATH('')
)
,1,1,''))
PRINT #columnlist
SET #query = 'SELECT * FROM (SELECT '+CAST(#columnlist AS NVARCHAR(MAX))+'
FROM '+CAST(#tableName AS nvarchar(MAX))+'
GROUP BY '+CAST(#columnlist AS NVARCHAR(MAX))+'
HAVING COUNT(*) > 1)Res1
ORDER BY '+#columnlist
EXEC SP_EXECUTESQL #query;
END
GO
I'm trying to do a cursor over a SQL Server database to get all the Distinct values from each table.
I want the table with values to look like this:
Database Schema Table Field Value
A dbo T1 F1 1
A dbo T1 F1 2
A dbo T1 F1 3
... ... ... ... ...
My trouble is that when I try to put the database name, with QUOTENAME(TABLE_CATALOG), It doesn't evaluate the function, and instead, it writes the function to the table that I want to generate.
I've tried with single/double quotes without results.
¿How should I write the query? Thanks!
Set Nocount ON
DECLARE cur CURSOR FOR
SELECT 'SELECT DISTINCT ' + ''' QUOTENAME(TABLE_CATALOG) + '''+' As DBName,'+ + QUOTENAME(c.COLUMN_NAME) + ' AS Datos' + ' FROM ' + QUOTENAME(TABLE_CATALOG) + '.' + QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME)
FROM INFORMATION_SCHEMA.COLUMNS AS c
WHERE c.DATA_TYPE LIKE '%char%'
DECLARE #cmd VARCHAR(MAX);
OPEN cur;
FETCH NEXT FROM cur INTO #cmd;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #cmd
EXEC(#cmd);
FETCH NEXT FROM cur INTO #cmd;
END
CLOSE cur;
DEALLOCATE cur;
Set Nocount OFF
BTW: I took the code and edited it from an example (Get distinct values from each table and each column with SQL Server)
You did not get quotes quite right with the ''' QUOTENAME(TABLE_CATALOG) + '''. It should be ''' + QUOTENAME(TABLE_CATALOG) + '''.
Quotes are escaped by doubling them up e.g.
SELECT 'blah' --> blah
SELECT 'it''s' --> it's
SELECT ' Alex''' + 's pen' --> Alex's pen
Syntax error
SELECT 'it's' --> syntax error
The finished query is here:
SET NOCOUNT ON
DECLARE cur CURSOR FOR
SELECT 'SELECT DISTINCT ''' + QUOTENAME(TABLE_CATALOG) + ''' As DBName, ''' + QUOTENAME(TABLE_SCHEMA) + ''' AS [Schema], ''' + QUOTENAME(COLUMN_NAME) + ''' AS ColumnName, '
+ QUOTENAME(c.COLUMN_NAME) + ' AS Datos ' +
'FROM ' + QUOTENAME(TABLE_CATALOG) + '.' + QUOTENAME(TABLE_SCHEMA) + '.' + QUOTENAME(TABLE_NAME)
FROM INFORMATION_SCHEMA.COLUMNS AS c
WHERE c.DATA_TYPE LIKE '%char%'
DECLARE #cmd VARCHAR(MAX);
OPEN cur;
FETCH NEXT FROM cur INTO #cmd;
WHILE ##FETCH_STATUS = 0
BEGIN
PRINT #cmd
EXEC(#cmd);
FETCH NEXT FROM cur INTO #cmd;
END
CLOSE cur;
DEALLOCATE cur;
SET NOCOUNT OFF
Notes:
Remove QUOTENAME if you do not want the string to be surrounded by [] (square brakets)
A comment on INFORMATION_SCHEMA.COLUMNS: for your purposes this view WILL return correct data as stated in the https://stackoverflow.com/a/7310746/6305294 (comment from #ShannonSeverance). What this view lacks is detailed information about column properties.
I am currently working on an SQL Server 2005, and trying to structure a dynamic query as follows:
DECLARE #GETDATE AS NVARCHAR(12);
DECLARE #GETDATE2 AS NVARCHAR(12);
SET #GETDATE = ...;
SET #GETDATE2 = ...;
SET #SQL =
'CREATE TABLE [dbo].[' + #TABLENAME + ']'
+'('
+'ShibNo' 'INT'
+')'
+';'
+ CHAR(10)
+'INSERT INTO [dbo].[' + #TABLENAME + '] (ShibNo)'
+'SELECT X.[ShibNo]'
+'FROM'
+'('
+'SELECT'
+ 'I.[Shibno]'
+',' + 'I.[ShibAzmnDate]'
+',' + 'I.[ShibBeginTime]'
+',' + 'I.[ShibEndTime]'
+',' + 'I.[CarNum]'
+',' + 'I.[DriverNo1]'
+',' + 'I.[ShibKind]'
+',' + 'I.[FStationID]'
+',' + 'I.[LStationID]'
+',' + 'I.[LineDetailRecordID]'
+'FROM Inserted2 I'
+'WHERE I.[ShibAzmnDate] BETWEEN ' + #GETDATE + ' AND ' + #GETDATE2 +
+'INTERCEPT'
+'SELECT'
+ 'D.[Shibno]'
+',' + 'D.[ShibAzmnDate]'
+',' + 'D.[ShibBeginTime]'
+',' + 'D.[ShibEndTime]'
+',' + 'D.[CarNum]'
+',' + 'D.[DriverNo1]'
+',' + 'D.[ShibKind]'
+',' + 'D.[FStationID]'
+',' + 'D.[LStationID]'
+',' + 'D.[LineDetailRecordID]'
+'FROM Deleted2 D'
+'WHERE D.[ShibAzmnDate] BETWEEN ' + #GETDATE + ' AND ' + #GETDATE2 +
+') AS X'
+';'
;
EXECUTE sp_executesql #SQL
;
As you can see, there are parameters in the WHERE clauses of the query string which are meant to restrict the date range of the checks performed here. However, when the string is executed as a query using sp_executesql, the dates don't receive the proper apostrophe punctuation, which creates errors.
I have tried using replace and escaping the characters, but apparently do not know the proper way to do this. I would be very happy (and grateful!) to learn how to do this correctly.
The string returned if I check the build-up of the query is one of the following variations:
' WHERE D.[ShibAzmnDate] BETWEEN ''03/13/2016'' AND ''03/14/2016'' '
OR
' WHERE D.[ShibAzmnDate] BETWEEN 03/13/2016 AND 03/14/2016 '
OR
' WHERE D.[ShibAzmnDate] BETWEEN ''''03/13/2016'''' AND ''''03/14/2016'''' '
and so on...
Can someone please help me to understand how to properly structure this dynamic query string (and future dynamic query strings) to avoid this issue?
Many, many thanks in advance!
Use parameter placeholders in the query, and then pass the values of the parameters to sp_executesql. See https://msdn.microsoft.com/en-us/library/ms188001.aspx for more details.
DECLARE #GETDATE AS NVARCHAR(12);
DECLARE #GETDATE2 AS NVARCHAR(12);
SET #GETDATE = ...;
SET #GETDATE2 = ...;
SET #SQL =
'CREATE TABLE [dbo].[' + #TABLENAME + ']'
+'('
+'ShibNo' 'INT'
+')'
+';'
+ CHAR(10)
+'INSERT INTO [dbo].[' + #TABLENAME + '] (ShibNo)'
+'SELECT X.[ShibNo]'
+'FROM'
+'('
+'SELECT'
+ 'I.[Shibno]'
. . .
+'FROM Inserted2 I'
+'WHERE I.[ShibAzmnDate] BETWEEN #StartDate AND #EndDate'
+'INTERCEPT'
+'SELECT'
+ 'D.[Shibno]'
. . .
+'FROM Deleted2 D'
+'WHERE D.[ShibAzmnDate] BETWEEN #StartDate AND #EndDate'
+') AS X'
+';'
;
EXECUTE sp_executesql #SQL, N'#StartDate DATE, #EndDate DATE',
#StartDate = #GETDATE, #EndDate = #GETDATE2
;
Try it is working for me.
DECLARE #DATE VARCHAR(250) = '2016-01-01', #VAR VARCHAR(MAX)
SELECT #VAR = 'SELECT * FROM TABLE_A WHERE CREATE_DTE> '''+#DATE+''''
SELECT #VAR
1 MSSQL supports multiline string literals so you don't have to concat each line separately (don't quite understand why are you concatenating comma as a separate litera)
2 Since you are using sp_executesql and have variables of appropriate type you don't need them in sql text and don't have to convert them to varchar
SET #SQL =
cast('CREATE TABLE [dbo].[' as nvarchar(max)) + #TABLENAME + ']
(
ShibNo INT
);
INSERT INTO [dbo].[' + #TABLENAME + '] (ShibNo)
SELECT X.[ShibNo]
FROM
(
SELECT
I.[Shibno]
,I.[ShibAzmnDate]
,I.[ShibBeginTime]
,I.[ShibEndTime]
,I.[CarNum]
,I.[DriverNo1]
,I.[ShibKind]
,I.[FStationID]
,I.[LStationID]
,I.[LineDetailRecordID]
FROM Inserted2 I
WHERE I.[ShibAzmnDate] BETWEEN #date1 AND #date2
INTERCEPT
SELECT
D.[Shibno]
,D.[ShibAzmnDate]
,D.[ShibBeginTime]
,D.[ShibEndTime]
,D.[CarNum]
,D.[DriverNo1]
,D.[ShibKind]
,D.[FStationID]
,D.[LStationID]
,D.[LineDetailRecordID]
FROM Deleted2 D
WHERE D.[ShibAzmnDate] BETWEEN #date1 AND #date2
) AS X'
EXECUTE sp_executesql #SQL, N'#date1 datetime, #date2 datetime', #getdate, #getdate2;