Why FOR XML PATH() is used in this script? - sql-server

I got a help from some one to compose below sql but full script is not written by me. so i have bit of confusion how the below sql is working ?
CREATE Proc USP_GetValuationValue
(
#Ticker VARCHAR(10),
#ClientCode VARCHAR(10),
#GroupName VARCHAR(10)
)
AS
DECLARE #SPID VARCHAR(MAX), --Is this even used now?
#SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SELECT #SPID=CAST(##SPID AS VARCHAR);
SET #SQL = N'SELECT * FROM (SELECT min(id) ID,f.ticker,f.ClientCode,f.GroupName,f.RecOrder,' + STUFF((SELECT N',' + #CRLF + N' ' +
N'MAX(CASE FieldName WHEN ' + QUOTENAME(FieldName,'''') + N' THEN FieldValue END) AS ' + QUOTENAME(FieldName)
FROM tblValuationSubGroup g
WHERE ticker=#Ticker AND ClientCode=#ClientCode AND GroupName=#GroupName
GROUP BY FieldName
ORDER BY MIN(FieldOrder)
FOR XML PATH(''),TYPE).value('(./text())[1]','nvarchar(MAX)'),1,10,N'') + #CRLF +
N'FROM (select * from tblValuationFieldValue' + #CRLF +
N'WHERE Ticker = #Ticker AND ClientCode = #ClientCode AND GroupName= #GroupName) f' + #CRLF +
N'GROUP BY f.ticker,f.ClientCode,f.GroupName,f.RecOrder) X' + #CRLF +
N'ORDER BY Broker;';
PRINT #SQL;
Below sql is generated after executing above dynamic sql
SELECT * FROM (SELECT min(id) ID,f.ticker,f.ClientCode,f.GroupName,f.RecOrder,
MAX(CASE FieldName WHEN 'Last Update' THEN FieldValue END) AS [Last Update],
MAX(CASE FieldName WHEN 'Broker' THEN FieldValue END) AS [Broker],
MAX(CASE FieldName WHEN 'Rating' THEN FieldValue END) AS [Rating],
MAX(CASE FieldName WHEN 'Equivalent Rating' THEN FieldValue END) AS [Equivalent Rating],
MAX(CASE FieldName WHEN 'Target Price' THEN FieldValue END) AS [Target Price]
FROM (select * from tblValuationFieldValue
WHERE Ticker = #Ticker AND ClientCode = #ClientCode AND GroupName= #GroupName) f
GROUP BY f.ticker,f.ClientCode,f.GroupName,f.RecOrder) X
ORDER BY Broker;
This part is not clear why used in above sql?
FOR XML PATH(''),TYPE).value('(./text())[1]','nvarchar(MAX)'),1,10,N'')
why FOR XML PATH() has been used here ? i always use FOR XML PATH() to generate xml with data from table.
please help me to understand first dynamic sql like how it is working.
Thanks

FOR XML PATH('') is used in older versions of SQL server (pre 2017) to get values from multiple rows into one. Since you're setting a variable, #SQL, you're setting a single string and therefore need one row returned.
FOR XML PATH('') produces all the MAX(CASE FieldName [...] with data from the tblValuationSubGroup table. Each row in tblValuationSubGroup contains one field. FOR XML PATH('') adds the rows into one string, which is later turned into fields during execution.
The issue FOR XML PATH('') solves in older versions of SQL Server has been solved with the STRING_AGG() function since SQL Server 2017.
https://learn.microsoft.com/en-us/sql/t-sql/functions/string-agg-transact-sql?view=sql-server-ver16

FOR XML PATH is being to dynamically generate the last 5 columns in your query (the MAX(CASE FieldName WHEN '...' THEN FieldValue END) AS [...] columns) using the FieldName column of tblValuationSubGroup.

Related

Command Object executing Stored Procedure not populating output parameters [duplicate]

When I try to run a more advanced SQL query on an ASP page I get this error:
operation not allowed when the object is closed
When I run this code it's working:
...
sql = "SELECT distinct team FROM tbl_teams"
rs.open sql, conndbs, 1, 1
...
But when I run this code (and this code is working if I run it in Microsoft SQL Server Management Studio), I get the error...
...
sql = "DECLARE #cols AS NVARCHAR(MAX), #query AS NVARCHAR(MAX), #orderby nvarchar(max), #currentYear varchar(4) select #currentYear = cast(year(getdate()) as varchar(4)) select #cols = STUFF((SELECT ',' + QUOTENAME(year([datefrom])) from tbl_teams group by year([datefrom]) order by year([datefrom]) desc FOR XML PATH(''), TYPE ).value('.', 'NVARCHAR(MAX)') ,1,1,'') select #orderby = 'ORDER BY ['+cast(year(getdate()) as varchar(4)) + '] desc' set #query = 'SELECT team, Won = [1], Lost=[2], Draw = [3]' + #cols + ', Total from ( select team, new_col, total from ( select team, dt = year([datefrom]), result, total = count(*) over(partition by team) from tbl_teams ) d cross apply ( select ''dt'', dt union all select ''result'', case when dt = '+#currentYear+' then result end ) c (old_col_name, new_col) ) x pivot ( count(new_col) for new_col in ([1], [2], [3],' + #cols + ') ) p '+ #orderby exec sp_executesql #query"
rs.open sql, conndbs, 1, 1
...
This is a better overview of the query:
DECLARE
#cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#orderby nvarchar(max),
#currentYear varchar(4)
select #currentYear = cast(year(getdate()) as varchar(4))
select #cols
= STUFF((SELECT ',' + QUOTENAME(year([datefrom]))
from tbl_teams
group by year([datefrom])
order by year([datefrom]) desc
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
select #orderby = 'ORDER BY ['+cast(year(getdate()) as varchar(4)) + '] desc'
set #query = 'SELECT team, Won = [1],
Lost=[2], Draw = [3]' + #cols + ', Total
from
(
select
team,
new_col,
total
from
(
select team,
dt = year([datefrom]),
result,
total = count(*) over(partition by team)
from tbl_teams
) d
cross apply
(
select ''dt'', dt union all
select ''result'', case when dt = '+#currentYear+' then result end
) c (old_col_name, new_col)
) x
pivot
(
count(new_col)
for new_col in ([1], [2], [3],' + #cols + ')
) p '+ #orderby
exec sp_executesql #query
Do I need to run the query on another way or what is wrong with this code?
This is a common problem caused by row counts being interpreted as output from a Stored Procedure when using ADODB with SQL Server.
To avoid this remember to set
SET NOCOUNT ON;
in your Stored Procedure this will stop ADODB returning a closed recordset, or if for whatever reason you don't want to do this (not sure why as you can always use ##ROWCOUNT to pass the row count back), you can use
'Return the next recordset, which will be the result of the Stored Procedure, not
'the row count generated when SET NOCOUNT OFF (default).
Set rs = rs.NextRecordset()
which returns the next ADODB.Recordset if ADODB has detected one being returned by the Stored Procedure (might be best to check rs.State <> adStateClosed when dealing with multiple ADODB.Recordset objects).

Query with columns calculated at runtime

I need some help with a query in SQL Server 2012.
My customer has a (sort of) internal wiki where his employees save documents; these documents are stored in a table in the database (let's call it Documentation, with columns ID, Title, Text and Tags) and accessed via a web frontend.
Up to now every employee had access to all the documents, now my customer now wants his employees to access only to specific procedures. There are no specific criteria such as role or user type, he wants to decide who sees what on an individual basis (from now on John will see documents 1,2 and 4 while Janet will see only documents 3,4 and 5 and so on). Don't ask me why...
My task is to prepare a big table in the frontend where every line is a document and the employees are in the column; for every document there is a checkbox for every employee, to indicate whether that user can access the document, something similar to this:
The problem is that the number of documents is not fixed, nor the number of employees. I need to find a query to extract this data without knowing the number of columns, it must be done dynamically at runtime.
I have access to the User Table of course, so I know the number and the name of the employees. As for the authorizations I thought I could use a new table with just 3 columns: ID, ID_Document and ID_User.
I googled and searched but I could not find an appropriate answer. I tried using a Pivot Table but it does not seem right to me, I do not have to do any aggregation to the data.
Can anybody help me?
I'm going to we assume you have 5 tables in total. I'm going to call these Document, Tag, Employee and then DocumentTag and DocumentEmployee. To therefore get the solution you are after you need 2 different types of aggregation, string aggregation for the tags, and pivoting for the employees.
--Base tables
CREATE TABLE dbo.Document (DocumentID int, DocumentTitle nvarchar(50));
CREATE TABLE dbo.Employee (EmployeeID int, EmployeeName nvarchar(50));
CREATE TABLE dbo.Tag (TagID int, TagName nvarchar(50));
GO
--Link tables
CREATE TABLE dbo.DocumentTag (DocumentID int, TagID int);
CREATE TABLE dbo.DocumentEmployee (DocumentID int, EmployeeID int);
GO
--Sample data
INSERT INTO dbo.Document
VALUES(2,N'Important Doc'),(3,N'New Doc');
INSERT INTO dbo.Employee
VALUES(1,N'John'),
(2,N'Mary'),
(3,N'Patricia'),
(4,N'Paul');
INSERT INTO dbo.Tag
VALUES(1,N'Classified'),
(2,N'Finance'),
(3,N'Warehouse');
GO
--Link Data
INSERT INTO dbo.DocumentTag
VALUES(1,1),(1,2),(2,3);
INSERT INTO dbo.DocumentEmployee
VALUES(1,1),(1,2),(1,3),(2,2),(2,4);
If didn't need a dynamic pivot, then your SQL would look somethuing like this:
SELECT D.DocumentID,
D.DocumentTitle,
STUFF((SELECT N' ' + T.TagName
FROM dbo.DocumentTag DT
JOIN dbo.Tag T ON DT.TagID = T.TagID
WHERE DT.DocumentID = D.DocumentID
FOR XML PATH(N''),TYPE).value('.','nvarchar(MAX)'),1,1,N'') AS Tags,
MAX(CASE E.EmployeeName WHEN N'John' THEN 1 ELSE 0 END) AS John,
MAX(CASE E.EmployeeName WHEN N'Mary' THEN 1 ELSE 0 END) AS Mary,
MAX(CASE E.EmployeeName WHEN N'Patricia' THEN 1 ELSE 0 END) AS Patricia,
MAX(CASE E.EmployeeName WHEN N'Paul' THEN 1 ELSE 0 END) AS Paul
FROM dbo.Document D
JOIN dbo.DocumentEmployee DE ON D.DocumentID = DE.EmployeeID
JOIN dbo.Employee E ON DE.EmployeeID = E.EmployeeID
GROUP BY D.DocumentID,
D.DocumentTitle;
As you need the dataset to scale as you add employees, however, you need dynamic SQL to do this. So to achieve this using the above solution you can do something like this:
DECLARE #SQL nvarchar(MAX),
#CRLF nchar(2) = NCHAR(13) + NCHAR(10);
SET #SQL = N'SELECT D.DocumentID,' + #CRLF +
N' D.DocumentTitle,' + #CRLF +
N' STUFF((SELECT N'' '' + T.TagName' + #CRLF +
N' FROM dbo.DocumentTag DT' + #CRLF +
N' JOIN dbo.Tag T ON DT.TagID = T.TagID' + #CRLF +
N' WHERE DT.DocumentID = D.DocumentID' + #CRLF +
N' FOR XML PATH(N''''),TYPE).value(''.'',''nvarchar(MAX)''),1,1,N'''') AS Tags,' + #CRLF +
STUFF((SELECT N',' + #CRLF +
N' MAX(CASE E.EmployeeName WHEN N' + QUOTENAME(E.EmployeeName,'''') + N' THEN 1 ELSE 0 END) AS ' + QUOTENAME(E.EmployeeName)
FROM dbo.Employee E
ORDER BY E.EmployeeID ASC
FOR XML PATH(''),TYPE).value('.','nvarchar(MAX)'),1,3,N'') + #CRLF +
N'FROM dbo.Document D' + #CRLF +
N' JOIN dbo.DocumentEmployee DE ON D.DocumentID = DE.EmployeeID' + #CRLF +
N' JOIN dbo.Employee E ON DE.EmployeeID = E.EmployeeID' + #CRLF +
N'GROUP BY D.DocumentID,' + #CRLF +
N' D.DocumentTitle;';
--PRINT #SQL; --YOur debugging friend
EXEC sys.sp_executesql #SQL;
DB<>Fiddle

Update with dynamic tables

I have to write update using dynamic sql becaus i know only name of column that I want to update and names of columns which I will use to join tables in my update. But I don't know the numbers of tables and names. Names of tables I will get in parameter of my procedure in this way
declare #Tables = N'Customer,Employee,Owner'
So I want to have update like this:
update t
set [Status] = 100
from
TemporaryTable t
left join Customer t1 on t1.RecordId = t.RecordId
left join Employee t2 on t2.RecordId = t.RecordId
left join Owner t3 on t3.RecordId =t.RecordId
where
t1.RecordId is null
and t2.RecordId is NULL
and t3.RecordId is null
I know that each table will have column RecordId and want to left join this tables to my TemporaryTable on this column but I don't know the names and numbers of tables. For example I will have one, two, or ten tables with different names. I know that this tables names will be save in parameter #Tables in that way:
#Tables = N'Customer,Employee,Owner'
There is possilble to write this update in dynamic way?
This is an answer, which helps ... to write update using dynamic sql ... and only shows how to generate a dynamic statement. It's based on string splitting. From SQL Server 2016+ you may use STRING_SPLIT() (because here the order of the substrings is not important). For previous versions you need to find a string splitting function.
T-SQL:
DECLARE #Tables nvarchar(max) = N'Customer,Employee,Owner'
DECLARE #join nvarchar(max) = N''
DECLARE #where nvarchar(max) = N''
DECLARE #stm nvarchar(max) = N''
SELECT
#join = #join + CONCAT(
N' LEFT JOIN ',
QUOTENAME(s.[value]),
N' t',
ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
N' ON t',
ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
N'.RecordId = t.RecordId'
),
#where = #where + CONCAT(
N' AND t',
ROW_NUMBER() OVER (ORDER BY (SELECT 1)),
N'.RecordId is NULL'
)
FROM STRING_SPLIT(#Tables, N',') s
SET #stm = CONCAT(
N'UPDATE t SET [Status] = 100 ',
N'FROM TemporaryTable t',
#join,
N' WHERE ',
STUFF(#where, 1, 5, N'')
)
PRINT #stm
EXEC sp_executesql #stm
Notes:
One note, that I think is important - consider passing tables names using table value type for parameter, not as comma-separated text.
It seems like this will suit your needs, though I don't fully understand what you're trying to do. Here we're constructing the final SQL in two pieces (#s and #where) and then concatenating into the final SQL at the end.
declare #Tables varchar(100) = N'Customer,Employee,Owner'
declare #tablenames table (tablename nvarchar(100))
insert #tablenames (tablename)
select value
from string_split(#Tables, ',');
declare #where varchar(max) = ''
declare #s varchar(max) = '
update t
set [Status] = 100
from TemporaryTable t'
select #s += '
left join ' + tablename + ' on ' + tablename + '.RecordId = t.RecordId'
, #where += case when #where = '' then '' else ' and ' end + tablename + '.RecordId is null
'
from #tablenames
print #s + char(13) + ' where ' + #where
exec( #s + char(13) + ' where ' + #where)

How do I validate a variable against multple database tables

Does anyone know how to check a a variable against all database table with columns storing the same type of information? I have a poorly designed database that stores ssn in over 60 tables within one database. some of the variations of columns in the various tables include:
app_ssn
ca_ssn
cand_ssn
crl_ssn
cu_ssn
emtaddr_ssn
re_ssn
sfcart_ssn
sfordr_ssn
socsecno
ssn
Ssn
SSN
I want to create a stored procedure that will accept a value and check it against every table that has 'ssn' in the name.Does anyone have idea as to how to do this?
-- I assume that table/column names don't need to be surrounded by square braces. You may want to save matches in a table - I just select them. I also assume ssn is a char.
alter proc proc1
#search1 varchar(500)
as
begin
set nocount on
declare #strsql varchar(500)
declare #curtable sysname
declare #prevtable sysname
declare #column sysname
select top 1 #curtable= table_schema+'.'+table_name, #column=column_name
from INFORMATION_SCHEMA.COLUMNS
where CHARINDEX('ssn',column_name) > 0
order by table_schema+'.'+table_name +column_name
-- make sure that at least one column has ssn in the column name
if #curtable is not null
begin
while (1=1)
begin
set #strsql = 'select * from ' +#curtable +' where '+''''+#search1+''''+ ' = '+#column
print #strsql
-- any matches for passed in ssn will match here...
exec (#strsql)
set #prevtable = #curtable+#column
select top 1 #curtable= table_schema+'.'+table_name, #column=column_name
from INFORMATION_SCHEMA.COLUMNS
where CHARINDEX('ssn',column_name) > 0
and table_schema+'.'+table_name +column_name> #prevtable
order by table_schema+'.'+table_name +column_name
-- when we run out of columns that contain ssn we are done...
if ##ROWCOUNT = 0
break
end
end
end
What you will need to do is some research. But here is where you can start;
SELECT tbl.NAME AS TableName
,cl.NAME AS ColumnName
,IDENTITY(INT, 1, 1) AS ID
INTO #ColumnsToLoop
FROM sys.tables tbl
JOIN sys.columns cl ON cl.object_id = tbl.object_id
This will give you the table / column relation then you can simply build a dynamic SQL string based on each row in the query above (basically loop it) and use EXEC or sp_execsql. So basically;
DECLARE #Loop int = (select min(ID) From #ColumnsToLoop),#MX int = (Select MAX(ID) From #ColumnsToLoop)
WHILE(#Loop<=#MX)
BEGIN
DECLARE #SQL nvarchar(MAX) = 'SQL String'
//Construct the dynamic SQL String
EXEC(#SQL);
SET #Loop += 1
END
Perhaps I went a little too crazy with this one, but let me know. I thought it would best the primary key of the search results with the table name so you could join it to your tables. I also managed to do it without a single cursor or loop.
DECLARE #SSN VARCHAR(25) = '%99%',
#SQL VARCHAR(MAX);
WITH CTE_PrimaryKeys
AS
(
SELECT TABLE_CATALOG,
TABLE_SCHEMA,
TABLE_NAME,
column_name
FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE D
WHERE OBJECTPROPERTY(OBJECT_ID(constraint_name), 'IsPrimaryKey') = 1
),
CTE_Columns
AS
(
SELECT A.*,
CONCAT(A.TABLE_CATALOG,'.',A.TABLE_SCHEMA,'.',A.TABLE_NAME) AS FullTableName,
CASE WHEN B.COLUMN_NAME IS NOT NULL THEN 1 ELSE 0 END AS IsPrimaryKey
FROM INFORMATION_SCHEMA.COLUMNS A
LEFT JOIN CTE_PrimaryKeys B
ON A.TABLE_CATALOG = B.TABLE_CATALOG
AND A.TABLE_SCHEMA = B.TABLE_SCHEMA
AND A.TABLE_NAME = B.TABLE_NAME
AND A.COLUMN_NAME = B.COLUMN_NAME
),
CTE_Select
AS
(
SELECT
'SELECT ' +
--This returns the pk_col casted as Varchar and the table name in another columns
STUFF((SELECT ',CAST(' + COLUMN_NAME + ' AS VARCHAR(MAX)) AS pk_col,''' + B.TABLE_NAME + ''' AS Table_Name'
FROM CTE_Columns B
WHERE A.Table_Name = B.TABLE_NAME
AND B.IsPrimaryKey = 1
FOR XML PATH ('')),1,1,'')
+ ' FROM ' + fullTableName +
--This is where I list the columns where LIKE desired SSN
' WHERE ' +
STUFF((SELECT COLUMN_NAME + ' LIKE ''' + #SSN + ''' OR '
FROM CTE_Columns B
WHERE A.Table_Name = B.TABLE_NAME
--This is where I filter so I only get desired columns
AND (
--Uncomment the Collate if your database is case sensitive
COLUMN_NAME /*COLLATE SQL_Latin1_General_CP1_CI_AS*/ LIKE '%ssn%'
--list your column Names that don't have ssn in them
--OR COLUMN_NAME IN ('col1','col2')
)
FOR XML PATH ('')),1,0,'') AS Selects
FROM CTE_Columns A
GROUP BY A.FullTableName,A.TABLE_NAME
)
--Unioning them all together and getting rid of last trailing "OR "
SELECT #SQL = COALESCE(#sql,'') + SUBSTRING(selects,1,LEN(selects) - 3) + ' UNION ALL ' + CHAR(13) --new line for easier debugging
FROM CTE_Select
WHERE selects IS NOT NULL
--Look at your code
SELECT SUBSTRING(#sql,1,LEN(#sql) - 11)

SQL Server Dynamic Pivot Column Names

For the necessity of my application, I must return the column names of a query as the very first row.
Now I must PIVOT this result in order to UNION it with my result set, but the difficult part is: it must be dynamic, so if I ever add new columns to this table, the SELECT will bring all the names pivoted.
The following SELECT brings me the Column names:
SELECT COLUMN_NAME
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Codes'
ORDER BY INFORMATION_SCHEMA.COLUMNS.ORDINAL_POSITION
And my result set is:
COLUMN_NAME
Id
CodeName
Country
StartDate
EndDate
What I expect is:
Id CodeName Country StartDate EndDate (... whatever other columns I might have)
Is there any easy way to do that without hardcoding the column names?
Thank you in advance!
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',[' + COLUMN_NAME + ']',
'[' + COLUMN_NAME + ']')
FROM (SELECT DISTINCT COLUMN_NAME,INFORMATION_SCHEMA.COLUMNS.ORDINAL_POSITION O
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'CODES') PV
ORDER BY O
DECLARE #query NVARCHAR(MAX)
SET #query = '
SELECT TOP 0 * FROM
(
SELECT COLUMN_NAME FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = ''CODES''
) x
PIVOT
(
MIN(COLUMN_NAME)
FOR [COLUMN_NAME] IN (' + #cols + ')
) p
'
EXEC SP_EXECUTESQL #query
Starting with SQL Server 2017, there's a function for this: STRING_AGG.
I used the QUOTENAME function as well here, to make adding the [ ] brackets easier.
DECLARE #ColumnNames NVARCHAR(MAX);
SELECT #ColumnNames = STRING_AGG(QUOTENAME(COLUMN_NAME), ',')
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME='Codes';
Simple way would be by declaring a variable and assigning the columns with comma separted. Try this.
DECLARE #col_list VARCHAR(max)=''
SELECT #col_list += '['+ COLUMN_NAME + '],'
FROM INFORMATION_SCHEMA.COLUMNS
WHERE TABLE_NAME = 'Codes'
ORDER BY INFORMATION_SCHEMA.COLUMNS.ORDINAL_POSITION
SELECT #col_list = LEFT(#col_list, Len(#col_list) - 1)
SELECT #col_list

Resources