Can someone please explain why the below query works? I assume the first DECLARE uses a VARCHAR that's long enough to hold the table name. But why does the second DECLARE use a VARCHAR and why does it's corresponding query need to be wrapped in 'quotes'?
USE Northwind
DECLARE #TableName VARCHAR(25)=
(Select top 1 tab.name
From Sys.tables tab
Where name not like 'dtproperties'
and name not like 'sysdiagrams'
order by tab.name asc)
DECLARE #Output VARCHAR(100) =
'SELECT COUNT(*) AS [CountOf_' + #TABLENAME + ']
FROM [' + #TABLENAME + ']'
EXEC(#Output)
The datatype of #TableName being VARCHAR(25) is incorrect (or at least a poor choice). Most objects (Tables, Views, Stored Procedures, Functions, etc) have a datatype of sysname which is an alias for NVARCHAR(128). So no, the first DECLARE uses a datatype that is not only not long enough, but would also not be able to hold a wide range of otherwise valid Unicode characters.
The second DECLARE uses a VARCHAR(100) because it is making two possibly bad assumptions:
that there will never be any Unicode characters, and
that the names of the tables will never be more than 62 characters long (that's the amount of characters left after you remove the rest of the characters shown in that query)
The query is wrapped in quotes and submitted via the EXEC() (i.e. it is Dynamic SQL) since neither the columns nor the tables of a query can be variables.
Related
I'm using Microsoft SQL server management studio.
I would like to add a new column to a table (altertable1), and name that column using the data from a cell (Date) of another table (stattable1).
DECLARE #Data nvarchar(20)
SELECT #Data = Date
FROM stattable1
WHERE Adat=1
DECLARE #sql nvarchar(1000)
SET #sql = 'ALTER TABLE altertable1 ADD ' + #Data + ' nvarchar(20)'
EXEC (#sql)
Executing this, I get the following error and can't find out why:
"Incorrect syntax near '2021'."
The stattable1 looks like this:
Date |Adat
2021-09-08 |1
2021-09-08 is a daily generated data:
**CONVERT(date,GETDATE())**
Just like Larnu said in comment, maybe this is not a main problem for you, but if you want to do this add [ ] when you want to name column starting with number.
Like this:
SET #sql = 'ALTER TABLE altertable1 ADD [' + #Data + '] nvarchar(20)'
And of course, naming columns by date or year is not best practice.
The problem with your overall design is that you seem to be adding a column to the table every day. A table is not a spreadsheet and you should be storing data for each day in a row, not in a separate column. If your reports need to look that way, there are many ways to pivot the data so that you can handle that at presentation time without creating impossible-to-maintain technical debt in your database.
The problem with your current code is that 2021-06-08 is not a valid column name, both because it starts with a number, and because it contains dashes. Even if you use a more language-friendly form like YYYYMMDD (see this article to see what I mean), it still starts with a number.
The best solution to the local problem is to not name columns that way. If you must, the proper way to escape it is to use QUOTENAME() (and not just manually slap [ and ] on either side):
DECLARE #Data nvarchar(20), #sql nvarchar(max);
SELECT #Data = Date
FROM dbo.stattable1
WHERE Adat = 1;
SET #sql = N'ALTER TABLE altertable1
ADD ' + QUOTENAME(#Data) + N' nvarchar(20);';
PRINT #sql;
--EXEC sys.sp_executesql #sql;
This also demonstrates your ability to debug a statement instead of trying to decipher the error message that came from a string you can't inspect.
Some other points to consider:
if you're declaring a string as nvarchar, and especially when dealing with SQL Server metadata, always use the N prefix on any literals you define.
always reference user tables with two-part names.
always end statements with statement terminators.
generally prefer sys.sp_executesql over EXEC().
some advice on dynamic SQL:
Protecting Yourself from SQL Injection - Part 1
Protecting Yourself from SQL Injection - Part 2
In T-SQL, I can create a table variable using syntax like
DECLARE #table AS TABLE (id INT, col VARCHAR(20))
For now, if I want to create an exact copy of a real table in the database, I do something like this
SELECT *
FROM INFOMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'MY_TABLE_NAME'
to check the column datatype and also max length, and start to create the #table variable, naming the variable, datatype and max_length one by one which is not very effective. May I know if there is any simpler way to do it like
DECLARE #table AS TABLE = SOME_REAL_TABLE_IN_DATABASE
Furthermore, is there any way to retrieve the column name, data type and max length of the column and use it directly in the declaration like
DECLARE #table AS TABLE (#col1_specs)
Thank you in advance.
EDIT:
Thanks for the answers and comments, we can do that for #table_variable but only in dynamic SQL and it is not good for maintainability. However, we can do that using #temp_table.
Based on the answer by Ezlo, we can do something like this :
SELECT TABLE.* INTO #TEMP_TABLE FROM TABLE
For more information, please refer to this answer.
Difference between temp table and table variable (stackoverflow)
Difference between temp table and table variable (dba.stackexchange)
Object names and data types (tables, columns, etc.) can't be parameterized (can't come from variables). This means you can't do the following (which would be required to copy a table structure, for example):
DECLARE #TableName VARCHAR(50) = 'Employees'
SELECT
T.*
FROM
#TableName AS T
The only workaround is to use dynamic SQL:
DECLARE #TableName VARCHAR(50) = 'Employees'
DECLARE #DynamicSQL VARCHAR(MAX) = '
SELECT
T.*
FROM
' + QUOTENAME(#TableName) + ' AS T '
EXEC (#DynamicSQL)
However, variables (scalar and table variables) declared outside the dynamic SQL won't be accessible inside as they lose scope:
DECLARE #VariableOutside INT = 10
DECLARE #DynamicSQL VARCHAR(MAX) = 'SELECT #VariableOutside AS ValueOfVariable'
EXEC (#DynamicSQL)
Msg 137, Level 15, State 2, Line 1
Must declare the scalar variable "#VariableOutside".
This means that you will have to declare your variable inside the dynamic SQL:
DECLARE #DynamicSQL VARCHAR(MAX) = 'DECLARE #VariableOutside INT = 10
SELECT #VariableOutside AS ValueOfVariable'
EXEC (#DynamicSQL)
Result:
ValueOfVariable
10
Which brings me to my conclusion: if you want to dynamically create a copy of an existing table as a table variable, all the access of your table variable will have to be inside a dynamic SQL script, which is a huge pain and has some cons (harder to maintain and read, more prone to error, etc.).
A common approach is to work with temporary tables instead. Doing a SELECT * INTO to create them will inherit the table's data types. You can add an always false WHERE condition (like WHERE 1 = 0) if you don't want the actual rows to be inserted.
IF OBJECT_ID('tempdb..#Copy') IS NOT NULL
DROP TABLE #Copy
SELECT
T.*
INTO
#Copy
FROM
YourTable AS T
WHERE
1 = 0
The answer for both questions is simple NO.
Although, I agree with you that T-SQL should change in this way.
In the first case, it means having a command to clone a table structure.
Of course, there is a possibility to make your own T-SQL extension by using SQLCLR.
My stored procedure may be passed an arbitrary string of characters #String as a varchar or a nvarchar, no more than 128 characters long.
The stored procedure needs to use that #String value as an object name, i.e. the name of a table, field, view or similar.
How can I determine if the value of #String as supplied is a valid object name in itself - i.e. is not a reserved word and does not contain invalid characters - or if it will require enclosure within (and escape of any contained) square brackets in order to make it a valid object name.
For example, "X", "Foo" and "C:\Temp\X.csv" are valid object names (yes, I've successfully used "C:\Temp\X.csv" as-is) and can be used unmodified, while "Select", "AB]C" and "1_ABC" are not valid, and would need to be changed to "[Select]", "[AB]]C]" and "[1_ABC]"
I would prefer a solution that doesn't simply attempt to create an object with that name and then checks to see if it worked.
Preference will be given to answers that are applicable over a wider range of SQL Server versions rather than only the latest version, and shorter, less complex code will be preferred over longer, more complex code.
As an example of the problem that led me to ask this question, my SP can be passed a #Name, which might be "C:\Temp\X.csv" or "ABC" or "SELECT" or whatever. My SP then creates a table, populates it and then (so I don't have to use dynamic SQL) changes it's name to #Name using sp_rename. I need to QUOTENAME(#Name) where #Name = "SELECT", but not in the other two cases, yielding (in the SSMS Object Explorer) tables "dbo.C:\Temp\X.csv", "dbo.ABC" and "dbo.SELECT". However, I also need to check if these objects exist before I run the rest of the SP, and if I QUOTENAME "C:\Temp\X.csv", before I pass it as the new name to sp_rename, it appears in the SSMS Object Explorer as "dbo.[C:\Temp\X.csv]", and I must use IF OBJECT_ID('[[C:\Temp\X.csv]]]') IS NOT NULL to successfully determine that it exists, not IF OBJECT_ID('[C:\Temp\X.csv]') IS NOT NULL.
When using sp_rename, the new name for the object never needs to be escaped, so just use the passed in #Name value.
It's true that OBJECT_ID might want an escaped name sometimes, but since a more direct test is available that, again, doesn't require escaping, I'd suggest using that as your test instead:
IF EXISTS(SELECT * from sys.tables where name = #Name)
...
(Or you can query sys.objects instead, up to you)
I had originally envisaged that you were using the name in dynamic SQL. In dynamic SQL contexts, I'd suggest just always escaping the name using QUOTENAME, rather than trying to determine if it needs escaping.
This code may help understand when to use QUOTENAME. It is not used in sp_rename.
CREATE PROCEDURE TestObjectNames
#Name SYSNAME
AS
DECLARE #SQL NVARCHAR(MAX);
-- Create temporary table
CREATE TABLE Temp (ID INT, Name SYSNAME);
-- Load temporary table
INSERT INTO Temp SELECT database_id, name FROM sys.databases;
-- Check existance of table
IF NOT EXISTS(SELECT * FROM sys.tables WHERE name = #Name)
-- Rename table (Don't use QUOTENAME)
EXEC sp_rename 'Temp', #Name;;
-- Fetch object id of new table (Use QUOTENAME)
SELECT OBJECT_ID(QUOTENAME(#Name)) AS NewTableID;
-- Select from table dynamically (Use QUOTENAME)
SET #SQL = 'SELECT COUNT(*) AS NewTableCount FROM ' + QUOTENAME(#Name);
EXEC sp_executesql #SQL;
-- Drop new table dynamically (Use QUOTENAME)
SET #SQL = 'DROP TABLE ' + QUOTENAME(#Name);
EXEC sp_executesql #SQL;
GO
EXEC TestObjectNames 'SELECT';
EXEC TestObjectNames 'C:\Temp\X.csv';
EXEC TestObjectNames 'AB]C';
EXEC TestObjectNames '1_ABC';
EXEC TestObjectNames 'ABC';
I have a question for best use when creating where clause in a SQL Procedure.
I have written a query three different ways one using Coalesce in where clause, one using a isnull or statement, and one which is dynamic using sp_executesql.
Coalesce:
WHERE ClientID = COALESCE(#Client, ClientID) AND
AccessPersonID = COALESCE(#AccessPerson, AccessPersonID)
IsNull Or:
WHERE (#Client IS NULL OR #Client = ClientID)
AND (#AccessPerson IS NULL OR #AccessPerson= AccessPersonID)
and dynamically:
SET #sql = #sql + Char(13) + Char(10) + N'WHERE 1 = 1';
IF #Client <> 0
BEGIN
SET #sql = #sql + Char(13) + Char(10) + N' AND ClientID = #Client '
END
IF #AccessPerson <> 0
BEGIN
SET #sql = #sql + Char(13) + Char(10) + N' AND AccessPersonID = #AccessPerson '
END
When I use SQL Sentry Plan Explorer the results show for the estimated that the Coalesce is the best but the the lest accurate between estimated and actual. Where the dynamic has the worst estimated but it is 100% accurate to the actual.
This is a very simple procedure I am just trying to figure out what is the bes way to write procedures like this. I would thin the dynamic is the way to go since it is the most accurate.
The correct answer is the 'dynamic' option. It's good you left parameters in because it protects against SQL Injection (at this layer anyway).
The reason 'dynamic' is the best is because it will create a query plan that is best for the given query. With your example you might get up to 3 plans for this query, depending on which parameters are > 0, but each plan generated one will be optimized for that scenario (they will leave out unnecessary parameter comparisons).
The other two styles will generate one plan (each), and it will only be optimized for the parameters you used AT THAT TIME ONLY. Each subsequent execution will use the old plan and might be cached using the parameter you are not calling with.
'Dynamic' is not as clean-code as the other two options, but for performance, it will give you the optimal query plan each time.
And the dynamic SQL operates in a different scope than your sproc will, so even though you declare a variable in your sproc, you'll have to redeclare it in your dynamic SQL. Or concat it into the statement. But then you should also do NULL checks in your dynamic SQL AND in your sproc, because NULL isn't equal to 0 nor is it not equal to 0. You can't compare it because it doesn't exist. :-S
DECLARE #Client int = 1
, #AccessPerson int = NULL
;
DECLARE #sql nvarchar(2000) = N'SELECT * FROM ##TestClientID WHERE 1=1'
;
IF #Client <> 0
BEGIN
SET #sql = CONCAT(#sql, N' AND ClientID = ', CONVERT(nvarchar(10), #Client))
END
;
IF #AccessPerson <> 0
BEGIN
SET #sql = CONCAT(#sql, N' AND AccessPersonID =', CONVERT(nvarchar(10), #AccessPerson))
END
;
PRINT #sql
EXEC sp_ExecuteSQL #sql
Note: For demo purposes, I also had to modify my temp table above and make it a global temp instead of a local temp, since I'm calling it from dynamic SQL. It exists in a different scope. Don't forget to clean it up after you're done. :-)
Your top two statements don't do quite the same things if either value is NULL.
http://sqlfiddle.com/#!9/d0aa3/4
IF OBJECT_ID (N'tempdb..#TestClientID', N'U') IS NOT NULL
DROP TABLE #TestClientID;
GO
CREATE TABLE #TestClientID ( ClientID int , AccessPersonID int )
INSERT INTO #TestClientID (ClientID, AccessPersonID)
SELECT 1,1 UNION ALL
SELECT NULL,1 UNION ALL
SELECT 1,NULL UNION ALL
SELECT 0,0
DECLARE #ClientID int = NULL
DECLARE #AccessPersonID int = 1
SELECT * FROM #TestClientID
WHERE ClientID = COALESCE(#ClientID, ClientID)
AND AccessPersonID = COALESCE(#AccessPersonID, AccessPersonID)
SELECT * FROM #TestClientID
WHERE (#ClientID IS NULL OR #ClientID = ClientID)
AND (#AccessPersonID IS NULL OR #AccessPersonID = AccessPersonID)
That said, if you're looking to eliminate a NULL input value, then use the COALESCE(). NULLs can get weird when doing comparisons. COALESCE(a,b) is more akin to MS SQL's ISNULL(a,b). In other words, if a IS NULL, use b.
And again, it really all depends on what you're ultimately trying to do. sp_ExecuteSQL is MS-centric, so if you don't plan to port this to any other database, you can use that. But honestly, in 15 years I've probably ported an application from one db to another fewer than a dozen times. It's more important if you're writing an application that will be used by other people who will install it on different systems, but if it's an enclosed system, the benefits of the database you're using usually outweigh the lack of portability.
I probably should have included One more section of the query
For the ISNULL and the COALESCE I am converting a value of 0 to null where in the dynamic I am leaving the value as 0 for the if clause. That is why the look a bit different.
From what I have been seeing the the COALESCE seems to be the consistently the worst performing.
Surprisingly from what I have tested the ISNULL and dynamic are very similar with the ISNULL version being slightly better in most cases.
In most cases it has reviled indexes that needed to be add and in most cases the indexes improved the queries the most but after thet have been added the ISNULL and Dynamic still perform better than the COALESCE.
Also I can not see us switching from MSSQL in the near or distant future.
The year is 2010.
SQL Server licenses are not cheap.
And yet, this error still does not indicate the row or the column or the value that produced the problem. Hell, it can't even tell you whether it was "string" or "binary" data.
Am I missing something?
A quick-and-dirty way of fixing these is to select the rows into a new physical table like so:
SELECT * INTO dbo.MyNewTable FROM <the rest of the offending query goes here>
...and then compare the schema of this table to the schema of the table into which the INSERT was previously going - and look for the larger column(s).
I realize that this is an old one. Here's a small piece of code that I use that helps.
What this does, is returns a table of the max lengths in the table you're trying to select from. You can then compare the field lengths to the max returned for each column and figure out which ones are causing the issue. Then it's just a simple query to clean up the data or exclude it.
DECLARE #col NVARCHAR(50)
DECLARE #sql NVARCHAR(MAX);
CREATE TABLE ##temp (colname nvarchar(50), maxVal int)
DECLARE oloop CURSOR FOR
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'SOURCETABLENAME' AND TABLE_SCHEMA='dbo'
OPEN oLoop
FETCH NEXT FROM oloop INTO #col;
WHILE (##FETCH_STATUS = 0)
BEGIN
SET #sql = '
DECLARE #val INT;
SELECT #val = MAX(LEN(' + #col + ')) FROM dbo.SOURCETABLENAME;
INSERT INTO ##temp
( colname, maxVal )
VALUES ( N''' + #col + ''', -- colname - nvarchar(50)
#val -- maxVal - int
)';
EXEC(#sql);
FETCH NEXT FROM oloop INTO #col;
END
CLOSE oloop;
DEALLOCATE oloop
SELECT * FROM ##temp
DROP TABLE ##temp;
Another way here is to use binary search.
Comment half of the columns in your code and try again. If the error persists, comment out another half of that half and try again. You will narrow down your search to just two columns in the end.
You could check the length of each inserted value with an if condition, and if the value needs more width than the current column width, truncate the value and throw a custom error.
That should work if you just need to identify which is the field causing the problem. I don't know if there's any better way to do this though.
Recommend you vote for the enhancement request on Microsoft's site. It's been active for 6 years now so who knows if Microsoft will ever do anything about it, but at least you can be a squeaky wheel: Microsoft Connect
For string truncation, I came up with the following solution to find the max lengths of all of the columns:
1) Select all of the data to a temporary table (supply column names where needed), e.g.
SELECT col1
,col2
,col3_4 = col3 + '-' + col4
INTO #temp;
2) Run the following SQL Statement in the same connection (adjust the temporary table name if needed):
DECLARE #table VARCHAR(MAX) = '#temp'; -- change this to your temp table name
DECLARE #select VARCHAR(MAX) = '';
DECLARE #prefix VARCHAR(256) = 'MAX(LEN(';
DECLARE #suffix VARCHAR(256) = ')) AS max_';
DECLARE #nl CHAR(2) = CHAR(13) + CHAR(10);
SELECT #select = #select + #prefix + name + #suffix + name + #nl + ','
FROM tempdb.sys.columns
WHERE object_id = object_id('tempdb..' + #table);
SELECT #select = 'SELECT ' + #select + '0' + #nl + 'FROM ' + #table
EXEC(#select);
It will return a result set with the column names prefixed with 'max_' and show the max length of each column.
Once you identify the faulty column you can run other select statements to find extra long rows and adjust your code/data as needed.
I can't think of a good way really.
I once spent a lot of time debugging a very informative "Division by zero" message.
Usually you comment out various pieces of output code to find the one causing problems.
Then you take this piece you found and make it return a value that indicates there's a problem instead of the actual value (in your case, should be replacing the string output with the len(of the output)). Then manually compare to the lenght of the column you're inserting it into.
from the line number in the error message, you should be able to identify the insert query that is causing the error. modify that into a select query to include AND LEN(your_expression_or_column_here) > CONSTANT_COL_INT_LEN for the string various columns in your query. look at the output and it will give your the bad rows.
Technically, there isn't a row to point to because SQL didn't write the data to the table. I typically just capture the trace, run it Query Analyzer (unless the problem is already obvious from the trace, which it may be in this case), and quickly debug from there with the ages old "modify my UPDATE to a SELECT" method. Doesn't it really just break down to one of two things:
a) Your column definition is wrong, and the width needs to be changed
b) Your column definition is right, and the app needs to be more defensive
?
The best thing that worked for me was to put the rows first into a temporary table using select .... into #temptable
Then I took the max length of each column in that temp table. eg. select max(len(jobid)) as Jobid, ....
and then compared that to the source table field definition.