SQL transform semi-colon list into relationship table - sql

I currently have a code table containing a list of types (Type_ID, Description), but they are saved in another table as ID;;ID;;ID...etc
I am looking for a script that will take those ID's and place them in a relationship table corresponding to there Type ID
For example in table A the Type_ID entries could look like:
1;;2;;4
1
3;;4
1;;2;;3;;4
I am completely stumped on how to accomplish this and any help is appreciated.

First of all, I would probably recommend going the UDF route (so that you don't reinvent the wheel). However, given that this sounds like a one-off activity, you could just use the following:
declare #output table (parentKey int, value int)
declare #values table (idx int identity(1, 1), parentKey int, value varchar(255))
-- Modify the below query to capture the data from your table
insert into #values (parentKey, value) values(1, '1;;2;;4'),(2, '1'),(3, '3;;4'),(4, '1;;2;;3;;4')
declare #i int
declare #cnt int
select #i = MIN(idx) - 1, #cnt = MAX(idx) from #values
while(#i < #cnt)
begin
select #i = #i + 1
declare #value varchar(255)
declare #key int
select #value = value, #key = parentKey from #values where idx = #i
declare #idx int
declare #next int
select #idx = 1
while(#idx <= LEN(#value))
begin
select #next = CHARINDEX(';;', #value, #idx)
if(#next > #idx)
begin
insert into #output (parentKey, value) values(#key, SUBSTRING(#value, #idx, #next - #idx))
select #idx = #next + 2
end
else
begin
insert into #output (parentKey, value) values(#key, SUBSTRING(#value, #idx, LEN(#value) - #idx + 1))
select #idx = LEN(#value) + 1
end
end
end
select * from #output
The #output table variable now contains the mapping you're looking for. You can either copy from that to your destination at the end, or you can remove #output from the query and substitute equivalent inserts directly into your relationship table.

Probably the easiest way is to use a UDF (User Defined Function), such as the Split functions outlined here.

Related

SQL Query for searching for a specific part of a string in a column

I want to search for a specific part of a nvarchar text string in a column.
The text string is structured like this X#2016-06-17#7483631#2016-06-27#2#167890##920, it's split up by #. I want to find 6th text block, 167890 in this case.
Is there a simple way to do this?
Perhaps with a little XML
Example
Declare #YourTable table (SomeCol varchar(500))
Insert Into #YourTable values
('X#2016-06-17#7483631#2016-06-27#2#167890##920')
Select SomeCol
,Pos6 = cast('<x>' + replace(A.SomeCol,'#','</x><x>')+'</x>' as xml).value('/x[6]','varchar(50)')
From #YourTable A
Returns
SomeCol Pos6
X#2016-06-17#7483631#2016-06-27#2#167890##920 167890
Create a SPLIT function, returning each part with an index like below:
CREATE FUNCTION [dbo].[StringSplit_WithIndex] (#str_in VARCHAR(8000),#separator VARCHAR(4) ) RETURNS #strtable TABLE (strval VARCHAR(8000),rn INT IDENTITY(1,1)) AS
BEGIN
DECLARE
#Occurrences INT,
#Counter INT,
#tmpStr VARCHAR(8000)
SET #Counter = 0
IF SUBSTRING(#str_in,LEN(#str_in),1) <> #separator
SET #str_in = #str_in + #separator
SET #Occurrences = (DATALENGTH(REPLACE(#str_in,#separator,#separator+'#')) - DATALENGTH(#str_in))/ DATALENGTH(#separator)
SET #tmpStr = #str_in
WHILE #Counter <= #Occurrences
BEGIN
SET #Counter = #Counter + 1
INSERT INTO #strtable
VALUES ( SUBSTRING(#tmpStr,1,CHARINDEX(#separator,#tmpStr)-1))
SET #tmpStr = SUBSTRING(#tmpStr,CHARINDEX(#separator,#tmpStr)+1,8000)
IF DATALENGTH(#tmpStr) = 0
BREAK
END
RETURN
END
Then, all you need to use the "rn" field after splitting like:
DECLARE #s VARCHAR(255) = 'X#2016-06-17#7483631#2016-06-27#2#167890##920'
SELECT *
FROM [StringSplit_WithIndex](#s,'#')
WHERE rn=6

SQL Server procedure declare a list

My SQL code is fairly simple. I'm trying to select some data from a database like this:
SELECT * FROM DBTable
WHERE id IN (1,2,5,7,10)
I want to know how to declare the list before the select (in a variable, list, array, or something) and inside the select only use the variable name, something like this:
VAR myList = "(1,2,5,7,10)"
SELECT * FROM DBTable
WHERE id IN myList
You could declare a variable as a temporary table like this:
declare #myList table (Id int)
Which means you can use the insert statement to populate it with values:
insert into #myList values (1), (2), (5), (7), (10)
Then your select statement can use either the in statement:
select * from DBTable
where id in (select Id from #myList)
Or you could join to the temporary table like this:
select *
from DBTable d
join #myList t on t.Id = d.Id
And if you do something like this a lot then you could consider defining a user-defined table type so you could then declare your variable like this:
declare #myList dbo.MyTableType
That is not possible with a normal query since the in clause needs separate values and not a single value containing a comma separated list. One solution would be a dynamic query
declare #myList varchar(100)
set #myList = '1,2,5,7,10'
exec('select * from DBTable where id IN (' + #myList + ')')
You can convert the list of passed values into a table valued parameter and then select against this list
DECLARE #list NVARCHAR(MAX)
SET #list = '1,2,5,7,10';
DECLARE #pos INT
DECLARE #nextpos INT
DECLARE #valuelen INT
DECLARE #tbl TABLE (number int NOT NULL)
SELECT #pos = 0, #nextpos = 1;
WHILE #nextpos > 0
BEGIN
SELECT #nextpos = charindex(',', #list, #pos + 1)
SELECT #valuelen = CASE WHEN #nextpos > 0
THEN #nextpos
ELSE len(#list) + 1
END - #pos - 1
INSERT #tbl (number)
VALUES (convert(int, substring(#list, #pos + 1, #valuelen)))
SELECT #pos = #nextpos;
END
SELECT * FROM DBTable WHERE id IN (SELECT number FROM #tbl);
In this example the string passed in '1,2,5,7,10' is split by the commas and each value is added as a new row within the #tbl table variable. This can then be selected against using standard SQL.
If you intend to reuse this functionality you could go further and convert this into a function.
I've always found it easier to invert the test against the list in situations like this. For instance...
SELECT
field0, field1, field2
FROM
my_table
WHERE
',' + #mysearchlist + ',' LIKE '%,' + CAST(field3 AS VARCHAR) + ',%'
This means that there is no complicated mish-mash required for the values that you are looking for.
As an example, if our list was ('1,2,3'), then we add a comma to the start and end of our list like so: ',' + #mysearchlist + ','.
We also do the same for the field value we're looking for and add wildcards: '%,' + CAST(field3 AS VARCHAR) + ',%' (notice the % and the , characters).
Finally we test the two using the LIKE operator: ',' + #mysearchlist + ',' LIKE '%,' + CAST(field3 AS VARCHAR) + ',%'.
Alternative to #Peter Monks.
If the number in the 'in' statement is small and fixed.
DECLARE #var1 varchar(30), #var2 varchar(30), #var3 varchar(30);
SET #var1 = 'james';
SET #var2 = 'same';
SET #var3 = 'dogcat';
Select * FROM Database Where x in (#var1,#var2,#var3);
If you want input comma separated string as input & apply in in query in that then you can make Function like:
create FUNCTION [dbo].[Split](#String varchar(MAX), #Delimiter char(1))
returns #temptable TABLE (items varchar(MAX))
as
begin
declare #idx int
declare #slice varchar(8000)
select #idx = 1
if len(#String)<1 or #String is null return
while #idx!= 0
begin
set #idx = charindex(#Delimiter,#String)
if #idx!=0
set #slice = left(#String,#idx - 1)
else
set #slice = #String
if(len(#slice)>0)
insert into #temptable(Items) values(#slice)
set #String = right(#String,len(#String) - #idx)
if len(#String) = 0 break
end
return
end;
You can use it like :
Declare #Values VARCHAR(MAX);
set #Values ='1,2,5,7,10';
Select * from DBTable
Where id in (select items from [dbo].[Split] (#Values, ',') )
Alternatively if you don't have comma-separated string as input, You can try Table variable OR TableType Or Temp table like: INSERT using LIST into Stored Procedure

HOW to convert CSV to record set inside T-SQL?

In my stored procedure I am passing a filter (using "WHERE Column IN" clause) as a parameter. The value of the parameter is given as CSV. What is the best method to convert this CSV in to a record set.
Example:-
SELECT *
FROM Employee
WHERE Name IN ('John','Joe','Jerry','James')
and I need to pass the names as a parameter which is a CSV string like
"John,Joe,Jerry,James"
.
Take a look at Erland Sommarskog's articles. He has in-depth information on the different ways of doing this kind of thing:
Arrays and Lists in SQL Server
Create a split string function
CREATE FUNCTION [dbo].[SplitString]
(
#String VARCHAR(MAX) ,
#Delimiter VARCHAR(10)
)
RETURNS #RetTable TABLE(
String varchar(MAX)
)
AS
BEGIN
DECLARE #i INT ,
#j INT
SELECT #i = 1
WHILE #i <= LEN(#String)
BEGIN
SELECT #j = CHARINDEX(#Delimiter, #String, #i)
IF #j = 0
BEGIN
SELECT #j = LEN(#String) + 1
END
INSERT #RetTable SELECT SUBSTRING(#String, #i, #j - #i)
SELECT #i = #j + LEN(#Delimiter)
END
RETURN
END

SQL - using a variable for an IN clause

I wish to do something like the following:
declare #FrameNumber nvarchar(20)
set #FrameNumber = '(p1, p2)'
select from myTable where c1 in #FrameNumber
What is the correct syntax for this?
(for note: I need to pass the value of #FrameNumber in as a parameter to the stored procedure... so I have to at least use the string "p1, p2")
would prefure and answer that was SQL 7 compatible, but SQL 2005 would be sufficient.
DECLARE #FrameNumbers TABLE (code NVARCHAR(20) PRIMARY KEY)
INSERT
INTO #framenumbers
VALUES ('p1')
INSERT
INTO #framenumbers
VALUES ('p2')
SELECT *
FROM mytable
WHERE c1 IN
(
SELECT code
FROM #framenumbers
)
CREATE FUNCTION [dbo].[func_ParseStringToTable] (#stringIN varchar(2000))
RETURNS #tOUT TABLE(RoomID int) AS
BEGIN
DECLARE #pos int
SET #pos=CHARINDEX(',',#StringIN)
WHILE #pos>0
BEGIN
INSERT #tOUT(RoomID) SELECT LEFT(#StringIN,CHARINDEX(',',#StringIN)-1)
SET #stringIN = SUBSTRING(#StringIN,CHARINDEX(',',#StringIN)+1,LEN(#StringIN))
SET #pos=CHARINDEX(',',#StringIN)
END
IF LEN(#StringIN)>0
BEGIN
INSERT #tOUT(RoomID) SELECT #StringIN
END
RETURN
END
usage...
SELECT * FROM table WHERE id IN (func_ParseStringToTable(#ids))
You could put load those values into a table variable, or you could use dynamic sql. Here are examples of each:
TABLE VARIABLE
DECLARE #FrameNumbers TABLE (
Frame NVARCHAR(20)
)
INSERT INTO #FrameNumbers (
Frame
)
SELECT 'p1'
UNION ALL SELECT 'p2'
option 1:
SELECT * FROM myTable WHERE c1 in (
SELECT Frame
FROM #FrameNumbers
)
option 2:
SELECT
m.*
FROM myTable m
INNER JOIN #FrameNumbers f ON f.Frame = m.c1
All that is fine, but this is my favorite:
DYNAMIC SQL
DECLARE
#FrameNumber nvarchar(20),
#sql nvarchar(max),
#ParamDef nvarchar(1000)
SET #FrameNumber = '(p1, p2)'
SET #sql = N'SELECT FROM myTable WHERE c1 IN ' + #FrameNumber
EXECUTE dbo.sp_ExecuteSQL #sql
I have another solution to do with split function,
DECLARE #FrameNumber NVARCHAR(20)
SET #FrameNumber = 'p1,p2'
SELECT * FROM MyTable WHERE ProductCode IN
(SELECT Value FROM fn_Split(#FrameNumber, ','))
OutPut:
Split Functions:
CREATE FUNCTION fn_Split (
#String VARCHAR(8000)
,#Delimiter CHAR(1)
)
RETURNS #temptable TABLE (Value VARCHAR(8000))
AS
BEGIN
DECLARE #idx INT
DECLARE #slice VARCHAR(8000)
SELECT #idx = 1
IF len(#String) < 1
OR #String IS NULL
RETURN
WHILE #idx != 0
BEGIN
SET #idx = charindex(#Delimiter, #String)
IF #idx != 0
SET #slice = left(#String, #idx - 1)
ELSE
SET #slice = #String
IF (len(#slice) > 0)
INSERT INTO #temptable (Value)
VALUES (#slice)
SET #String = right(#String, len(#String) - #idx)
IF len(#String) = 0
BREAK
END
RETURN
END
what version of SQL Server ?
If you are in 2008 you might be able to use table datatypes. Simplifies these things a lot.
If you are using Sql Server 2005+ have a look at this
--Split
DECLARE #textXML XML
DECLARE #data NVARCHAR(MAX),
#delimiter NVARCHAR(5)
SELECT #data = 'A,B,C',
#delimiter = ','
SELECT #textXML = CAST('<d>' + REPLACE(#data, #delimiter, '</d><d>') + '</d>' AS XML)
SELECT T.split.value('.', 'nvarchar(max)') AS data
FROM #textXML.nodes('/d') T(split)
You can you that as your in table to select from.
Have a look at XML Support in Microsoft SQL Server 2005
final solution:
DECLARE #FrameNumbers TABLE (FrameNumber NVARCHAR(20) PRIMARY KEY)
DECLARE #pos int
SET #pos=CHARINDEX(',',#FrameNumber)
WHILE #pos>0 BEGIN
INSERT #FrameNumbers SELECT LEFT(#FrameNumber,CHARINDEX(',',#FrameNumber)-1)
SET #FrameNumber = SUBSTRING(#FrameNumber,CHARINDEX(',',#FrameNumber)+1,LEN(#FrameNumber))
SET #pos=CHARINDEX(',',#FrameNumber)
END
IF LEN(#FrameNumber)>0 BEGIN
INSERT #FrameNumbers SELECT #FrameNumber
END
select from myTable where c1 in (select FrameNumber from #FrameNumbers)
thanks Quassnoi and Sam, this solution is just a combination of your solutions.

T-SQL: Looping through an array of known values

Here's my scenario:
Let's say I have a stored procedure in which I need to call another stored procedure on a set of specific ids; is there a way to do this?
i.e. instead of needing to do this:
exec p_MyInnerProcedure 4
exec p_MyInnerProcedure 7
exec p_MyInnerProcedure 12
exec p_MyInnerProcedure 22
exec p_MyInnerProcedure 19
Doing something like this:
*magic where I specify my list contains 4,7,12,22,19*
DECLARE my_cursor CURSOR FAST_FORWARD FOR
*magic select*
OPEN my_cursor
FETCH NEXT FROM my_cursor INTO #MyId
WHILE ##FETCH_STATUS = 0
BEGIN
exec p_MyInnerProcedure #MyId
FETCH NEXT FROM my_cursor INTO #MyId
END
My Main goal here is simply maintainability (easy to remove/add id's as the business changes), being able to list out all Id's on a single line... Performance shouldn't be as big of an issue
declare #ids table(idx int identity(1,1), id int)
insert into #ids (id)
select 4 union
select 7 union
select 12 union
select 22 union
select 19
declare #i int
declare #cnt int
select #i = min(idx) - 1, #cnt = max(idx) from #ids
while #i < #cnt
begin
select #i = #i + 1
declare #id = select id from #ids where idx = #i
exec p_MyInnerProcedure #id
end
What I do in this scenario is create a table variable to hold the Ids.
Declare #Ids Table (id integer primary Key not null)
Insert #Ids(id) values (4),(7),(12),(22),(19)
-- (or call another table valued function to generate this table)
Then loop based on the rows in this table
Declare #Id Integer
While exists (Select * From #Ids)
Begin
Select #Id = Min(id) from #Ids
exec p_MyInnerProcedure #Id
Delete from #Ids Where id = #Id
End
or...
Declare #Id Integer = 0 -- assuming all Ids are > 0
While exists (Select * From #Ids
where id > #Id)
Begin
Select #Id = Min(id)
from #Ids Where id > #Id
exec p_MyInnerProcedure #Id
End
Either of above approaches is much faster than a cursor (declared against regular User Table(s)). Table-valued variables have a bad rep because when used improperly, (for very wide tables with large number of rows) they are not performant. But if you are using them only to hold a key value or a 4 byte integer, with a index (as in this case) they are extremely fast.
use a static cursor variable and a split function:
declare #comma_delimited_list varchar(4000)
set #comma_delimited_list = '4,7,12,22,19'
declare #cursor cursor
set #cursor = cursor static for
select convert(int, Value) as Id from dbo.Split(#comma_delimited_list) a
declare #id int
open #cursor
while 1=1 begin
fetch next from #cursor into #id
if ##fetch_status <> 0 break
....do something....
end
-- not strictly necessary w/ cursor variables since they will go out of scope like a normal var
close #cursor
deallocate #cursor
Cursors have a bad rep since the default options when declared against user tables can generate a lot of overhead.
But in this case the overhead is quite minimal, less than any other methods here. STATIC tells SQL Server to materialize the results in tempdb and then iterate over that. For small lists like this, it's the optimal solution.
You can try as below :
declare #list varchar(MAX), #i int
select #i=0, #list ='4,7,12,22,19,'
while( #i < LEN(#list))
begin
declare #item varchar(MAX)
SELECT #item = SUBSTRING(#list, #i,CHARINDEX(',',#list,#i)-#i)
select #item
--do your stuff here with #item
exec p_MyInnerProcedure #item
set #i = CHARINDEX(',',#list,#i)+1
if(#i = 0) set #i = LEN(#list)
end
I usually use the following approach
DECLARE #calls TABLE (
id INT IDENTITY(1,1)
,parameter INT
)
INSERT INTO #calls
select parameter from some_table where some_condition -- here you populate your parameters
declare #i int
declare #n int
declare #myId int
select #i = min(id), #n = max(id) from #calls
while #i <= #n
begin
select
#myId = parameter
from
#calls
where id = #i
EXECUTE p_MyInnerProcedure #myId
set #i = #i+1
end
CREATE TABLE #ListOfIDs (IDValue INT)
DECLARE #IDs VARCHAR(50), #ID VARCHAR(5)
SET #IDs = #OriginalListOfIDs + ','
WHILE LEN(#IDs) > 1
BEGIN
SET #ID = SUBSTRING(#IDs, 0, CHARINDEX(',', #IDs));
INSERT INTO #ListOfIDs (IDValue) VALUES(#ID);
SET #IDs = REPLACE(',' + #IDs, ',' + #ID + ',', '')
END
SELECT *
FROM #ListOfIDs
Make a connection to your DB using a procedural programming language (here Python), and do the loop there. This way you can do complicated loops as well.
# make a connection to your db
import pyodbc
conn = pyodbc.connect('''
Driver={ODBC Driver 13 for SQL Server};
Server=serverName;
Database=DBname;
UID=userName;
PWD=password;
''')
cursor = conn.cursor()
# run sql code
for id in [4, 7, 12, 22, 19]:
cursor.execute('''
exec p_MyInnerProcedure {}
'''.format(id))