SQLSERVER FOR XML PATH - treat attributes name as variable - sql-server

SELECT field1 as "#q1_12"
FROM table1
FOR XML PATH ('qt'),TYPE
I need to shape the result of this query in xml, but the field's alias "#q1_12" should change according to different and complicated conditions.
So, is it possible to put the name of this attribute in a variable and use it in the query (like a sort of dynamic sql)
Is there some trick or workaround?
SELECT field1 as #attributename <-- this is a variable
FROM table1
FOR XML PATH ('qt'),TYPE
i know that, here the character '#' makes confusion, it has different meaning of treated either as variable or as literal(in this example it will be used by "XML FOR" command to put the field’s value into an attribute instead of an element)
Moreover this query is used as subquery, so using CASE/IF is very difficult, any ideas?

Your question shows clearly, that you know about dynamic SQL and your got some hints about REPLACE() to solve this on string level.
suggestion 1
But there might be an approach, especially if the possible names are all known and the list is not to big:
DECLARE #mockupTable TABLE(ID INT IDENTITY,SomeValue VARCHAR(100));
INSERT INTO #mockupTable VALUES('Row 1'),('Row 2');
--As you know, it is impossible to use a variable column alias...
--You can use a dynamic statement, but this involves some major draw-backs... (no ad-hoc, difficult to proceed with the output...)
--But you might try this
--To simulate your complex conditions I just check for one single value (but you'll get the ghist):
DECLARE #SomeCondition INT=2;
SELECT CASE WHEN #SomeCondition=1 THEN SomeValue END AS [#q1_01]
,CASE WHEN #SomeCondition=2 THEN SomeValue END AS [#q1_02]
,CASE WHEN #SomeCondition=3 THEN SomeValue END AS [#q1_03]
--add all possible attribute names here...
FROM #mockupTable t
FOR XML PATH('qt'),TYPE;
The magic behind: If not specified explicitly (Read about ELEMENTS XSINIL) the XML created with FOR XML PATH() will omit NULL values.
You can state all possible column names and all of them will be omitted, if the condition is not reached.
suggestion 2
this is the opposite approach:
DECLARE #KeepThis VARCHAR(100)='q1_02';
SELECT
(
SELECT SomeValue AS [#q1_01]
,SomeValue AS [#q1_02]
,SomeValue AS [#q1_03]
FROM #mockupTable t
FOR XML PATH('qt'),TYPE
).query('for $qt in /qt
return
<qt>
{
for $a in $qt/#*[local-name(.)=sql:variable("#KeepThis")]
return $a
}
</qt>');
The XML will be created with all attributes side-by-side, while the XQuery will return just the one we want to keep.

The only way is with dynamic SQL. You cannot use a variable to replace the value of a object, alias, etc. Therefore you will need to safely inject it into the statement:
DECLARE #attributename sysname = N'#q1_12';
DECLARE #SQL nvarchar(MAX);
SET #SQL = N'SELECT field1 AS ' + QUOTENAME(#attributename) + NCHAR(13) + NCHAR(10) +
N'FROM table1' + NCHAR(13) + NCHAR(10) +
N'FOR XML PATH ('qt'),TYPE;';
EXEC sp_executesql #SQL;

Related

Create table variable with exact same structure of another table

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.

How to run a sql query for each entry by the user?

So the user enters the policy number in the form: 2000, 2001, 2002
I need to run a query for each of those 3 policy numbers. I am not sure how to do it.
This is the code I have right now. I was thinking about some sort of string manipulation and then use loop, but I am not sure how to do that. Can anyone please help me?
declare #sql1 varchar(1000)
declare #policy varchar(1000)
set #policy = '2000, 2001, 2002'
--THIS IS WHERE i NEED HELP???
set #policy = replace(#policy, ' ', '')
set #policy = '''' + replace(#policy, ',', ''',''') + ''''
print (#policy)
if #policy <> 'null'
set #sql1 =
(SELECT top 1
[MIL]
FROM
[DataManagement].[dbo].[lookuptable] where [policy] = #policy group by [MIL] )
exec (#sql1)
print(#sql1)
Options:
Are you sure you actually need one results set per item in #policy and not a single result set that matches any of the IDs? An IN query would let you get that result - sadly you can't do WHERE IN (#List), but it can be concatenated into a dynamic query (SQL injection concerns aside - make sure of your data source, but that applies to anything that's taking input like you seem to be.
If you declare #Policy as a user-defined table type variable, then you can pass that in as a list of IDs and simply loop through it. (More info - http://blog.sqlauthority.com/2008/08/31/sql-server-table-valued-parameters-in-sql-server-2008/, https://www.brentozar.com/archive/2014/02/using-sql-servers-table-valued-parameters/)
There's no shortage of examples online of code to split a delimited string list into a table of values. That would let you do the same as I've suggested in point 2.
The FOR XML PATH('') trick can be used to take a table of results and flatten it into a single variable. If you join against your values table and build the SELECT query from your question as the result, you can then do a single EXEC with no need for a loop at all.
Depends what you want for your environment really. I'd use option 2 to split the string, then build a combined single query using FOR XML PATH and execute that. But for some environments the table-valued parameter and loop approach would definitely be superior.

Which column is being truncated? [duplicate]

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.

How to pass multiple values in a single parameter for a stored procedures?

I want to pass multiple values in a single parameter. SQL Server 2005
You can have your sproc take an xml typed input variable, then unpack the elements and grab them. For example:
DECLARE #XMLData xml
DECLARE
#Code varchar(10),
#Description varchar(10)
SET #XMLData =
'
<SomeCollection>
<SomeItem>
<Code>ABCD1234</Code>
<Description>Widget</Description>
</SomeItem>
</SomeCollection>
'
SELECT
#Code = SomeItems.SomeItem.value('Code[1]', 'varchar(10)'),
#Description = SomeItems.SomeItem.value('Description[1]', 'varchar(100)')
FROM #XMLDATA.nodes('//SomeItem') SomeItems (SomeItem)
SELECT #Code AS Code, #Description AS Description
Result:
Code Description
========== ===========
ABCD1234 Widget
You can make a function:
ALTER FUNCTION [dbo].[CSVStringsToTable_fn] ( #array VARCHAR(8000) )
RETURNS #Table TABLE ( value VARCHAR(100) )
AS
BEGIN
DECLARE #separator_position INTEGER,
#array_value VARCHAR(8000)
SET #array = #array + ','
WHILE PATINDEX('%,%', #array) <> 0
BEGIN
SELECT #separator_position = PATINDEX('%,%', #array)
SELECT #array_value = LEFT(#array, #separator_position - 1)
INSERT #Table
VALUES ( #array_value )
SELECT #array = STUFF(#array, 1, #separator_position, '')
END
RETURN
END
and select from it:
DECLARE #LocationList VARCHAR(1000)
SET #LocationList = '1,32'
SELECT Locations
FROM table
WHERE LocationID IN ( SELECT CAST(value AS INT)
FROM dbo.CSVStringsToTable_fn(#LocationList) )
OR
SELECT Locations
FROM table loc
INNER JOIN dbo.CSVStringsToTable_fn(#LocationList) list
ON CAST(list.value AS INT) = loc.LocationID
Which is extremely helpful when you attempt to send a multi-value list from SSRS to a PROC.
Edited: to show that you may need to CAST - However be careful to control what is sent in the CSV list
Just to suggest. You can't really do so in SQL Server 2005. At least there is no a straightforward way. You have to use CSV or XML or Base 64 or JSON. However I strongly discourage you to do so since all of them are error prone and generate really big problems.
If you are capable to switch to SQL Server 2008 you can use Table valued parameters (Reference1, Reference2).
If you cannot I'd suggest you to consider the necessity of doing it in stored procedure, i.e. do you really want (should/must) to perform the sql action using SP. If you are solving a problem just use Ad hoc query. If you want to do so in education purposes, you might try don't even try the above mentioned things.
There are multiple ways you can achieve this, by:
Passing CSV list of strings as an argument to a (N)VARCHAR parameter, then parsing it inside your SP, check here.
Create a XML string first of all, then pass it as an XML datatype param. You will need to parse the XML inside the SP, you may need APPLY operator for this, check here.
Create a temp table outside the SP, insert the multiple values as multiple rows, no param needed here. Then inside the SP use the temp table, check here.
If you are in 2008 and above try TVPs (Table Valued Parameters) and pass them as params, check here.

Convert text into db object and field identifier

Given a select statement that lists all my tables and columns, how do I convert the table and column names into identifiers that can be used in a seperate select statement?
In other words, if I have the string #Table = 'Person' and #Column = 'Name', I want something like this:
select
DATALENGTH(MAX(t.#Column)) as Longest,
DATALENGTH(MIN(t.#Column)) as Shortest
from #Table as t;
Of course that does not accomplish what I want. I want to know the length of the longest and shortest string stored in a column. If I wanted this information for a single column, there would be no need to use these stringified-variables. But I want to do this for every (varchar) column across my database. Life is too short to create every SQL statement to accomplish this. This is why I want a parametric mechanism for specifying the table and column. How do I fix this to achieve the goal?
Of course I could be going about this all wrong. Perhaps I should address each column by index, and convert to a column name only when output is needed. Thoughts?
DECLARE #sql VARCHAR(999)
DECLARE #col VARCHAR(50)
DECLARE #table VARCHAR(50)
SET #col = <colname>
SET #table = <tablename>
SET #SQL = '
select
DATALENGTH(MAX(t.#Column)) as Longest,
DATALENGTH(MIN(t.#Column)) as Shortest
from #Table as t'
SET #SQL = REPLACE(REPLACE(#SQL, '#Column', #col), '#Table', #table)
EXEC(#SQL)
If you are new to tsql and specifically dynamic sql this is a good read on things you should consider. Most people, when they discover dynamic sql think of all kinds of uses and things they can do with it. But like I said... take caution.
http://www.sommarskog.se/dynamic_sql.html

Resources