I have a table and it has a value column that lists data as:
Row 1: '00','01','02','03'
Row 2: '03','02','09','08'
I have a couple of split functions
FUNCTION [dbo].[udf_Split](#String varchar(MAX), #Delimiter char(1))
returns #temptable TABLE (Item 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(Item) values(#slice)
set #String = right(#String,len(#String) - #idx)
if len(#String) = 0 break
end return end;
I'm trying to create a view of the table, with that column and then I'd like my view results to be a list of rows that have each value broken to its own row (and distinct) So would look like: (the tics can stay or go, don't care about them right now)
Row 1: 00
Row 2: 01
Row 3: 02
Row 4: 03
My view is pretty much a:
SELECT DISTINCT VALUE FROM TABLE
cross apply dbo.split(Value, ',') as Item
But it's not working. Can someone lend me some direction on how I should work this?
This is because you're SELECTing the field VALUE instead of Item.Item. You should do this:
SELECT DISTINCT x.Item
FROM TABLE
CROSS APPLY dbo.split(Value, ',') AS x
Additionally, your dbo.split function is not optimal. There are number of ways to split a string in a set-based fashion, instead of RBAR. 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)
);
GO
Sample usage:
;WITH CteData(Value) AS(
SELECT '''00'',''01'',''02'',''03''' UNION ALL
SELECT '''03'',''02'',''09'',''08'''
)
SELECT DISTINCT x.Item
FROM CteData d
CROSS APPLY dbo.SplitStrings_XML(d.Value, ',') x
Result:
Item
--------
'00'
'01'
'02'
'03'
'08'
'09'
For other string splitter, you can read this article by Aaron Bertrand.
Related
I have text column with numeric values separated by semicolons. I'm trying to figure out how to get the most frequent pair of values that appeared together in the same row. I've found a solution for a very similar problem in Python Finding the most frequent occurrences of pairs in a list of lists, but I don't know how to rewrite it in using SQL In example below it returns 2 and 3 because this pair appeared 3 times in the input set:
Input rows Output
---------- -------
';1;2;3;5' | '2;3'
';2;3' | '1;2'
';3;4;5;1;2' | '1;3'
';1;5;2' | '1;5'
Orginal data:
You may try with the following approach. First, using OPENJSON(), get all possible combinations. When OPENJSON parses a JSON array the indexes of the elements in the JSON text are returned as keys (0-based). Then, count the most frequent pair with DENSE_RANK().
Input:
CREATE TABLE #Items (
Id int,
ItemValues varchar(max)
)
INSERT INTO #Items
(Id, ItemValues)
VALUES
(1, '1;2;3;5'),
(2, '2;3'),
(3, '3;4;5;1;2'),
(4, '1;5;2')
Statement:
;WITH combinationsCTE AS (
SELECT
CASE
WHEN s1.[value] <= s2.[value] THEN CONCAT(s1.[value], ';', s2.[value])
ELSE CONCAT(s2.[value], ';', s1.[value])
END AS PairValue
FROM #Items i
CROSS APPLY (SELECT [key], [value] FROM OPENJSON('["' + REPLACE(i.ItemValues,';','","') + '"]')) s1
CROSS APPLY (SELECT [key], [value] FROM OPENJSON('["' + REPLACE(i.ItemValues,';','","') + '"]')) s2
WHERE (s1.[key] < s2.[key])
), rankingCTE AS (
SELECT
PairValue,
DENSE_RANK() OVER (ORDER BY COUNT(PairValue) DESC) AS PairRank
FROM combinationsCTE
GROUP BY PairValue
)
SELECT PairValue
FROM rankingCTE
WHERE PairRank = 1
Output:
PairValue
1;2
1;5
2;3
2;5
First have a split function
CREATE FUNCTION Splitfn(#String varchar(8000), #Delimiter char(1))
returns #temptable TABLE (items 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(Items) values(#slice)
set #String = right(#String,len(#String) - #idx)
if len(#String) = 0 break
end
return
end
Second Step To get all rows in a single string
Declare #val Varchar(MAX);
Select #val = COALESCE(#val + '; ' + YourColumn, YourColumn)
From YourTable
Third step,
SELECT TOP 1 items, count(*)
FROM dbo.Splitfn(#Val, ';')
WHERE LTRIM(RTRIM(items)) <> ''
GROUP BY items
ORDER BY Count(*) DESC
I need multi-valued columns divided into single values
SOS_ID ALLOCATED_PART_NBR ALLOCATED_SALES_ITM ALLOCATED_QTY
523 500~5008~038~5008 2302~~007~5û005 1~1~~~1~2
Note: if no values between ~ delimiter it should insert empty string.
I want the output like this:
SOS_ID ALLOCATED_PART_NBR ALLOCATED_SALES_ITM ALLOCATED_QTY
523 500 2302 1
523 5008 '' 1
523 038 007 ''
523 5008 5û005 ''
523 ''/NULL ''/NULL 1
523 ''/NULL ''/NULL 2
So... here's a method I got to work for what you wanted. First, you need a table-valued function that will split a string into fields based on a delimiter, and which will pad out the number of rows returned to a specified length:
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(N'[dbo].[SplitString]') AND type IN (N'FN', N'IF', N'TF', N'FS', N'FT'))
DROP FUNCTION [dbo].[SplitString]
GO
SET ANSI_NULLS ON
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[SplitString] (
#delimitedString nvarchar(4000),
#delimiter nvarchar(100),
#padRows int
)
/**************************************************************************
DESCRIPTION:
Accepts a delimited string and splits it at the specified
delimiter points. Returns the individual items as a table data
type with the ElementID field as the array index and the Element
field as the data
PARAMETERS:
#delimitedString - The string to be split
#delimiter - String containing the delimiter where
delimited string should be split
#padRows - Any rows less than this value will be padded
with empty rows (NULL means no padding)
RETURNS:
Table data type containing array of strings that were split with
the delimiters removed from the source string
USAGE:
SELECT ElementID, Element
FROM asi_SplitString('11111,22222,3333', ',', NULL)
ORDER BY ElementID
***************************************************************************/
RETURNS #tblArray TABLE
(
ElementID int IDENTITY(1,1),
Element nvarchar(1000)
)
AS
BEGIN
DECLARE #index int
DECLARE #siStart int
DECLARE #siDelSize int
DECLARE #count int
SET #count = 1;
SET #siDelSize = LEN(#delimiter);
--loop through source string and add elements to destination table array
WHILE LEN(#delimitedString) > 0
BEGIN
SET #index = CHARINDEX(#delimiter, #delimitedString);
IF #index = 0
BEGIN
INSERT INTO #tblArray VALUES (#delimitedString);
BREAK;
END
ELSE
BEGIN
INSERT INTO #tblArray VALUES(SUBSTRING(#delimitedString, 1,#index - 1));
SET #siStart = #index + #siDelSize;
SET #delimitedString = SUBSTRING(#delimitedString, #siStart , LEN(#delimitedString) - #siStart + 1);
END
SET #count += 1;
END
IF (#padRows IS NOT NULL)
WHILE (#count < #padRows)
BEGIN
SET #count += 1;
INSERT INTO #tblArray VALUES ('');
END
RETURN;
END
GO
Now you need a sample table with data to test this with (based on your question):
CREATE TABLE TestTable (SOS_ID nvarchar(10),
ALLOCATED_PART_NBR nvarchar(400),
ALLOCATED_SALES_ITM nvarchar(400),
ALLOCATED_QTY nvarchar(400))
INSERT INTO TestTable (SOS_ID, ALLOCATED_PART_NBR, ALLOCATED_SALES_ITM, ALLOCATED_QTY)
VALUES ('523', '500~5008~038~5008', '2302~~007~5û005', '1~1~~~1~2')
Now, some code that will transform the data above into the result you wanted:
DECLARE #fieldCount int;
WITH TildeCounts AS (
SELECT LEN(ALLOCATED_PART_NBR) - LEN(REPLACE(ALLOCATED_PART_NBR, '~', '')) AS TildeCount
FROM TestTable t
UNION ALL
SELECT LEN( ALLOCATED_SALES_ITM) - LEN(REPLACE( ALLOCATED_SALES_ITM, '~', '')) AS TildeCount
FROM TestTable t
UNION ALL
SELECT LEN(ALLOCATED_QTY) - LEN(REPLACE(ALLOCATED_QTY, '~', '')) AS TildeCount
FROM TestTable t
) SELECT #fieldCount = MAX(TildeCount) + 1 FROM TildeCounts;
SELECT t.SOS_ID, a.Element AS [ALLOCATED_PART_NBR], b.Element AS [ALLOCATED_SALES_ITM], c.Element AS [ALLOCATED_QTY]
FROM TestTable t
CROSS APPLY dbo.SplitString(ALLOCATED_PART_NBR, '~', #fieldCount) a
CROSS APPLY dbo.SplitString(ALLOCATED_SALES_ITM, '~', #fieldCount) b
CROSS APPLY dbo.SplitString(ALLOCATED_QTY, '~', #fieldCount) c
WHERE a.ElementID = b.ElementID AND b.ElementID = c.ElementID
What this does is it first gets the maximum number of fields in all the strings (so it can pad out the ones that are shorter). It then selects from the table, CROSS APPYING the function to each column, filtering only for the rows where all the IDs match (line up).
Convert the strings to xml, then select the nth node from each one.
SQL Fiddle Demo
DECLARE #max_field_count int = 6;
SELECT
SOS_ID
,ALLOCATED_PART_NBR = CAST(N'<a>'+REPLACE(ALLOCATED_PART_NBR ,'~','</a><a>')+'</a>' AS XML).query('(a)[sql:column("i")]').value('.','varchar(max)')
,ALLOCATED_SALES_ITM = CAST(N'<a>'+REPLACE(ALLOCATED_SALES_ITM,'~','</a><a>')+'</a>' AS XML).query('(a)[sql:column("i")]').value('.','varchar(max)')
,ALLOCATED_QTY = CAST(N'<a>'+REPLACE(ALLOCATED_QTY ,'~','</a><a>')+'</a>' AS XML).query('(a)[sql:column("i")]').value('.','varchar(max)')
FROM MyTable
CROSS JOIN (SELECT TOP (#max_field_count) ROW_NUMBER() OVER(ORDER BY (SELECT 1)) FROM master.dbo.spt_values) n(i)
I have one old db in which there are two columns which contain comma separated values like this,
SQL FIDDLE LINK for SCHEMA
Now my problem is that I am trying to import those values into another database which is normalized. So instead of comma separated values, I need to convert those values into a tabular format .
So my output should be look like this,
You need to define what those columns mean. In your example are you discarding the original ID column, in which case what does "1,2,3" & "A,B" actually mean?
I'd probably approach this by cursoring through each row and using a split function to convert each field to a table of values.
create FUNCTION dbo.fn_Split1 (#sep nchar(1), #s nvarchar(4000))
RETURNS table
/**************************************************************************************************
* Author: http://stackoverflow.com/questions/314824/
* Description: splits a string into a table of values, with single-char delimiter.
* Example Usage:
select * from dbo.fn_split1(',', '1,2,5,2,,dggsfdsg,456,df,1,2,5,2,,dggsfdsg,456,df,1,2,5,2,,')
**************************************************************************************************/
AS
RETURN (
WITH Pieces(pn, start, stop) AS (
SELECT 1, 1, CHARINDEX(#sep, #s)
UNION ALL
SELECT pn + 1, stop + 1, CHARINDEX(#sep, #s, stop + 1)
FROM Pieces
WHERE stop > 0
)
SELECT pn,
SUBSTRING(#s, start, CASE WHEN stop > 0 THEN stop-start ELSE 4000 END) AS s
FROM Pieces
)
CREATE TABLE #RegionDetail
(
Id int identity(1,1) not null,
RegionId nvarchar(50),
Zone nvarchar(50)
)
INSERT INTO #RegionDetail (RegionId,Zone) values ('1,2,3','A,B')
INSERT INTO #RegionDetail (RegionId,Zone) values ('1,2,3','X,Y')
INSERT INTO #RegionDetail (RegionId,Zone) values ('4,3,5','A,B')
GO
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;
GO
SELECT Id,RegionId,Zone FROM #RegionDetail
select
r.Id,f.items as RegionId,z.items as Zone
from
#RegionDetail r
cross apply [dbo].[Split](r.RegionId,',') f
cross apply [dbo].[Split](r.Zone,',') z
order by Id,RegionId,Zone
In my table, I have a varchar column whereby multi-values are stored. An example of my table:
RecNum | Title | Category
-----------------------------------------
wja-2012-000001 | abcdef | 4,6
wja-2012-000002 | qwerty | 1,3,7
wja-2012-000003 | asdffg |
wja-2012-000004 | zxcvbb | 2,7
wja-2012-000005 | ploiuh | 3,4,12
The values in the Category column points to another table.
How can I return the relevant rows if I want to retrieve the rows with value 1,3,5,6,8 in the Category column?
When I tried using IN, I get the 'Conversion failed when converting the varchar value '1,3,5,6,8' to data type int' error.
Breaking the Categories out into a separate table would be a better design if that's a change you can make... otherwise, you could create a function to split the values into a table of integers like this:
CREATE FUNCTION dbo.Split(#String varchar(8000), #Delimiter char(1))
returns #temptable TABLE (id int)
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(id) values(convert(int, #slice))
set #String = right(#String,len(#String) - #idx)
if len(#String) = 0 break
end
return
end
Then call it from your query:
SELECT ...
FROM ...
WHERE #SomeID IN (SELECT id FROM dbo.Split(Category, ','))
Or if you're looking to provide a list of categories as an input parameter (such as '1,3,5,6,8'), and return all records in your table that contain at least one of these values, you could use a query like this:
SELECT ...
FROM ...
WHERE
EXISTS (
select 1
from dbo.Split(Category, ',') s1
join dbo.Split(#SearchValues, ',') s2 ON s1.id = s2.id
)
you can do like this
declare #var varchar(30); set #var='2,3';
exec('select * from category where Category_Id in ('+#var+')')
Try this solution:
CREATE TABLE test4(RecNum varchar(20),Title varchar(10),Category varchar(15))
INSERT INTO test4
VALUES('wja-2012-000001','abcdef','4,6'),
('wja-2012-000002','qwerty','1,3,7'),
('wja-2012-000003','asdffg',null),
('wja-2012-000004','zxcvbb','2,7'),
('wja-2012-000005','ploiuh','3,4,12')
select * from test4
Declare #str varchar(25) = '1,3,5,6,8'
;WITH CTE as (select RecNum,Title,Category from test4)
,CTE1 as (
select RecNum,Title,RIGHT(#str,LEN(#str)-CHARINDEX(',',#str,1)) as rem from CTE where category like '%'+LEFT(#str,1)+'%'
union all
select c.RecNum,c.Title,RIGHT(c1.rem,LEN(c1.rem)-CHARINDEX(',',c1.rem,1)) as rem from CTE1 c1 inner join CTE c
on c.category like '%'+LEFT(c1.rem,1)+'%' and CHARINDEX(',',c1.rem,1)>0
)
select RecNum,Title from CTE1
As mentioned by others, your table design violates basic database design principles and if there is no way around it, you could normalize the table with little code (example below) and then join away with the other table. Here you go:
Data:
CREATE TABLE data(RecNum varchar(20),Title varchar(10),Category varchar(15))
INSERT INTO data
VALUES('wja-2012-000001','abcdef','4,6'),
('wja-2012-000002','qwerty','1,3,7'),
('wja-2012-000003','asdffg',null),
('wja-2012-000004','zxcvbb','2,7'),
('wja-2012-000005','ploiuh','3,4,12')
This function takes a comma separated string and returns a table:
CREATE FUNCTION listToTable (#list nvarchar(MAX))
RETURNS #tbl TABLE (number int NOT NULL) AS
BEGIN
DECLARE #pos int,
#nextpos int,
#valuelen int
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
RETURN
END
Then, you can do something like this to "normalize" the table:
SELECT *
FROM data m
CROSS APPLY listToTable(m.Category) AS t
where Category is not null
And then use the result of the above query to join with the "other" table. For example (i did not test this query):
select * from otherTable a
join listToTable('1,3,5,6,8') b
on a.Category = b.number
join(
SELECT *
FROM data m
CROSS APPLY listToTable(m.Category) AS t
where Category is not null
) c
on a.category = c.number
I'm working on a report in reporting services that has the user select a number of items from a multivalue list. The query for the report uses the resulting list in a simple
SELECT foo FROM bar WHERE foobar IN (#SelectedItemsFromMultiValueList)
I'm now altering the report and need to iterate over the items in #SelectedItemsFromMultiValueList using a cursor. I've looked around but can't figure out how to do this - made even more difficult by the fact that I'm not sure what to call a list of values used in an IN or even declare one manually (eg. DECLARE #SelectedItemsFromMultiValueList ???)
Does anybody know how to cursor over a multivalue list parameter or how to call something like that in SQL so I can search more effectively for a solution?
Your multi-value list is going to come in to sql as a list of comma separated values (i.e. "31,26,17")
To iterate through these values you need a way to split the values into a table. This is a function I have used, that I believe was originally coded by Jens Suessmeyer:
CREATE FUNCTION [dbo].[ufn_split]
( #Delimiter varchar(5),
#List nvarchar(max)
)
RETURNS #TableOfValues table
( RowID smallint IDENTITY(1,1),
[Value] NVARCHAR(max)
)
AS
BEGIN
DECLARE #LenString int
WHILE len( #List ) > 0
BEGIN
SELECT #LenString =
(CASE charindex( #Delimiter, #List )
WHEN 0 THEN len( #List )
ELSE ( charindex( #Delimiter, #List ) -1 )
END
)
INSERT INTO #TableOfValues
SELECT substring( #List, 1, #LenString )
SELECT #List =
(CASE ( len( #List ) - #LenString )
WHEN 0 THEN ''
ELSE right( #List, len( #List ) - #LenString - 1 )
END
)
END
RETURN
END
So you call this function, passing it #SelectedItemsFromMultiValueList and it will return to you a table of values that you can then do with what you want.
For example:
SELECT * FROM Foo WHERE X IN (SELECT [value] FROM dbo.ufn_split(',', #SelectedItemsFromMultiValueList)
i prefer my set-based solution using a recursive cte
declare #delim varchar(max),
#string varchar(max)
set #delim=','
set #string='1,2,3,4,5,6,7,8,9,10'
;with c as
(
select
CHARINDEX(#delim,#string,1) as Pos,
case when CHARINDEX(#delim,#string,1)>0 then SUBSTRING(#string,1,CHARINDEX(#delim,#string,1)-1) else #string end as Value,
case when CHARINDEX(#delim,#string,1)>0 then SUBSTRING(#string,CHARINDEX(#delim,#string,1)+1,LEN(#string)-CHARINDEX(#delim,#string,1)) else '' end as String
union all
select
CHARINDEX(#delim,String,1) as Pos,
case when CHARINDEX(#delim,String,1)>0 then SUBSTRING(String,1,CHARINDEX(#delim,String,1)-1) else String end as Value,
case when CHARINDEX(#delim,String,1)>0 then SUBSTRING(String,CHARINDEX(#delim,String,1)+1,LEN(String)-CHARINDEX(#delim,String,1)) else '' end as String
from c
where LEN(String)>0
)
select
Value
from c
option (maxrecursion 10000)
you then take whatever your query is and do an inner join with c.