another way to write this sql - sql-server

Environment: Sql Server 2008
have rows of a column that contains comma separated values.
what to get the row even if a single product exists in that csv.
this is how i can do it but was just wondering if another way to write it?
SELECT * FROM REWARDS
WHERE ProductCsv like '%banana%'
or ProductCsv like '%strawberry%'
or ProductCsv like '%orange%'

Your current query doesn't seem to accurately capture the results you want. What if we have a row like this:
bananaberry cream pie,strawberry shortcake,orange juice
Should this match your query or not? I think this would be more accurate:
WHERE ',' + ProductCsv + ',' LIKE '%,banana,%'
OR ',' + ProductCsv + ',' LIKE '%,strawberry,%'
OR ',' + ProductCsv + ',' LIKE '%,orange,%'
If you're just trying to find one item, this is probably much more efficient:
WHERE ',' + ProductCsv + ',' LIKE '%,banana,%'
You probably want to use a split function. For example:
CREATE FUNCTION dbo.SplitStrings_XML
(
#List NVARCHAR(MAX),
#Delim NVARCHAR(255)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
SELECT Item = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>' + REPLACE(#List, #Delim, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
GO
So now you can say:
SELECT * FROM dbo.REWARDS AS r
WHERE EXISTS
(
SELECT 1 FROM dbo.REWARDS AS r2
CROSS APPLY dbo.SplitStrings_XML(r2.ProductCsv, ',') AS x
WHERE x.Item IN ('orange','strawberry','banana')
AND r2.[key] = r.[key]
);
Or more simply:
SELECT DISTINCT r.ProductCsv --, other columns
FROM dbo.REWARDS AS r
CROSS APPLY dbo.SplitStrings_XML(r.ProductCsv, ',') AS x
WHERE x.Item IN ('orange','strawberry','banana');
The XML approach is a little brittle depending on the kinds of strings can be stored in the table, but there are many alternatives (including passing in your set via a TVP instead of as a list or separate values). For some much deeper background on split functions see these blog posts:
http://www.sqlperformance.com/2012/07/t-sql-queries/split-strings
http://www.sqlperformance.com/2012/08/t-sql-queries/splitting-strings-follow-up
http://www.sqlperformance.com/2012/08/t-sql-queries/splitting-strings-now-with-less-t-sql
That all said, I don't know if this is any better than what you have now.

Have a look at the following example using XML
DECLARE #Table TABLE(
CommaList VARCHAR(MAX)
)
INSERT INTO #Table SELECT 'banana,test,hello'
INSERT INTO #Table SELECT 'homer,banana,test,hello'
INSERT INTO #Table SELECT 'homer,banana'
INSERT INTO #Table SELECT '1,2,3'
;WITH XMLValues AS (
SELECT *,
CAST('<d>' + REPLACE(CommaList, ',', '</d><d>') + '</d>' AS XML) XMLValue
FROM #Table t
)
, SplitValues AS (
SELECT xv.*,
T2.Loc.value('.','VARCHAR(MAX)') SplitValue
FROM XMLValues xv
CROSS APPLY XMLValue.nodes('/d') as T2(Loc)
)
SELECT DISTINCT
CommaList
FROM SplitValues
WHERE SplitValue = 'banana'
xml Data Type Methods
nodes() Method (xml Data Type)
value() Method (xml Data Type)
Using Common Table Expressions

You could store all the values to be compared in a table
DECLARE #Product_list TABLE(
products VARCHAR(50)
)
Insert into #Product_list values
('banana'),
('strawberry'),
('orange')
SELECT * FROM REWARDS
join #Product_list
on ProductCsv like '%'+products+'%'

You could use a split function (there are many around on the web) to split ProductsCsv into individual elements and then compare that against banana, strawberry etc.
Ideally, you wouldn't store CSV data in a column, but instead have a separate table for that.

Related

Display All Columns from all Table by using Union ALL with Different no of Columns in Each Table

I have Three Tables with Different no of Columns. e.g T1(C1), T2(C1,C2,C3), T3(C1,C4). I want to generate a Dynamic SQL that will create a View like
CREATE VIEW [dbo].[vwData]
AS
SELECT C1,NULL AS C2,NULL AS C3,NULL AS C4
FROM DBO.T1
UNION ALL
SELECT C1,C2,C3,NULL AS C4
FROM DBO.T2
UNION ALL
SELECT C1,NULL AS C2,NULL AS C3,C4
FROM DBO.T3
I have achieved this goal by using two nested loop by Checking Each column If It is Existed in a table or not.
But in Production we have around 30 tables with around 60 Columns in Each table.
Create of Dynamic SQL is taking around 7 minutes and this is not Acceptable to us. We want to improve performance Further.
Immediate help would be highly appreciated.
Here's some Dynamic SQL which would create and execute what you describe. How does this compare to your current SQL's performance?
Fiddle: https://dbfiddle.uk/?rdbms=sqlserver_2017&fiddle=800747a3d832e6e29a15484665f5cc8b
declare #tablesOfInterest table(tableName sysname, sql nvarchar(max))
declare #allColumns table(columnName sysname)
declare #sql nvarchar(max)
insert #tablesOfInterest(tableName) values ('table1'), ('table2')
insert #allColumns (columnName)
select distinct c.name
from sys.columns c
where c.object_id in
(
select object_id(tableName)
from #tablesOfInterest
)
update t
set sql = 'select ' + columnSql + ' from ' + quotename(tableName)
from #tablesOfInterest t
cross apply
(
select string_agg(coalesce(quotename(c.Name), 'null') + ' ' + quotename(ac.columnName), ', ') within group (order by ac.columnName)
from #allColumns ac
left outer join sys.columns c
on c.object_id = object_id(t.tableName)
and c.Name = ac.columnName
) x(columnSql)
select #sql = string_agg(sql, ' union all ')
from #tablesOfInterest
print #sql
exec (#sql)
As mentioned in the comments, rather than running this dynamic SQL every time you need to execute this query, you could use it to generate a view which you can then reuse as required.
Adding indexes and filters to the underlying tables as appropriate could further improve performance; but without knowing more of the context, we can't give much advise on specifics.
You might try this:
I use some general tables where I know, that they share some of their columns to show the principles. Just replace the tables with your own tables:
Attention: I do not use these INFORMATION_SCHEMA tables to read their content. They serve as examples with overlapping columns...
DECLARE #statement NVARCHAR(MAX);
WITH cte(x) AS
(
SELECT
(SELECT TOP 1 * FROM INFORMATION_SCHEMA.TABLES FOR XML AUTO, ELEMENTS XSINIL,TYPE) AS [*]
,(SELECT TOP 1 * FROM INFORMATION_SCHEMA.COLUMNS FOR XML AUTO, ELEMENTS XSINIL,TYPE) AS [*]
,(SELECT TOP 1 * FROM INFORMATION_SCHEMA.ROUTINES FOR XML AUTO, ELEMENTS XSINIL,TYPE) AS [*]
--add all your tables here...
FOR XML PATH(''),TYPE
)
,AllColumns AS
(
SELECT DISTINCT a.value('local-name(.)','nvarchar(max)') AS ColumnName
FROM cte
CROSS APPLY x.nodes('/*/*') A(a)
)
,AllTables As
(
SELECT a.value('local-name(.)','nvarchar(max)') AS TableName
,a.query('*') ConnectedColumns
FROM cte
CROSS APPLY x.nodes('/*') A(a)
)
SELECT #statement=
STUFF((
(
SELECT 'UNION ALL SELECT ' +
'''' + TableName + ''' AS SourceTableName ' +
(
SELECT ',' + CASE WHEN ConnectedColumns.exist('/*[local-name()=sql:column("ColumnName")]')=1 THEN QUOTENAME(ColumnName) ELSE 'NULL' END + ' AS ' + QUOTENAME(ColumnName)
FROM AllColumns ac
FOR XML PATH('root'),TYPE
).value('.','nvarchar(max)') +
' FROM ' + REPLACE(QUOTENAME(TableName),'.','].[')
FROM AllTables
FOR XML PATH(''),TYPE).value('.','nvarchar(max)')
),1,10,'');
EXEC( #statement);
Short explanation:
The first row of each table will be tranformed into an XML. Using AUTO-mode will use the table's name in the <root> and add all columns as nested elements.
The second CTE will create a distinct list of all columns existing in any of the tables.
the third CTE will extract all Tables with their connected columns.
The final SELECT will use a nested string-concatenation to create a UNION ALL SELECT of all columns. The existance of a given name will decide, whether the column is called with its name or as NULL.
Just use PRINT to print out the #statement in order to see the resulting dynamically created SQL command.

What will be STRING_SPLIT function alternative in DataSet SSRS

DataSet:dsStudent (It's a DropDown in the Report)
DECLARE #gid VARCHAR(36) = CONVERT(VARCHAR(36), NEWID());
SELECT CAST(StudentId AS VARCHAR(MAX))+#gid AS StudentId, StudentName
FROM dbo.Student1;
DataSet:dsStudentFilter (Use to Capture data selected from DropDown)
SELECT StudentName
FROM Student1
WHERE StudentId IN
(
SELECT REPLACE(b.value('text()[1]', 'NVARCHAR(MAX)'), RIGHT(b.value('text()[1]', 'NVARCHAR(MAX)'), 36), '')
FROM
(
VALUES
(CAST('<x>' + REPLACE(#StudentId, ',', '</x><x>') + '</x>' AS XML))
) A (a)
CROSS APPLY a.nodes('/x') B(b)
);
Now, when I preview the report I am getting below error:
Report Design:
In dsStudentFilter DataSet of SSRS report I want to do something like below:
DECLARE #StudentId VARCHAR(MAX)
= '1111112EE300718-79A4-4260-A5E9-22B7CA71998,1111122EE300718-79A4-4260-A5E9-22B7CA71998';
SELECT StudentName
FROM Student1
WHERE StudentId IN
(
SELECT value
FROM STRING_SPLIT(REPLACE(#StudentId, RIGHT(#StudentId, 36), ''), ',')
);
But I cannot use STRING_SPLIT in DataSet is there any alternative to do so?
Data in the table:
Above query will display Jay and Sam.
If you are allowed to do so, you can create an inline table valued function on the SQL-Server. Another chance was to use a stored procedure and create the IN-clause within dyanmic SQL...
Or you can use this approach for inline splitting:
DECLARE #StudentId VARCHAR(MAX) = '3,5,6,7';
SELECT o.*
FROM sys.objects o
WHERE o.object_id IN
(
SELECT b.value('text()[1]','int')
FROM (VALUES(CAST('<x>' + REPLACE(#StudentId,',','</x><x>') + '</x>' AS XML)))A(a)
CROSS APPLY a.nodes('/x') B(b)
);
The value list is transfered from 3,5,6,7 to <x>3</x><x>5</x><x>6</x><x>7</x> and then handled as XML...
Hint:
For your example you will have to use uniqueidentifier instead of int within .value()
Here is one workaround:
DECLARE #StudentId VARCHAR(MAX)
= '291540054631905-414A-4669-A941-E21ACB3C0912,291766154631905-414A-4669-A941-E21ACB3C0912';
SELECT StudentName
FROM Student
WHERE ',' + #StudentId + ',' LIKE '%,' + StudentId + ',%';
Demo
The basic idea is to search for a given StudentId directly in the input string, separated by commas on both sides. We add commas to the very start and end of the input string before comparing to ensure that every StudentId in fact is always separated by commas.

Select rows with any member of list of substrings in string

In a Micrososft SQL Server table I have a column with a string.
Example:
'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3'
I also have a dynamic list of substrings
Example:
('icouldnt', 'stuff', 'banana')
I don't care for string manipulation. The substrings could also be called:
('%icouldnt%', '%stuff%', '%banana%')
What's the best way to find all rows where the string contains one of the substrings?
Solutions that are not possible:
multiple OR Statements in the WHERE clause, the list is dynamic
external Code to do a "for each", its a multi value parameter from the reportbuilder, so nothing useful here
changing the database, its the database of a tool a costumer is using and we can't change it, even if we would like... so much
I really cant believe how hard such a simple problem can turn out. It would need a "LIKE IN" command to do it in a way that looks ok. Right now I cant think of anything but a messy temp table.
One option is to use CHARINDEX
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3' )
;WITH cteX
AS(
SELECT 'icouldnt' Strings
UNION ALL
SELECT 'stuff'
UNION ALL
SELECT 'banana'
)
SELECT
T.*, X.Strings
FROM #tab T
CROSS APPLY (SELECT X.Strings FROM cteX X) X
WHERE CHARINDEX(X.Strings, T.Col1) > 1
Output
EDIT - using an unknown dynamic string variable - #substrings
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'Servernamexyz.server.operationunit.otherstuff.icouldnt.predict.domain.domain2.domain3' )
DECLARE #substrings NVARCHAR(200) = 'icouldnt,stuff,banana'
SELECT
T.*, X.Strings
FROM #tab T
CROSS APPLY
( --dynamically split the string
SELECT Strings = y.i.value('(./text())[1]', 'nvarchar(4000)')
FROM
(
SELECT x = CONVERT(XML, '<i>'
+ REPLACE(#substrings, ',', '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
) X
WHERE CHARINDEX(X.Strings, T.Col1) > 1

Comparing two tables and displaying the result as a separate output

I have two tables and the values like this, `
CREATE TABLE Location (ID int ,Location Varchar(500))
INSERT INTO Location values (1,'Loc3'),(2,'Loc4'),(3,'Loc5'),(4,'Loc7')
CREATE TABLE InputLocation (ID int ,Location Varchar(500))
Insert into InputLocation values(1,'Loc1,Loc2,Loc3,Loc4,Loc5,Loc6')
I need to get the output by matching each values from table Location with table InputLocation and need to display the output whichever not matched with 2nd table, i.e Loc1,Loc2,Loc6 , I have tried some code like this and it worked But i need even simpler code, Any help would be greatly appreciated
My code :
SELECT STUFF((select ','+ Data.C1
FROM
(select
n.r.value('.', 'varchar(50)') AS C1
from InputLocation as T
cross apply (select cast('<r>'+replace(replace(Location,'&','&'), ',', '</r><r>')+'</r>' as xml)) as S(XMLCol)
cross apply S.XMLCol.nodes('r') as n(r)) DATA
WHERE data.C1 NOT IN (SELECT Location
FROM Location) for xml path('')),1,1,'') As Output
your script is ok.
Another method will be to use SPLIT String as describe here.
http://www.sqlservercentral.com/articles/Tally+Table/72993/
use [dbo].[DelimitedSplit8K]
Suppose my comma seperated string won't be longer than 500 then in my custom UDF i make it 500 varchar instead of varchar(8000) in order to improve performance.
SELECT STUFF((
SELECT ',' + Data.item
FROM (
SELECT il.ID
,fn.item
FROM #InputLocation IL
CROSS APPLY (
SELECT *
FROM dbo.DelimitedSplit2K(il.Location, ',')
) fn
WHERE NOT EXISTS (
SELECT *
FROM #Location L
WHERE l.Location = fn.Item
)
) data
FOR XML path('')
), 1, 1, '') AS
OUTPUT
Use recursion to avoid using slow XML Reader:
;with tmp(DataItem, Location) as (
select cast(LEFT(Location, CHARINDEX(',',Location+',')-1) as nvarchar(50)),
cast(STUFF(Location, 1, CHARINDEX(',',Location+','), '') as nvarchar(50))
from [InputLocation]
union all
select cast(LEFT(Location, CHARINDEX(',',Location+',')-1) as nvarchar(50)),
cast(STUFF(Location, 1, CHARINDEX(',',Location+','), '') as nvarchar(50))
from tmp
where Location > ''
)
select STUFF((SELECT ',' + x.Location
from (
select DataItem as Location from tmp
except Select Location from [Location]) x
FOR XML path('')), 1, 1, '') AS OUTPUT

How to create comma delimited list from table with dynamic columns

I want to be able to grab all of the records in a table into a comma delimited list that I can then use to insert into a table on another database. Due to permission restrictions on the customer's server I cannot access any of the options when right-clicking on the database name, and all of the solutions I've found so far involve having permission to do so (e.g. Tasks > Export Data...)
I have tried using COALESCE to do this, however the problem is that my table could have any number of columns. Columns can be added/deleted at any time through the UI by the users and therefore I cannot hard code the columns in my select statement.
Here is what I have written so far, using a simple CTE statement where there are three columns (RowCode, RowOrd, RowText) and concatenating them into a variable that I print out. I just want to find a way to grab these column names dynamically instead of hard coding them. I'll also need to account for various types of column names by casting them each as varchar in the variable.
DECLARE #listStr VARCHAR(MAX)
;WITH tableData AS
(
SELECT *
FROM tableRows
)
SELECT
#listStr = ISNULL(#listStr + 'select ','select ') + '''' + RowCode + ''',''' + cast(RowOrd as varchar) + ''',''' + RowText + '''' + Char(13)
FROM
tableData
PRINT #listStr
The tableRows table contains the following records
RowCode RowOrd RowText
-----------------------
RowA 1 Row A
RowB 2 Row B
And the variable #listStr is currently printing this, which is correct
select 'RowA','1.00','Row A'
select 'RowB','2.00','Row B'
Thanks in advance!
With a bit of XML you can dynamically gather and "stringify" your values
Declare #tableRows table (RowCode varchar(50), RowOrd int, RowText varchar(50))
Insert Into #tableRows values
('RowA',1,'Row A'),
('RowB',2,'Row B')
Declare #listStr VARCHAR(MAX) = ''
Select #listStr = #listStr + C.String + char(13)
From #tableRows A
Cross Apply (Select XMLData = cast((Select A.* for XML RAW) as xml)) B
Cross Apply (
Select String = 'select '+Stuff((Select ',' +Value
From (
Select Value = ''''+attr.value('.','varchar(max)')+''''
From B.XMLData.nodes('/row') as A(r)
Cross Apply A.r.nodes('./#*') AS B(attr)
) X
For XML Path ('')),1,1,'')
) C
Select #listStr
Returns
select 'RowA','1','Row A'
select 'RowB','2','Row B'

Resources