SQL-SERVER search array - sql

Is that a way to search array by using SQL?
SELECT Top 1 IVDTL.YourPONo
FROM IV
INNER JOIN IVDTL
ON IV.DOCKEY = IVDTL.DOCKEY
WHERE DocNo = /*array[0]*/
Any one have idea?

For Mysql you can use FIND_IN_SET
SELECT Top 1 IVDTL.YourPONo
from IV
INNER JOIN IVDTL
ON IV.DOCKEY = IVDTL.DOCKEY
WHERE FIND_IN_SET(DocNo ,'array[0]') > 0
Update :
For SQL SERVER
You need a split string function and split the values in array to individual rows. Then it can be used in IN clause
For SQL SERVER 2016
you can use STRING_SPLIT function
SELECT Top 1 IVDTL.YourPONo
FROM IV
INNER JOIN IVDTL
ON IV.DOCKEY = IVDTL.DOCKEY
WHERE DocNo in (select value from STRING_SPLIT('array[0]',',')
For any thing less than SQL SERVER 2016
use any one of the methods from below excellent article Split strings the right way – or the next best way
Here is one way using XML
CREATE FUNCTION dbo.SplitStrings_XML
(
#List NVARCHAR(MAX),
#Delimiter 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, #Delimiter, '</i><i>')
+ '</i>').query('.')
) AS a CROSS APPLY x.nodes('i') AS y(i)
);
After creating the function use the function in your query
SELECT Top 1 IVDTL.YourPONo
FROM IV
INNER JOIN IVDTL
ON IV.DOCKEY = IVDTL.DOCKEY
WHERE DocNo in (select value from SplitStrings_XML('array[0]',',')

Related

How Do I Split a Delimited String without creating a function or using the STRING_SPLIT function

Need to split a delimited string in the code without using the STRING_SPLIT function.
This is in SQL Server 2016 running in compatibility mode 110 (SQL Server 2012), this unfortunately cannot be changed.
SELECT
rsys.Netbios_Name0 As Name,
(SELECT bg.Name
FROM vSMS_BoundaryGroup bg
WHERE bg.GroupID = value) As 'Boundary Group',
rsys.Full_Domain_Name0 AS Domain
FROM
v_R_System rsys
INNER JOIN
v_GS_BOUNDARYGROUPCACHE bgc ON bgc.ResourceID = rsys.ResourceID
OUTER APPLY
STRING_SPLIT(bgc.BoundaryGroupIDs0, ',')
I get this error:
Msg 208, Level 16, State 1, Line 1
Invalid object name 'STRING_SPLIT'
Created the following function:
Using XML
create a table valued function like the below which is using Sql XML feature to split the string.
CREATE FUNCTION [dbo].StringSplitXML
(
#String VARCHAR(MAX), #Separator CHAR(1)
)
RETURNS #RESULT TABLE(Value VARCHAR(MAX))
AS
BEGIN
DECLARE #XML XML
SET #XML = CAST(
('<i>' + REPLACE(#String, #Separator, '</i><i>') + '</i>')
AS XML)
INSERT INTO #RESULT
SELECT t.i.value('.', 'VARCHAR(MAX)')
FROM #XML.nodes('i') AS t(i)
WHERE t.i.value('.', 'VARCHAR(MAX)') <> ''
RETURN
END
Below example shows how we can use the above function to split the comma delimited string
SELECT *
FROM StringSplitXML('Basavaraj,Kalpana,Shree',',')
With my query
SELECT
rsys.Netbios_Name0 As Name
, (SELECT
bg.Name
FROM vSMS_BoundaryGroup bg
WHERE bg.GroupID = value) As 'Boundary Group',
rsys.Full_Domain_Name0 AS Domain
FROM v_R_System rsys
INNER JOIN v_GS_BOUNDARYGROUPCACHE bgc ON bgc.ResourceID = rsys.ResourceID
OUTER APPLY StringSplitXML(bgc.BoundaryGroupIDs0, ',')
order by rsys.Netbios_Name0
Thank you
https://sqlhints.com/tag/split-delimited-string-in-sql/

Order Concatenated field

I have a field which is a concatenation of single letters. I am trying to order these strings within a view. These values can't be hard coded as there are too many. Is someone able to provide some guidance on the function to use to achieve the desired output below? I am using MSSQL.
Current output
CustID | Code
123 | BCA
Desired output
CustID | Code
123 | ABC
I have tried using a UDF
CREATE FUNCTION [dbo].[Alphaorder] (#str VARCHAR(50))
returns VARCHAR(50)
BEGIN
DECLARE #len INT,
#cnt INT =1,
#str1 VARCHAR(50)='',
#output VARCHAR(50)=''
SELECT #len = Len(#str)
WHILE #cnt <= #len
BEGIN
SELECT #str1 += Substring(#str, #cnt, 1) + ','
SET #cnt+=1
END
SELECT #str1 = LEFT(#str1, Len(#str1) - 1)
SELECT #output += Sp_data
FROM (SELECT Split.a.value('.', 'VARCHAR(100)') Sp_data
FROM (SELECT Cast ('<M>' + Replace(#str1, ',', '</M><M>') + '</M>' AS XML) AS Data) AS A
CROSS APPLY Data.nodes ('/M') AS Split(a)) A
ORDER BY Sp_data
RETURN #output
END
This works when calling one field
ie.
Select CustID, dbo.alphaorder(Code)
from dbo.source
where custid = 123
however when i try to apply this to top(10) i receive the error
"Invalid length parameter passed to the LEFT or SUBSTRING function."
Keeping in mind my source has ~4million records, is this still the best solution?
Unfortunately i am not able to normalize the data into a separate table with records for each Code.
This doesn't rely on a id column to join with itself, performance is almost as fast
as the answer by #Shnugo:
SELECT
CustID,
(
SELECT
chr
FROM
(SELECT TOP(LEN(Code))
SUBSTRING(Code,ROW_NUMBER() OVER(ORDER BY (SELECT NULL)),1)
FROM sys.messages) A(Chr)
ORDER by chr
FOR XML PATH(''), type).value('.', 'varchar(max)'
) As CODE
FROM
source t
First of all: Avoid loops...
You can try this:
DECLARE #tbl TABLE(ID INT IDENTITY, YourString VARCHAR(100));
INSERT INTO #tbl VALUES ('ABC')
,('JSKEzXO')
,('QKEvYUJMKRC');
--the cte will create a list of all your strings separated in single characters.
--You can check the output with a simple SELECT * FROM SeparatedCharacters instead of the actual SELECT
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,(
SELECT Chr As [*]
FROM SeparatedCharacters sc1
WHERE sc1.ID=t.ID
ORDER BY sc1.Chr
FOR XML PATH(''),TYPE
).value('.','nvarchar(max)') AS Sorted
FROM #tbl t;
The result
ID YourString Sorted
1 ABC ABC
2 JSKEzXO EJKOSXz
3 QKEvYUJMKRC CEJKKMQRUvY
The idea in short
The trick is the first CROSS APPLY. This will create a tally on-the-fly. You will get a resultset with numbers from 1 to n where n is the length of the current string.
The second apply uses this number to get each character one-by-one using SUBSTRING().
The outer SELECT calls from the orginal table, which means one-row-per-ID and use a correalted sub-query to fetch all related characters. They will be sorted and re-concatenated using FOR XML. You might add DISTINCT in order to avoid repeating characters.
That's it :-)
Hint: SQL-Server 2017+
With version v2017 there's the new function STRING_AGG(). This would make the re-concatenation very easy:
WITH SeparatedCharacters AS
(
SELECT *
FROM #tbl
CROSS APPLY
(SELECT TOP(LEN(YourString)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values) A(Nmbr)
CROSS APPLY
(SELECT SUBSTRING(YourString,Nmbr,1))B(Chr)
)
SELECT ID,YourString
,STRING_AGG(sc.Chr,'') WITHIN GROUP(ORDER BY sc.Chr) AS Sorted
FROM SeparatedCharacters sc
GROUP BY ID,YourString;
Considering your table having good amount of rows (~4 Million), I would suggest you to create a persisted calculated field in the table, to store these values. As calculating these values at run time in a view, will lead to performance problems.
If you are not able to normalize, add this as a denormalized column to the existing table.
I think the error you are getting could be due to empty codes.
If LEN(#str) = 0
BEGIN
SET #output = ''
END
ELSE
BEGIN
... EXISTING CODE BLOCK ...
END
I can suggest to split string into its characters using referred SQL function.
Then you can concatenate string back, this time ordered alphabetically.
Are you using SQL Server 2017? Because with SQL Server 2017, you can use SQL String_Agg string aggregation function to concatenate characters splitted in an ordered way as follows
select
t.CustId, string_agg(strval, '') within GROUP (order by strval)
from CharacterTable t
cross apply dbo.SPLIT(t.code) s
where strval is not null
group by CustId
order by CustId
If you are not working on SQL2017, then you can follow below structure using SQL XML PATH for concatenation in SQL
select
CustId,
STUFF(
(
SELECT
'' + strval
from CharacterTable ct
cross apply dbo.SPLIT(t.code) s
where strval is not null
and t.CustId = ct.CustId
order by strval
FOR XML PATH('')
), 1, 0, ''
) As concatenated_string
from CharacterTable t
order by CustId

How to SORT in order as entered in SQL Server?

I'm using SQL Server and I'm trying to find results but I would like to get the results in the same order as I had input the conditions.
My code:
SELECT
AccountNumber, EndDate
FROM
Accounts
WHERE
AccountNumber IN (212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689) -- I would like the results to be in the same order as these numbers.
Here is an in-line approach
Example
Declare #List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689'
Select A.AccountNumber
,A.EndDate
From Accounts A
Join (
Select RetSeq = Row_Number() over (Order By (Select null))
,RetVal = v.value('(./text())[1]', 'int')
From (values (convert(xml,'<x>' + replace(#List,',','</x><x>')+'</x>'))) x(n)
Cross Apply n.nodes('x') node(v)
) B on A.AccountNumber = B.RetVal
Order By B.RetSeq
EDIT - the subquery Returns
RetSeq RetVal
1 212345
2 312345
3 145687
4 658975
5 256987
6 365874
7 568974
8 124578
9 125689
You can replace IN with a JOIN, and set a field for ordering, like this:
SELECT AccountNumber , EndDate
FROM Accounts a
JOIN (
SELECT 212345 AS Number, 1 AS SeqOrder
UNION ALL
SELECT 312345 AS Number, 2 AS SeqOrder
UNION ALL
SELECT 145687 AS Number, 3 AS SeqOrder
UNION ALL
... -- and so on
) AS inlist ON inlist.Number = a.AccountNumber
ORDER BY inlist.SeqOrder
I will offer one more approach I just found out, but this needs v2016. Regrettfully the developers forgot to include the index into the resultset of STRING_SPLIT(), but this would work and is documented:
A solution via FROM OPENJSON():
DECLARE #str VARCHAR(100) = 'val1,val2,val3';
SELECT *
FROM OPENJSON('["' + REPLACE(#str,',','","') + '"]');
The result
key value type
0 val1 1
1 val2 1
2 val3 1
The documentation tells clearly:
When OPENJSON parses a JSON array, the function returns the indexes of the elements in the JSON text as keys.
This is not an answer, just some test-code to check John Cappelletti's approach.
DECLARE #tbl TABLE(ID INT IDENTITY,SomeGuid UNIQUEIDENTIFIER);
--Create more than 6 mio rows with an running number and a changing Guid
WITH tally AS (SELECT ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) AS Nmbr
FROM master..spt_values v1
CROSS JOIN master..spt_values v2)
INSERT INTO #tbl
SELECT NEWID() from tally;
SELECT COUNT(*) FROM #tbl; --6.325.225 on my machine
--Create an XML with nothing more than a list of GUIDs in the order of the table's ID
DECLARE #xml XML=
(SELECT SomeGuid FRom #tbl ORDER BY ID FOR XML PATH(''),ROOT('root'),TYPE);
--Create one invalid entry
UPDATE #tbl SET SomeGuid = NEWID() WHERE ID=10000;
--Read all GUIDs out of the XML and number them
DECLARE #tbl2 TABLE(Position INT,TheGuid UNIQUEIDENTIFIER);
INSERT INTO #tbl2(Position,TheGuid)
SELECT ROW_NUMBER() OVER(ORDER BY (SELECT NULL))
,g.value(N'text()[1]',N'uniqueidentifier')
FROM #xml.nodes(N'/root/SomeGuid') AS A(g);
--then JOIN them via "Position" and check,
--if there are rows, where not the same values get into the same row.
SELECT *
FROM #tbl t
INNER JOIN #tbl2 t2 ON t2.Position=t.ID
WHERE t.SomeGuid<>t2.TheGuid;
At least in this simple case I always get exactly only the one record back which was invalidated...
Okay, after some re-thinking I'll offer the ultimative XML based type-safe and sort-safe splitter:
Declare #List varchar(max)='212345, 312345, 145687, 658975, 256987, 365874, 568974, 124578, 125689';
DECLARE #delimiter VARCHAR(10)=', ';
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML) AS ListXml
)
,Tally(Nmbr) As
(
SELECT TOP((SELECT ElementCount FROM Casted)) ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM master..spt_values v1 CROSS JOIN master..spt_values v2
)
SELECT Tally.Nmbr AS Position
,(SELECT ListXml.value('(/x[sql:column("Tally.Nmbr")])[1]','int') FROM Casted) AS Item
FROM Tally;
The trick is to create a list of running numbers with the fitting number of element (a number's table was even better) and to pick the elements according to their position.
Hint: This is rather slow...
UPDATE: even better:
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML)
.query('
for $x in /x
return <x p="{count(/x[. << $x])}">{$x/text()[1]}</x>
') AS ListXml
)
SELECT x.value('#p','int') AS Position
,x.value('text()[1]','int') AS Item
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
Elements are create as
<x p="99">TheValue</x>
Regrettfully the XQuery function position() is not available to retrieve the value. But you can use the trick to count all elements before a given node. this is scaling badly, as this count must be performed over and over. The more elements the worse it goes...
UPDATE2: With a known count of elements one might use this (much better performance)
Use XQuery to iterate a literally given list:
WITH Casted AS
(
SELECT (LEN(#List)-LEN(REPLACE(#List,#delimiter,'')))/LEN(REPLACE(#delimiter,' ','.')) + 1 AS ElementCount
,CAST('<x>' + REPLACE((SELECT #List AS [*] FOR XML PATH('')),#delimiter,'</x><x>')+'</x>' AS XML)
.query('
for $i in (1,2,3,4,5,6,7,8,9)
return <x p="{$i}">{/x[$i]/text()[1]}</x>
') AS ListXml
)
SELECT x.value('#p','int') AS Position
,x.value('text()[1]','int') AS Item
FROM Casted
CROSS APPLY Casted.ListXml.nodes('/x') AS A(x);
In Azure SQL, there is now extended version of STRING_SPLIT which also can return the order of items if the third optional argument enable_ordinal is set to 1.
Then this simple task is finally easy:
DECLARE #string AS varchar(200) = 'a/b/c/d/e'
DECLARE #position AS int = 3
SELECT value FROM STRING_SPLIT(#string, '/', 1) WHERE ordinal = #position
Unfortunately not available in SQL Server 2019, only in Azure for now, lets hope it will be in SQL Server 2022.

SSRS selecting results based on comma delimited list with like statement

Based on my question SSRS selecting results based on comma delimited list
Is it possible to do this, but instead of doing this as a an EQUALS, can it be done as below?
WHERE value like 'abc%','def%'
One thing to note is that the % is not included in the list.
One option is to split the passed in SSRS variable (CSV) into a table and join on that
DECLARE #tab TABLE (Col1 NVARCHAR(200))
INSERT INTO #tab (Col1)
VALUES (N'abc'),(N'def'),(N'xyz'),(N'nop'),(N'ghi'),(N'lmn')
DECLARE #substrings NVARCHAR(200) = 'abc,def,ghi'
;WITH cteX
AS( --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)
)
SELECT
T.*
FROM #tab T
INNER JOIN cteX X ON X.Strings = T.Col1
gives the following result

Call SQL Functions after PIVOT

I have a stored procedure that's taking a very long time because I have 2 function calls that are being called before a PIVOT, which means it's calling the functions 5 times for each record rather than once for each record. How can I get rewrite my query so that the 2 function calls right at the end of the query are run after the Pivot rather than before?
Here's the query
CREATE TABLE #Temp
(
ServiceRecordID INT,
LocationStd VARCHAR(1000),
AreaServedStd VARCHAR(1000),
RegionalLimited BIT,
Region VARCHAR(255),
Visible BIT
)
DECLARE #RegionCount INT
SELECT #RegionCount = COUNT(RegionID) FROM Regions WHERE SiteID = #SiteID AND RegionID % 100 != 0
INSERT INTO #Temp
SELECT TOP (#RegionCount * 100) SR.ServiceRecordID, SR.LocationStd, SR.AreaServedStd, SR.RegionalLimited, R.Region,
CASE WHEN (ISNULL(R_SR.RegionID,0) = 0 AND ISNULL(R_SR_Serv.RegionID,0) = 0) THEN 0 ELSE 1 END AS Visible
FROM ServiceRecord SR
INNER JOIN Sites S ON SR.SiteID = S.SiteID
INNER JOIN Regions R ON R.SiteID = S.SiteID
LEFT OUTER JOIN lkup_Region_ServiceRecord R_SR ON R_SR.RegionID = R.RegionID AND R_SR.ServiceRecordID = SR.ServiceRecordID
LEFT OUTER JOIN lkup_Region_ServiceRecord_Serv R_SR_Serv ON R_SR_Serv.RegionID = R.RegionID AND R_SR_Serv.ServiceRecordID = SR.ServiceRecordID AND SR.RegionalLimited = 0
WHERE SR.SiteID = #SiteID
AND R.RegionID % 100 != 0
ORDER BY SR.ServiceRecordID
DECLARE #RegionList varchar(2000),#SQL varchar(max)
SELECT #RegionList = STUFF((SELECT DISTINCT ',[' + Region + ']' FROM #Temp ORDER BY ',[' + Region + ']' FOR XML PATH('')),1,1,'')
SET #SQL='SELECT * FROM
(SELECT ServiceRecordID,
dbo.fn_ServiceRecordGetServiceName(ServiceRecordID,'''') AS ServiceName,
LocationStd,
AreaServedStd,
RegionalLimited,
Region As Region,
dbo.fn_GetOtherRegionalSitesForServiceRecord(ServiceRecordID) AS OtherSites,
CAST(Visible AS INT) AS Visible FROM #Temp) B PIVOT(MAX(Visible) FOR Region IN (' + #RegionList + ')) A'
EXEC(#SQL)
Move the function calls after the PIVOT:
SET #SQL='
SELECT
A.*,
N.ServiceName,
S.OtherSites
FROM
(
SELECT
ServiceRecordID,
LocationStd,
AreaServedStd,
RegionalLimited,
Region,
CAST(Visible AS INT) AS Visible
FROM #Temp
) B
PIVOT(MAX(Visible) FOR Region IN (' + #RegionList + ')) A
OUTER APPLY (
SELECT dbo.fn_ServiceRecordGetServiceName(A.ServiceRecordID,'''')
) N (ServiceName)
OUTER APPLY (
SELECT dbo.fn_GetOtherRegionalSitesForServiceRecord(A.ServiceRecordID)
) S (OtherSites);
';
Or just put them in the outer SELECT:
SET #SQL='
SELECT
A.*,
ServiceName = dbo.fn_ServiceRecordGetServiceName(A.ServiceRecordID,''''),
OtherSites = dbo.fn_GetOtherRegionalSitesForServiceRecord(A.ServiceRecordID)
FROM
(
SELECT
ServiceRecordID,
LocationStd,
AreaServedStd,
RegionalLimited,
Region,
CAST(Visible AS INT) AS Visible
FROM #Temp
) B
PIVOT(MAX(Visible) FOR Region IN (' + #RegionList + ')) A
';
If you can possibly convert those functions to be table-valued rowset-returning consisting of a single SELECT statement, you may get a huge performance improvement as well.
CREATE FUNCTION dbo.fn_ServiceRecordGetServiceName2(
#ServiceRecordID itn
)
RETURNS TABLE
AS
RETURN ( -- single select statement
SELECT ServiceName = Blah
FROM dbo.Gorp
WHERE Gunk = 'Ralph'
);
Then
OUTER APPLY dbo.fn_ServiceRecordGetServiceName(ServiceRecordID,'''') N
And N.ServiceName will return the value(s).
Also, it is not correct to tack on square brackets to convert data values to valid sysnames. You should use the QuoteName function. This will ensure your system doesn't break no matter WHAT crazy value is entered 13 years from now (think 'Taiwan [North]'):
STUFF((SELECT DISTINCT ',' + QuoteName(Region) FROM #Temp ...
Note:
Since you said that this is for display in a web page, you don't even need to do the pivoting on the server. Instead, return 2 rowsets to the client, one with the Site data and one with the column data for the Regions. You would need an additional pass through every row in the Region rowset to find out all the regions needed, but this can be done very quickly. Finally, adjust your program code to step through the Region rows as needed for each matching Site, and created your output.
One reason this is worth the investment is that if your application grows in size, you can always throw another web server at the problem, but it's a lot harder to throw another database at it. A new web server will cost less than continually beefing up your SQL Server.
P.S. Even dynamic SQL is easier to deal with when you format it well. :)