IF EXISTS in SQL Server Cursor Not Working - sql

I have a cursor which works fine but when it gets to this part of the script, it seems to still run the update even though the table doesn't exists:
SET #sql = 'IF (EXISTS (SELECT * FROM ps_vars_' + #datasetid + '))
BEGIN
UPDATE ps_vars_' + #datasetid + '
SET programming_notes = replace(programming_notes, ''Some of the variables listed are source variables.'')
END';
EXEC SP_EXECUTESQL #sql
What am I missing? The #datasetid variable gets passed in correctly too.

DECLARE #tablename sysname
SET #tablename = 'ps_vars' + #datasetid
IF (OBJECT_ID(#tablename, 'U') IS NOT NULL)
BEGIN
SET #sql = ' UPDATE ' + QUOTENAME(#tablename) + '
SET programming_notes = replace(programming_notes, ''Some of the variables listed are source variables.'') ';
EXEC sp_executesql #sql
END

When you use the EXISTS with the table name to see if the table exists you're actually trying to access the table - which doesn't exist. That's why you're getting an error, not because of your UPDATE statement.
Try this instead:
SET #sql = 'IF (OBJECT_ID(''ps_vars_' + #datasetid + ''') IS NOT NULL)
BEGIN
UPDATE ...
END'
Then think about what might be wrong with your database design that requires you to use dynamic SQL like this. Maybe your design is exactly how it needs to be, but in my experience 9 out of 10 times (probably much more) this kind of code is a symptom of a poor design.

Related

MSSQL Dynamic SQL for table creation from view fails only when executed through job

I have a problem with my recurring table creation.
I have a bunch of views that need to be written into tables.
My approach is, that for every view there is, a table should be created and therefore I realized it with dynamic SQL. So that I don't have to touch the job every time I add a view.
My problem is, the code works fine as long as I execute it myself within SSSM.
As soon as I put it in a job and it is executed by schedule or by myself it fails. If I replace the dynamic SQL with the code it produces, the job fails as well. I even put the code into a stored procedure and just executed that from the job and that produced the same result.
The error states that it couldn't convert a nvarchar type into a datetime type.
I checked every view and ran the code for every view/table singly as well as all at once and there was no error.
Has anyone an idea what's wrong here?
Here is the dynamic SQL code that I use:
DECLARE #SQL varchar(max);
SELECT #SQL = COALESCE(#SQL + ' ', '') + 'IF OBJECT_ID(''' + REPLACE(name, 'qry_', 'tbl_') + ''', ''U'') IS NOT NULL DROP TABLE ' + QUOTENAME(REPLACE(name, 'qry_', 'tbl_')) + '; SELECT * INTO ' + QUOTENAME(REPLACE(name, 'qry_', 'tbl_')) + ' FROM ' + QUOTENAME(name) + ';'
FROM sys.views
WHERE LEFT(name, 4) = 'qry_'
EXEC (#sql);
This SQL is untested, however, this should greatly help you. Notice that I do each execution separately. Firstly, this does (actually) mean that if the dynamic SQL fails that it won't propagate out (this may not be intended, however). I also, though, have added PRINT statements so you can inspect the logs and see what was actually done; and what view was failed out. PRINT #SQL; is commented out, as (if I recall correctly), the Agent logs can only hold 8000 or 4000 characters, so printing the value of #SQL would over bloat it.
DECLARE #SQL nvarchar(MAX), #ViewName sysname;
DECLARE qry_views CURSOR FOR
SELECT [name]
FROM sys.views
WHERE [name] LIKE 'qry[_]%'
OPEN qry_views;
FETCH NEXT FROM qry_views
INTO #ViewName;
WHILE ##FETCH_STATUS = 0 BEGIN
PRINT 'Creating table from View ' + QUOTENAME(#ViewName);
SET #SQL = N'IF OBJECT_ID(''' + REPLACE(#ViewName, N'qry_', N'tbl_') + N''', ''U'') IS NOT NULL DROP TABLE ' + QUOTENAME(REPLACE(#ViewName, N'qry_', N'tbl_')) + N';' + NCHAR(10) +
N'SELECT *' + NCHAR(10) +
N'INTO ' + QUOTENAME(REPLACE(#ViewName, N'qry_', N'tbl_')) + NCHAR(10) +
N'FROM ' + QUOTENAME(#ViewName) + N';';
--PRINT #SQL;
EXEC sp_executesql #SQL;
FETCH NEXT FROM qry_views
INTO #ViewName;
END
CLOSE qry_views;
DEALLOCATE qry_views;
In the job, do you call a stored proc by using date literals, like:
exec my_proc #my_param='20180524'
? You can't do this. Assing the values to a var first, then use the var:
declare #my_value date ='20180524'
exec my_proc #my_param=#my_value

How to Create DELETE Statement Stored Procedure Using TableName, ColumnName, and ColumnValue as Passing Parameters

Here is what i'm trying to do. I'm trying to create a stored procedure where I could just enter the name of the table, column, and column value and it will delete any records associated with that value in that table. Is there a simple way to do this? I don't know too much about SQL and still learning about it.
Here is what I have so far.
ALTER PROCEDURE [dbo].[name of stored procedure]
#TABLE_NAME varchar(50),
#COLUMN_NAME varchar(50),
#VALUE varchar(5)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #RowsDeleted int;
DECLARE #sql VARCHAR(500);
SET #sql = 'DELETE FROM (name of table).' + #TABLE_NAME + ' WHERE ' + #COLUMN_NAME + '=' + '#VALUE'
EXEC(#sql)
SET #RowsDeleted=##ROWCOUNT
END
GO
Couple issues
First, you don't need (name of table)
SET #sql = 'DELETE FROM ' + #TABLE_NAME + etc.
In general you should try to include the appropriate schema prefix
SET #sql = 'DELETE FROM dbo.' + #TABLE_NAME + etc.
And in case your table name has special characters perhaps it should be enclosed in brackets
SET #sql = 'DELETE FROM dbo.[' + #TABLE_NAME + ']' + etc.
Since #Value is a string, you must surround it with single quotes when computing the value for #SQL. To insert a single quote into a string you have to escape it by using two single quotes, like this:
SET #SQL = 'DELETE FROM dbo.[' + #TABLE_NAME + '] WHERE [' + #COLUMN_NAME + '] = '''' + #VALUE + ''''
If #VALUE itself contains a single quote, this whole thing will break, so you need to escape that as well
SET #SQL = 'DELETE FROM dbo.[' + #TABLE_NAME + '] WHERE [' + #COLUMN_NAME + '] = '''' + REPLACE(#VALUE,'''','''''') + ''''
Also, ##ROWCOUNT will not populate from EXEC. If you want to be able to read ##ROWCOUNT, use sp_ExecuteSQL instead
EXEC sp_ExecuteSql #SQL
And finally, let me editorialize for a minute--
This sort of stored procedure is not a great idea. I know it seems pretty cool because it is flexible, and that kind of thinking is usually smart when it comes to other languages, but in the database world this approach causes problems, e.g. there are security issues (e.g. injection, and the fact that you need elevated privileges to call sp_executeSql) and there issues with precompilation/performance (because the SQL isn't known ahead of time, SQL Server will need to generate a new query plan each and every time you call this) and since the caller can supply any value for table and column name you have no idea whether this delete statement will be efficient and use indexes or if it will cause a huge performance issue because the table is large and the column is not indexed.
The proper approach is to have a series of appropriate stored procedures with strongly-typed inputs that are specific to each data use case where you need to delete based on criteria. Database engineers should not be trying to make things flexible; you should be forcing people to think through what exactly they are going to need, and implement that and only that. That is the only way to ensure people are following the rules, keeping R/I intact, efficient use of indexes, etc.
Yes, this may seem like repetitive and redundant work, but c'est la vie. There are tools available to generate the code for CRUD operations if you don't like the extra typing.
In addition to some of the information John Wu provided you have to worry about data types and ##ROWCOUNT may not be accurate if there are triggers on your tables and things..... You can get around both of those issues though by casting to nvarchar() and using OUTPUT clause with a temp table to do the COUNT().
So just for fun here is a way you can do it:
CREATE PROCEDURE dbo.[ProcName]
#TableName SYSNAME
,#ColumnName SYSNAME
,#Value NVARCHAR(MAX)
,#RecordCount INT OUTPUT
AS
BEGIN
DECLARE #SQL NVARCHAR(1000)
SET #SQL = N'IF OBJECT_ID(''tempdb..#DeletedOutput'') IS NOT NULL
BEGIN
DROP TABLE #DeletedOutput
END
CREATE TABLE #DeletedOutput (
ID INT IDENTITY(1,1)
ColumnValue NVARCHAR(MAX)
)
DELETE FROM dbo.' + QUOTENAME(#TableName) + '
OUTPUT deleted.' + QUOTENAME(#ColumnName) + ' INTO #DeletedOutput (ColumnValue)
WHERE CAST(' + QUOTENAME(#ColumnName) + ' AS NVARCHAR(MAX)) = ' + CHAR(39) + #Value + CHAR(39) + '
SELECT #RecordCountOUT = COUNT(ID) FROM #DeletedOutput
IF OBJECT_ID(''tempdb..#DeletedOutput'') IS NOT NULL
BEGIN
DROP TABLE #DeletedOutput
END'
DECLARE #ParmDefinition NVARCHAR(200) = N'#RecordCountOUT INT OUTPUT'
EXECUTE sp_executesql #SQL, #ParmDefinition, #RecordCountOUT = #RecordCount OUTPUT
END
So the use of QOUTENAME will help against the injection attack but not be perfect. And I use CHAR(39) instead of the escape sequence for a single quote on value because I find it easier when string building at that point.... By using Parameter OUTPUT from sp_executesql you can still return your count.
Keep in mind just because you can do something in SQL doesn't always mean you should.

Dynamic Sql - Creating a database - Problems with syntax

I'm having issues with a dynamic SQL script in particular this bit:EXEC('
if db_id(''' + $(db) + ''') is null
BEGIN
CREATE DATABASE ' + $(db) + '
END
The if statement part seems to work fine, I know this because if the database exists then the create database line is not run but when it needs to run I just get syntax errors near that line.
I have also tried:
CREATE DATABASE ''' + $(db) + '''
with no luck
Any Ideas?
DECLARE #DB_NAME NVARCHAR(128) = N'Test_DB'
DECLARE #Sql NVARCHAR(MAX);
IF DB_ID(#DB_NAME) IS NULL
BEGIN
SET #Sql = N' CREATE DATABASE ' + QUOTENAME(#DB_NAME)
EXECUTE sp_executesql #Sql
END
Important Note
Make sure your database name is in accordance with the Rules for Regular Identifiers

Update data in a SQL Server temp table where the column names are unknown

In a stored procedure I dynamically create a temp table by selecting the name of applications from a regular table. Then I add a date column and add the last 12 months. The result looks like this:
So far so good. Now I want to update the data in columns by querying another regular table. Normally it would be something like:
UPDATE ##TempTable
SET [columName] = (SELECT SUM(columName)
FROM RegularTable
WHERE FORMAT(RegularTable.Date,'MM/yyyy') = FORMAT(##TempMonths.x,'MM/yyyy'))
However, since I don't know what the name of the columns are at any given time, I need to do this dynamically.
So my question is, how can I get the column names of a temp table dynamically while doing an update?
Thanks!
I think you can use something like the following.
select name as 'ColumnName'
from tempdb.sys.columns
where object_id = object_id('tempdb..##TempTable');
And then generate dynamic sql using something like the following.
DECLARE #tableName nvarchar(50)
SET #tableName = 'RegularTable'
DECLARE #sql NVARCHAR(MAX)
SET #sql = ''
SELECT #sql = #sql + ' UPDATE ##TempTable ' + CHAR(13) +
' SET [' + c.name + '] = (SELECT SUM([' + c.name + ']) ' + CHAR(13) +
' FROM RegularTable' + CHAR(13) +
' WHERE FORMAT(RegularTable.Date,''MM/yyyy'') = FORMAT(##TempMonths.x,''MM/yyyy''));' + CHAR(13)
from tempdb.sys.columns c
where object_id = object_id('tempdb..##MyTempTable');
print #sql
-- exec sp_executesql #sql;
Then print statement in above snippet shows that the #sql variable has the following text.
UPDATE ##TempTable
SET [Test Application One] = (SELECT SUM([Test Application One])
FROM RegularTable
WHERE FORMAT(RegularTable.Date,'MM/yyyy') = FORMAT(##TempMonths.x,'MM/yyyy'));
UPDATE ##TempTable
SET [Test Application Two] = (SELECT SUM([Test Application Two])
FROM RegularTable
WHERE FORMAT(RegularTable.Date,'MM/yyyy') = FORMAT(##TempMonths.x,'MM/yyyy'));
So now, you use sp_exec to execute the updates as follows (un-comment it from above snippet).
exec sp_executesql #sql;
If it's a 1 time UPDATE you can PRINT the dynamic SQL statement (as shown above) and then execute it in the SSMS Query Windows.
I recommend you use the print statement first to make sure the UPDATE statements generated are what you want, and then do the sp_executesql or run the printed UPDATE statement in the query window.

Script to create a schema using a variable

I'm surprised that this hasn't come up before but I haven't found anything in searches either here or elsewhere. I found something vaguely similar which indicates that the problem is that the Use command in my script lasts only for that one line, but there was no indication of how to work around that.
What I'm trying to do: Create a generic script to create a "template" database with all of my common schemas and tables. All of the variables (such as the database name) are intended to be set in the header so that they can be changed as needed and the script can be just run without needing to do any risky search and replace operations to change hard coded values.
What the problem is: I can't get the schemas to generate in the right database; they're all generating in Master. Trying to explicitly set the database didn't help; I just received runtime errors.
My skill level: Long time Access user but still in the foothills of exploring SQL Server. I'm sure (well, hoping) this this will be ridiculously easy for someone further up the slope.
Does anyone know how I can do something like this? (Existing code shown below.)
DECLARE #DBName NVARCHAR(50) = 'TheDBName';
-- Assume that there's a bunch of code to drop and create the database goes here.
-- This code executes correctly.
SET #SQL = 'Use [' + #DBName + ']';
Print #SQL;
EXEC(#SQL);
SET #Counter = 1;
WHILE #Counter <=3
BEGIN
SET #SQL = 'CREATE SCHEMA [' +
CASE #Counter
WHEN 1 THEN 'Schema1'
WHEN 2 THEN 'Schema2'
WHEN 3 THEN 'Schema3'
END
SET #SQL = #SQL + '] AUTHORIZATION [dbo]';
PRINT 'Creating Schemas, ' + #SQL;
Exec(#SQL);
SET #Counter = #Counter + 1;
END
The use command only changes the current db in the scope you are in and dynamic SQL runs in a scope of its own.
Try this from master
declare #SQL nvarchar(max)
set #SQL = N'use tempdb; print db_name()'
exec(#SQL)
print db_name()
Result:
tempdb
master
Try this:
DECLARE #DBName NVARCHAR(50) = 'TheDBName';
DECLARE #SQL NVARCHAR(max)
DECLARE #SQLMain NVARCHAR(max)
DECLARE #Counter int
SET #SQLMain = 'Use [' + #DBName + ']; exec(#SQL)';
SET #Counter = 1;
WHILE #Counter <=3
BEGIN
SET #SQL = 'CREATE SCHEMA [' +
CASE #Counter
WHEN 1 THEN 'Schema1'
WHEN 2 THEN 'Schema2'
WHEN 3 THEN 'Schema3'
END
SET #SQL = #SQL + '] AUTHORIZATION [dbo]';
EXEC sp_executesql #SQLMain, N'#SQL nvarchar(max)', #SQL;
SET #Counter = #Counter + 1;
END
If you run USE statement inside EXEC() then run other statements also in EXEC()
but
you have to use USE databasename stmt. in every EXEC()
Other answers have explained your immediate problem but since this is really a deployment issue, as a general solution you might want to try using SQLCMD variables that you can then set at runtime from the command line. This would allow you to pass the database name and/or schema names into scripts dynamically so you can then automate your deployment using batch files or VS database projects.