dynamic filters and sp_executesql - sql-server

I have a stored procedure which has a parameter #SomeFilterIds, which takes in comma separated integer ids. If this parameter is not NULL it is translated into something like this:
AND [X] IN(1, 2, 4)
and assigned to #SomeFilter
I then used something along those lines:
SET #Sql = N' ...WHERE
c.SomeDate >= #SomeDate
' + #SomeFilter
and:
SET #ParameterDefinition = N'#SomeDate DateTime';
EXEC sp_executesql
#Sql
,#ParameterDefinition
,#SomeDate = #SomeDate
I would think that this is not best practice and opens potential security holes. Is this correct? Can this be improved? Thanks.

I think instead of #SomeFilterIds as varchar parameter you can use XML type variable and then use Inner join your main table with this XML variable.
This will avoid dynamic query execution and will be be safer.
Example:
--Instead of comma separated ID use below XML
declare #xml xml = '<row><ID>1</ID></row><row><ID>3</ID></row>'
--Assume this is your other table
declare #YourTable table (ItemId int, ColA varchar(20))
insert #YourTable
select 1, 'Hello World'
--Joining both the tables
select col.value('data(ID[1])', 'int') as ID
from #xml.nodes('/row') tbl(col)
inner join #YourTable t2
on t2. ItemId=tbl.col.value('data(ID[1])', 'int')
--WHERE c.SomeDate >= #SomeDate

Related

SQL Server extract data from XML column without tag names

I have an XML string:
<XML>
<xml_line>
<col1>1</col1>
<col2>foo 1</col2>
</xml_line>
<xml_line>
<col1>2</col1>
<col2>foo 2</col2>
</xml_line>
</XML>
I am extracting data from that string (stored in #data_xml) by storing it in SQL Server table and parsing it:
-- create temp table, insert XML string
CREATE TABLE table1 (data_xml XML)
INSERT table1
SELECT #data_xml
-- parse XML string into temp table
SELECT
N.C.value('col1[1]', 'int') col1_name,
N.C.value('col2[1]', 'varchar(31)') col2_name,
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
I would like to know if there is a generic way to accomplish the same without specifying column names (i.e. col1[1], col2[1])
You can use something like:
SELECT
N.C.value('let $i := . return count(//xml_line[. << $i]) + 1', 'int') as LineNumber,
Item.Node.value('local-name(.)', 'varchar(max)') name,
Item.Node.value('.', 'varchar(max)') value
FROM
table1
CROSS APPLY
data_xml.nodes('//xml_line') N(C)
CROSS APPLY
N.C.nodes('*') Item(Node)
To get:
LineNumber
name
value
1
col1
1
1
col2
foo 1
2
col1
2
2
col2
foo 2
See this db<>fiddle.
However, to spread columns horizontally, you will need to generate dynamic SQL after querying for distinct element names.
ADDENDUM: Here is an updated db<>fiddle that also shows a dynamic SQL example.
The above maps all values as VARCHAR(MAX). If you have NVARCHAR data you can make the appropriate changes. If you have a need to map specific columns to specific types, you will need to explicitly define and populate a name-to-type mapping table and incorporate that into the dynamic SQL logic. The same may be necessary if you prefer that the result columns be in a specific order.
ADDENDUM 2: This updated db<>fiddle now includes column type and ordering logic.
--------------------------------------------------
-- Extract column names
--------------------------------------------------
DECLARE #Names TABLE (name VARCHAR(100))
INSERT #Names
SELECT DISTINCT Item.Node.value('local-name(.)', 'varchar(max)')
FROM table1
CROSS APPLY data_xml.nodes('//xml_line/*') Item(Node)
--SELECT * FROM #Names
--------------------------------------------------
-- Define column-to-type mapping
--------------------------------------------------
DECLARE #ColumnTypeMap TABLE ( ColumnName SYSNAME, ColumnType SYSNAME, ColumnOrder INT)
INSERT #ColumnTypeMap
VALUES
('col1', 'int', 1),
('col2', 'varchar(10)', 2)
DECLARE #ColumnTypeDefault SYSNAME = 'varchar(max)'
--------------------------------------------------
-- Define SQL Templates
--------------------------------------------------
DECLARE #SelectItemTemplate VARCHAR(MAX) =
' , N.C.value(<colpath>, <coltype>) <colname>
'
DECLARE #SqlTemplate VARCHAR(MAX) =
'SELECT
N.C.value(''let $i := . return count(//xml_line[. << $i]) + 1'', ''int'') as LineNumber
<SelectItems>
FROM
table1
CROSS APPLY
data_xml.nodes(''//xml_line'') N(C)
'
--------------------------------------------------
-- Expand SQL templates into SQL
--------------------------------------------------
DECLARE #SelectItems VARCHAR(MAX) = (
SELECT STRING_AGG(SI.SelectItem, '')
WITHIN GROUP(ORDER BY ISNULL(T.ColumnOrder, 999), N.Name)
FROM #Names N
LEFT JOIN #ColumnTypeMap T ON T.ColumnName = N.name
CROSS APPLY (
SELECT SelectItem = REPLACE(REPLACE(REPLACE(
#SelectItemTemplate
, '<colpath>', QUOTENAME(N.name + '[1]', ''''))
, '<colname>', QUOTENAME(N.name))
, '<coltype>', QUOTENAME(ISNULL(T.ColumnType, #ColumnTypeDefault), ''''))
) SI(SelectItem)
)
DECLARE #Sql VARCHAR(MAX) = REPLACE(#SqlTemplate, '<SelectItems>', #SelectItems)
--------------------------------------------------
-- Execute
--------------------------------------------------
SELECT DynamicSql = #Sql
EXEC (#Sql)
Result (with some additional data):
LineNumber
col1
col2
bar
foo
1
1
foo 1
null
More
2
2
foo 2
Stuff
null

Two tables as parameters of user defined function

I am trying to create a function in SQL SERVER which I can use to compare two tables, to check if they are identical. I do that with two excepts.
The Tables are supposed to be exactly the same, with the same data formats and column names as well as all values identical in both tables. This will be a manual check, so if differences are there, a thrown error is not a problem. The aim is just to see if two approaches of creating the tables leads to the same tables.
I am really new to functions in SQL, so I am not sure how to solve the problem.
I want to pass both tables as parameters to the function, to get something like this:
CREATE FUNCTION DIFFERING_ROWS
(#TABLE1, #TABLE2)
RETURNS TABLE
AS
RETURN (
SELECT *, 'A_not_B' as [Difference] FROM #TABLE1
except
SELECT *, 'A_not_B' as [Difference] FROM #TABLE2
union all
SELECT *, 'B_not_A' as [Difference] FROM #TABLE2
except
SELECT *, 'B_not_A' as [Difference] FROM #TABLE1
)
END
How is this implemented correctly?
Can anybody help me?
You cannot do this in a function. The only way you can pass table names as parameters is to use Dynamic SQL, and Dynamic SQL is not allowed in functions. You CAN do it with a stored procedure.
You can create this stored procedure that counts if the tables have the same column_names:
CREATE PROCEDURE checkEqualTables
#table1 varchar(100),
#table2 varchar(100)
AS
BEGIN
DECLARE #xCount int;
(SELECT #xCount = COUNT(*) from (SELECT column_name FROM information_schema.COLUMNS WHERE table_name=#table1) base
where column_name not in (SELECT column_name FROM information_schema.COLUMNS WHERE table_name=#table2))
IF(#xCount <= 0)
print 'Tables are equal!';
ELSE
print 'Tables are not equal!'
END
Ok I took the information from the answers and comments and researched about how to put this into procedures, and this is what I built:
I think this does what I want:
CREATE PROCEDURE checkEqualTables
#table1 nvarchar(100),
#table2 nvarchar(100)
AS
BEGIN
DECLARE #SQL nvarchar(max);
SET #SQL = 'SELECT * FROM ' + #TABLE1 +
'except
SELECT * FROM ' + #TABLE2 +
'union all
SELECT * FROM ' + #TABLE2 +
'except
SELECT * FROM ' + #TABLE1
EXECUTE sp_executesql #SQL
END

SQL Server : convert XML data onto table

I need help in fine-tuning this code that I write. I am new to SQL Server and I believe there are better ways to do this or perhaps some of the following codes can be simplified or fine-tuned for performances or saving memory resources.
Basically, I have this XML data :
<table_result id="001" subj_cd="cdaaa" grade="b" name="Phua Chu Kang"/>
and I want to create a table which looks like this from that XML data
Please note on the following points :
SplitThis is not a built in function (check the code below).
The data can have space, but delimited by ". Do note as well that the XML data can have varying number of fields-data pairs for that particular given table - referred as #dummy in the following codes. i.e. example XML data above have 4 fields (id, subj_cd, grade, name) and the next XML data could have 5 fields (i.e. id, name, occupation, phone_no, address). In the following code, #table_result is created to match the example XML data for easier demonstration. In other words, the table structures are known..so I can ignore the field names from the XML data and focus on extracting the data itself.
The code ran well on SQL Server 2012 (you can copy and paste run code directly) and I am able to get as above. I just need to fine tune this, if possible. I have include line like this : - - test blabla. You can uncomment that and try. I could use enhancements such as in term of avoiding the number of temp tables used or any ways to replace the use of row_number() in the code.
/* remove all temp tables */
declare #sql varchar(5000)
SELECT #sql = isnull(#sql+';', '') + 'drop table ' + SUBSTRING(t.name, 1, CHARINDEX('___', t.name)-1)
FROM tempdb..sysobjects AS t
WHERE t.name LIKE '#%[_][_][_]%'
AND t.id =OBJECT_ID('tempdb..' + SUBSTRING(t.name, 1, CHARINDEX('___', t.name)-1));
exec (#sql)
/* end */
/* function */
drop function splitthis
go
create function splitthis(#separator char(1), #list varchar(max))
returns #returntable table(item nvarchar(max))
as
begin
declare #index int
declare #newtext varchar(max)
if #list = null
return
set #index = charindex(#separator, #list)
while not(#index = 0)
begin
set #newtext = rtrim(ltrim(left(#list, #index - 1)))
set #list = right(#list, len(#list) - #index)
insert into #returntable(item) values(#newtext)
set #index = charindex(#separator, #list)
end
insert into #returntable(item) values(rtrim(ltrim(#list)))
update #returntable set item='' where item is null
return
end
go
/* end of function */
/* create dummy tables */
create table #table_result
(id nvarchar(max), subj_cd nvarchar(max), grade nvarchar(max), name nvarchar(max))
create table #dummy (name nvarchar(max), data nvarchar(max))
insert into #dummy
values ('a', '<table_result id="001" subj_cd="cdaaa" grade="b" name="phua chu kang"/>');
--test : select * from #dummy
/* remove the fist non-data opening tag */
declare #record nvarchar(max)
select #record = data from #dummy where name = 'a'
select *, null as temp into #tempb from splitthis(' ',#record)
select *, row_number() over (order by temp) count into #tempc from #tempb
select item into #tempd from #tempc where #tempc.count>1
-- test : select * from #tempd
/* get the actual field & data into a single column table */
declare #temp varchar(max)
set #temp=''select #temp=#temp+' ' + item from #tempd
select *, null as temp into #tempe from splitthis('"',#temp)
select *, row_number() over (order by temp) count into #tempf from #tempe
select item, count into #tempg from #tempf
--test : select * from #tempg
/* prepare the data table */
select
case when #tempg.count % 2 = 0
then item
else null
end as data
into #temph
from #tempg
select data, null as temp into #tempi from #temph
select data, row_number() over (order by temp) count into #data from #tempi
where data is not null
--test : select * from #data
/* prepare the field table. */
select name, null as temp into #tempj
from tempdb.sys.columns where object_id=object_id('tempdb..#table_result');
select *, row_number() over (order by temp) count into #field from #tempj
--test : select * from #field
/* get the final table */
select a.name as field, b.data from #field a
left join #data b on a.count=b.count
This is - using XML methods - much easier!
Try this:
DECLARE #xml XML='<table_result id="001" subj_cd="cdaaa" grade="b" name="Phua Chu Kang"/>';
SELECT One.Attr.value('fn:local-name(.)','varchar(max)') AS field
,One.Attr.value('.','varchar(max)') AS data
FROM #xml.nodes('table_result/#*') AS One(Attr)
The result
field data
id 001
subj_cd cdaaa
grade b
name Phua Chu Kang
Now I try to imitate your table structure (I'd recommend to store the data as XML from the beginning! In this case you could omit the first CROSS APPLY with the CAST ... AS XML):
DECLARE #tbl TABLE(name VARCHAR(10),data VARCHAR(MAX));
INSERT INTO #tbl VALUES
('a','<table_result id="001" subj_cd="cdaaa" grade="b" name="Phua Chu Kang"/>')
,('b','<Another test="test data" test2="test2 data"/>')
,('c','<OneMore x="x data" y="y data" z="z data"/>');
SELECT tbl.name
,One.Attr.value('fn:local-name(..)','varchar(max)') AS element
,One.Attr.value('fn:local-name(.)','varchar(max)') AS field
,One.Attr.value('.','varchar(max)') AS data
FROM #tbl AS tbl
CROSS APPLY(SELECT CAST(tbl.data AS XML)) AS MyData(AsXml)
CROSS APPLY MyData.AsXml.nodes('*/#*') AS One(Attr)
The result
name element field data
a table_result id 001
a table_result subj_cd cdaaa
a table_result grade b
a table_result name Phua Chu Kang
b Another test test data
b Another test2 test2 data
c OneMore x x data
c OneMore y y data
c OneMore z z data
Now, I'm not at all very good with T-SQL XML, but can't you just do it like this:
create table #dummy (name nvarchar(max), data xml);
insert into #dummy
values ('a', '<table_result id="001" subj_cd="cdaaa" grade="b" name="phua chu kang"/>');
select 'id' "field",
elem.value('#id', 'nvarchar(50)') "data"
from #dummy
cross apply data.nodes('/table_result') tbl(elem)
union all
select 'subj_cd' "field",
elem.value('#subj_cd', 'nvarchar(50)') "data"
from #dummy
cross apply data.nodes('/table_result') tbl(elem)
union all
select 'grade' "field",
elem.value('#grade', 'nvarchar(50)') "data"
from #dummy
cross apply data.nodes('/table_result') tbl(elem)
union all
select 'name' "field",
elem.value('#name', 'nvarchar(50)') "data"
from #dummy
cross apply data.nodes('/table_result') tbl(elem);
Notice that I changed the data type for #dummy.data to be xml. That's required to be able to use the XML functions.

Select (Select field from FieldTable) from Table

I'm using MSQL 2005. I have 2 table.A and B
Table A
- ID DOVKOD
- 1 KURSATIS
Table B
- ID KURALIS KURSATIS
- 1 2,2522 2,2685
- 2 2,4758 2,4874
Table A has only 1 record
When I execute Select (Select DOVKOD from Table A) from Table B I want to get same result as Select KURSATIS from Table B
I am gonna use it in a view. How can I do that. Thanks..
You can simply use a CASE expression:
SELECT CASE WHEN (SELECT DOVKOD FROM A) = 'KURSATIS' THEN KURSATIS
ELSE KURALIS
END
FROM B
SQL Fiddle Demo here
You must use Dynamic TSQL
SELECT #column=DOVKOD from Table A
EXEC ('Select ' + #column + ' from Table B')
If I understood you right then in table A you have the name of the column that you want to return. Then your solution is bad at all. I'll rather do something like that:
CREATE TABLE #TableA
(
ID INT, DOVKOD VARCHAR(100)
);
INSERT INTO #TableA VALUES (1, 'KURSATIS');
CREATE TABLE #TableB
(
ID INT, Value DECIMAL (18,2),Name VARCHAR(100)
);
INSERT INTO #TableB VALUES (1, 2.2522 , 'KURALIS');
INSERT INTO #TableB VALUES (2, 2.4758 , 'KURSATIS');
SELECT #TableB.* FROM #TableB JOIN #TableA ON #TableA.DOVKOD = #TableB.Name
The only way how to do this in MySQL is using Prepared statements. Dynamic pivot tables (transform rows to columns) is a good article about this.
SET #sql = NULL;
Select DOVKOD INTO #sql
FROM from Table A;
SET #sql = CONCAT('SELECT ', #sql, 'FROM Table B');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

Stored procedure: Return column values as comma separated values [duplicate]

This question already has answers here:
How to concatenate text from multiple rows into a single text string in SQL Server
(47 answers)
Closed 7 years ago.
If I issue SELECT username FROM Users I get this result:
username
--------
Paul
John
Mary
but what I really need is one row with all the values separated by comma, like this:
Paul, John, Mary
How do I do this?
select
distinct
stuff((
select ',' + u.username
from users u
where u.username = username
order by u.username
for xml path('')
),1,1,'') as userlist
from users
group by username
had a typo before, the above works
This should work for you. Tested all the way back to SQL 2000.
create table #user (username varchar(25))
insert into #user (username) values ('Paul')
insert into #user (username) values ('John')
insert into #user (username) values ('Mary')
declare #tmp varchar(250)
SET #tmp = ''
select #tmp = #tmp + username + ', ' from #user
select SUBSTRING(#tmp, 0, LEN(#tmp))
good review of several approaches:
http://blogs.msmvps.com/robfarley/2007/04/07/coalesce-is-not-the-answer-to-string-concatentation-in-t-sql/
Article copy -
Coalesce is not the answer to string concatentation in T-SQL I've seen many posts over the years about using the COALESCE function to get string concatenation working in T-SQL. This is one of the examples here (borrowed from Readifarian Marc Ridey).
DECLARE #categories varchar(200)
SET #categories = NULL
SELECT #categories = COALESCE(#categories + ',','') + Name
FROM Production.ProductCategory
SELECT #categories
This query can be quite effective, but care needs to be taken, and the use of COALESCE should be properly understood. COALESCE is the version of ISNULL which can take more than two parameters. It returns the first thing in the list of parameters which is not null. So really it has nothing to do with concatenation, and the following piece of code is exactly the same - without using COALESCE:
DECLARE #categories varchar(200)
SET #categories = ''
SELECT #categories = #categories + ',' + Name
FROM Production.ProductCategory
SELECT #categories
But the unordered nature of databases makes this unreliable. The whole reason why T-SQL doesn't (yet) have a concatenate function is that this is an aggregate for which the order of elements is important. Using this variable-assignment method of string concatenation, you may actually find that the answer that gets returned doesn't have all the values in it, particularly if you want the substrings put in a particular order. Consider the following, which on my machine only returns ',Accessories', when I wanted it to return ',Bikes,Clothing,Components,Accessories':
DECLARE #categories varchar(200)
SET #categories = NULL
SELECT #categories = COALESCE(#categories + ',','') + Name
FROM Production.ProductCategory
ORDER BY LEN(Name)
SELECT #categories
Far better is to use a method which does take order into consideration, and which has been included in SQL2005 specifically for the purpose of string concatenation - FOR XML PATH('')
SELECT ',' + Name
FROM Production.ProductCategory
ORDER BY LEN(Name)
FOR XML PATH('')
In the post I made recently comparing GROUP BY and DISTINCT when using subqueries, I demonstrated the use of FOR XML PATH(''). Have a look at this and you'll see how it works in a subquery. The 'STUFF' function is only there to remove the leading comma.
USE tempdb;
GO
CREATE TABLE t1 (id INT, NAME VARCHAR(MAX));
INSERT t1 values (1,'Jamie');
INSERT t1 values (1,'Joe');
INSERT t1 values (1,'John');
INSERT t1 values (2,'Sai');
INSERT t1 values (2,'Sam');
GO
select
id,
stuff((
select ',' + t.[name]
from t1 t
where t.id = t1.id
order by t.[name]
for xml path('')
),1,1,'') as name_csv
from t1
group by id
;
FOR XML PATH is one of the only situations in which you can use ORDER BY in a subquery. The other is TOP. And when you use an unnamed column and FOR XML PATH(''), you will get a straight concatenation, with no XML tags. This does mean that the strings will be HTML Encoded, so if you're concatenating strings which may have the < character (etc), then you should maybe fix that up afterwards, but either way, this is still the best way of concatenating strings in SQL Server 2005.
building on mwigdahls answer. if you also need to do grouping here is how to get it to look like
group, csv
'group1', 'paul, john'
'group2', 'mary'
--drop table #user
create table #user (groupName varchar(25), username varchar(25))
insert into #user (groupname, username) values ('apostles', 'Paul')
insert into #user (groupname, username) values ('apostles', 'John')
insert into #user (groupname, username) values ('family','Mary')
select
g1.groupname
, stuff((
select ', ' + g.username
from #user g
where g.groupName = g1.groupname
order by g.username
for xml path('')
),1,2,'') as name_csv
from #user g1
group by g1.groupname
You can use this query to do the above task:
DECLARE #test NVARCHAR(max)
SELECT #test = COALESCE(#test + ',', '') + field2 FROM #test
SELECT field2 = #test
For detail and step by step explanation visit the following link
http://oops-solution.blogspot.com/2011/11/sql-server-convert-table-column-data.html
DECLARE #EmployeeList varchar(100)
SELECT #EmployeeList = COALESCE(#EmployeeList + ', ', '') +
CAST(Emp_UniqueID AS varchar(5))
FROM SalesCallsEmployees
WHERE SalCal_UniqueID = 1
SELECT #EmployeeList
source:
http://www.sqlteam.com/article/using-coalesce-to-build-comma-delimited-string
In SQLite this is simpler. I think there are similar implementations for MySQL, MSSql and Orable
CREATE TABLE Beatles (id integer, name string );
INSERT INTO Beatles VALUES (1, "Paul");
INSERT INTO Beatles VALUES (2, "John");
INSERT INTO Beatles VALUES (3, "Ringo");
INSERT INTO Beatles VALUES (4, "George");
SELECT GROUP_CONCAT(name, ',') FROM Beatles;
you can use stuff() to convert rows as comma separated values
select
EmployeeID,
stuff((
SELECT ',' + FPProjectMaster.GroupName
FROM FPProjectInfo AS t INNER JOIN
FPProjectMaster ON t.ProjectID = FPProjectMaster.ProjectID
WHERE (t.EmployeeID = FPProjectInfo.EmployeeID)
And t.STatusID = 1
ORDER BY t.ProjectID
for xml path('')
),1,1,'') as name_csv
from FPProjectInfo
group by EmployeeID;
Thanks #AlexKuznetsov for the reference to get this answer.
A clean and flexible solution in MS SQL Server 2005/2008 is to create a CLR Agregate function.
You'll find quite a few articles (with code) on google.
It looks like this article walks you through the whole process using C#.
If you're executing this through PHP, what about this?
$hQuery = mysql_query("SELECT * FROM users");
while($hRow = mysql_fetch_array($hQuery)) {
$hOut .= $hRow['username'] . ", ";
}
$hOut = substr($hOut, 0, strlen($hOut) - 1);
echo $hOut;

Resources