This question already has answers here:
Closed 10 years ago.
Possible Duplicate:
T-SQL: Opposite to string concatenation - how to split string into multiple records
Splitting variable length delimited string across multiple rows (SQL)
I have a database table that contains column data like this:
Data (field name)
1111,44,666,77
22,55,76,54
32,31,56
I realise this is a very poor design because it is not normalised (I didn't design it - I inherited it). Is there a query that will return the data like this:
1111
44
666
77
22
55
76
54
32
31
56
I am use to using CHARINDEX and SUBSTRING, but I cannot think of a way of doing this as the number of elements in each cell (delimited by a comma) is unknown.
You can use CTE to split the data:
;with cte (DataItem, Data) as
(
select cast(left(Data, charindex(',',Data+',')-1) as varchar(50)) DataItem,
stuff(Data, 1, charindex(',',Data+','), '') Data
from yourtable
union all
select cast(left(Data, charindex(',',Data+',')-1) as varchar(50)) DataItem,
stuff(Data, 1, charindex(',',Data+','), '') Data
from cte
where Data > ''
)
select DataItem
from cte
See SQL Fiddle with Demo
Result:
| DATAITEM |
------------
| 1111 |
| 22 |
| 32 |
| 31 |
| 56 |
| 55 |
| 76 |
| 54 |
| 44 |
| 666 |
| 77 |
Or you can create a split function:
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;
Which you can use when you query and this will produce the same result:
select s.items declaration
from yourtable t1
outer apply dbo.split(t1.data, ',') s
I created a table called [dbo].[stack] and filled it with the data you provided and this script produced what you needed. There may be a more efficient way of doing this but this works exactly how you requested.
BEGIN
DECLARE #tmp TABLE (data VARCHAR(20))
DECLARE #tmp2 TABLE (data VARCHAR(20))
--Insert all fields from your table
INSERT INTO #tmp (data)
SELECT [data]
FROM [dbo].[stack] -- your table name here
--Loop through all the records in temp table
WHILE EXISTS (SELECT 1
FROM #tmp)
BEGIN
DECLARE #data VARCHAR(100) --Variable to chop up
DECLARE #data1 VARCHAR(100) -- Untouched variable to delete from tmp table
SET #data = (SELECT TOP 1 [data]
FROM #tmp)
SET #data1 = (SELECT TOP 1 [data]
FROM #tmp)
--Loop through variable to get individual value
WHILE PATINDEX('%,%',#data) > 0
BEGIN
INSERT INTO #tmp2
SELECT SUBSTRING(#data,1,PATINDEX('%,%',#data)-1);
SET #data = SUBSTRING(#data,PATINDEX('%,%',#data)+1,LEN(#data))
IF PATINDEX('%,%',#data) = 0
INSERT INTO #tmp2
SELECT #data
END
DELETE FROM #tmp
WHERE [data] = #data1
END
SELECT * FROM #tmp2
END
Not talking about performance, you can concatenate the data in a single column and then split it.
Concatenate data: http://sqlfiddle.com/#!6/487a4/3
Split it: T-SQL: Opposite to string concatenation - how to split string into multiple records
Take a look at this article referenced in a similar question:
http://www.codeproject.com/Articles/7938/SQL-User-Defined-Function-to-Parse-a-Delimited-Str
If you create the function that they have in that article, you can call it using:
select * from dbo.fn_ParseText2Table('100|120|130.56|Yes|Cobalt Blue','|')
SELECT REPLACE(field_name, ',', ' ') from table
EDIT: Never mind this answer as you changed your question.
Related
I have a Table Family like the following
Family_Name | Family_Members_Age
Johnson | 45,60,56
Ken | 78,67,40
David | 40
Here is a proc I have
CREATE PROCEDURE getFamilyRowsByAge #Age nvarchar(30)
AS
SELECT *
FROM Family
WHERE Family_members_age LIKE FILL_IN -- need to get this fill_in dynamically
The #Age param is supplied with comma separated String like 45,67.
FILL_IN would be something like this for input String of "45,67" LIKE '%45%' OR LIKE '%67%'. I want this to be dynamically created by splitting input String for comma and joining with LIKE OR. Is there a way in MSSQL to do this?
Output:
Johnson | 45,60,56
Ken | 78,67,40
Here is another input and output:
input : 40, 67, 69
Output:
Johnson | 45,60,56
Ken | 78,67,40
David | 40
Based on those comments, try this:
USE tempdb;
GO
DROP TABLE IF EXISTS dbo.Family;
GO
CREATE TABLE dbo.Family
(
FamilyID int IDENTITY(1,1)
CONSTRAINT PK_dbo_Family PRIMARY KEY,
Family_Name varchar(100),
Family_Members_Age varchar(max)
);
GO
INSERT dbo.Family (Family_Name, Family_Members_Age)
VALUES ('Johnson', '45,60,56'),
('Ken', '78,67,40'),
('David', '40');
GO
CREATE PROCEDURE dbo.GetFamilyRowsByAge
#RequiredAges varchar(100)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
WITH FamilyAges
AS
(
SELECT f.Family_Name, fma.Age
FROM dbo.Family AS f
CROSS APPLY (SELECT value AS Age FROM STRING_SPLIT(f.Family_Members_Age,',')) AS fma
)
SELECT fa.Family_Name, fa.Age
FROM FamilyAges AS fa
WHERE fa.Age IN (SELECT value FROM STRING_SPLIT(#RequiredAges, ','));
END;
GO
EXEC dbo.GetFamilyRowsByAge '45,67,40';
GO
You can achieve it by this simple way, Live demo here
strSplit
CREATE FUNCTION [dbo].[strSplit] ( #string nvarchar( MAX), #splitter CHAR( 1) )
RETURNS #res TABLE (id INT PRIMARY KEY, rank INT, val nvarchar( MAX) )
AS
BEGIN
IF SUBSTRING ( #string, len ( #string), 1)<>#splitter
SET #string= #string+#splitter
DECLARE #start INT, #word nvarchar(MAX), #charindex INT, #i INT
SET #i=1
SET #start=1
SET #charindex= CHARINDEX( #splitter, #string, #start)
WHILE (#charindex <> 0)BEGIN
SET #word= SUBSTRING( #string, #start, #charindex - #start)
SET #start= #charindex +1
SET #charindex= CHARINDEX( #splitter, #string, #start)
INSERT INTO #res VALUES ( #start, #i, #word)
SET #i=#i+1
END
RETURN
END
ContainString
CREATE FUNCTION [dbo].[ContainString] (#string1 nvarchar( MAX), #string2 nvarchar( MAX))
RETURNS BIT
AS
BEGIN
IF(EXISTS(SELECT TOP 1 a.val From strSplit(#string1, ',') a
INNER JOIN strSplit(#string2, ',') b on a.val = b.val ))
BEGIN
RETURN 1
END
RETURN 0
END
Select result
SELECT * FROM Family WHERE [dbo].ContainString(Family_Members_Age, '45,67') = 1
SELECT * FROM Family WHERE [dbo].ContainString(Family_Members_Age, '40,67,69') = 1
The whole query within the proc will need to be dynamic. Then you can use something like STRING_SPLIT (2016 or later) to pull apart the comma-delimited value and build the string.
However, I'm unconvinced that's what you need. If you use LIKE, how are you going to avoid %9% matching a value like 91 ?
You might want to consider normalizing the table correctly instead. Why are there 3 ages within a single column? Why not just store it as 3 rows?
Probably you need something like this .
SELECT 'Johnson'Family_Name,'45,60,56' Family_Members_Age
into #YourTable
union all select 'Ken','78,67,40'
union all select 'David','40'
DECLARE #WhereQuery varchar(250)='40, 67, 69', #Query nvarchar(250)
select #Query = 'select * from #YourTable'
SET #WhereQuery = ' WHERE Family_Members_Age LIKE ''%'+ REPLACE (#WhereQuery,',','%'' OR Family_Members_Age LIKE ''%') + '%'''
SET #Query = #Query + #WhereQuery
EXEC SP_EXECUTESQL #Query
I have a table it contains ID, Description and code columns. I need to fill code column using description column. Sample Description is "Investigations and Remedial Measures" so my code should be "IRM".
Note: Is there any words like "and/for/to/in" avoid it
This code may help you..
declare #input as varchar(1000) -- Choose the appropriate size
declare #output as varchar(1000) -- Choose the appropriate size
select #input = 'Investigations and Remedial Measures', #output = ''
declare #i int
select #i = 0
while #i < len(#input)
begin
select #i = #i + 1
select #output = #output + case when unicode(substring(#input, #i, 1))between 65
and 90 then substring(#input, #i, 1) else '' end
end
SELECT #output
Personally I would do this with an inline table-valued function
On SQL Server 2017 or better, or Azure SQL Database:
CREATE OR ALTER FUNCTION dbo.ExtractUpperCase(#s nvarchar(4000))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH s(s) AS (SELECT 1 UNION ALL SELECT s+1 FROM s WHERE s < LEN(#s))
SELECT TOP (3) value = STRING_AGG(SUBSTRING(#s,s,1),'')
WITHIN GROUP (ORDER BY s.s)
FROM s WHERE ASCII(SUBSTRING(#s,s,1)) BETWEEN 65 AND 90
);
GO
On SQL Server 2016 or older:
CREATE FUNCTION dbo.ExtractUpperCase(#s nvarchar(4000))
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH s(s) AS (SELECT 1 UNION ALL SELECT s+1 FROM s WHERE s < LEN(#s))
SELECT value = (SELECT TOP (3) v = SUBSTRING(#s,s,1) FROM s
WHERE ASCII(SUBSTRING(#s,s,1)) BETWEEN 65 AND 90
ORDER BY s.s FOR XML PATH(''),
TYPE).value(N'./text()[1]',N'nvarchar(4000)')
);
GO
In either case:
CREATE TABLE #x(id int, name nvarchar(4000));
INSERT #x(id, name) VALUES
(1, N'Belo Horizonte Orange'),
(2, N'São Paulo Lala'),
(3, N'Ferraz de Vasconcelos Toranto');
SELECT id, f.value FROM #x AS x
CROSS APPLY dbo.ExtractUpperCase(x.name) AS f
ORDER BY id OPTION (MAXRECURSION 4000);
Results:
id name
---- ----
1 BHO
2 SPL
3 SVT
The OPTION (MAXRECURSION 4000) is only necessary if your strings can be longer than 100 characters.
Sample table
Record Number | Filter | Filters_Applied
----------------------------------------------
1 | yes | red, blue
2 | yes | green
3 | no |
4 | yes | red, red, blue
Is it possible to query all records where there are duplicate string values? For example, how could I query to pull record 4 where the string "red" appeared twice? Except in the table that I am dealing with, there are far more string values that can populate in the "filters_applied" column.
CLARIFICATION I am working out of Periscope and pulling data using SQL.
I assume that you have to check that in the logical page.
You can query the table with like '%red%'.
select Filters_Applied from table where Filters_Applied like '%red%';
You will get the data which has red at least one. Then, doing some string analysis in logic page.
In php, You can use the substr_count function to determine the number of occurrences of the string.
//the loop to load db query
while(){
$number= substr_count("Filters_Applied",red);
if($number>1){
echo "this".$Filters_Applied.">1"
}
}
for SQL-SERVER or other versions which can run these functions
Apply this logic
declare #val varchar(100) = 'yellow,blue,white,green'
DECLARE #find varchar(100) = 'green'
select #val = replace(#val,' ','') -- remove spaces
select #val;
select (len(#val)-len(replace(#val,#find,'')))/len(#find) [recurrence]
Create this Function which will parse string into rows and write query as given below. This will works for SQL Server.
CREATE FUNCTION [dbo].[StrParse]
(#delimiter CHAR(1),
#csv NTEXT)
RETURNS #tbl TABLE(Keys NVARCHAR(255))
AS
BEGIN
DECLARE #len INT
SET #len = Datalength(#csv)
IF NOT #len > 0
RETURN
DECLARE #l INT
DECLARE #m INT
SET #l = 0
SET #m = 0
DECLARE #s VARCHAR(255)
DECLARE #slen INT
WHILE #l <= #len
BEGIN
SET #l = #m + 1--current position
SET #m = Charindex(#delimiter,Substring(#csv,#l + 1,255))--next delimiter or 0
IF #m <> 0
SET #m = #m + #l
--insert #tbl(keys) values(#m)
SELECT #slen = CASE
WHEN #m = 0 THEN 255 --returns the remainder of the string
ELSE #m - #l
END --returns number of characters up to next delimiter
IF #slen > 0
BEGIN
SET #s = Substring(#csv,#l,#slen)
INSERT INTO #tbl
(Keys)
SELECT #s
END
SELECT #l = CASE
WHEN #m = 0 THEN #len + 1 --breaks the loop
ELSE #m + 1
END --sets current position to 1 after next delimiter
END
RETURN
END
GO
CREATE TABLE Table1# (RecordNumber int, [Filter] varchar(5), Filters_Applied varchar(100))
GO
INSERT INTO Table1# VALUES
(1,'yes','red, blue')
,(2,'yes','green')
,(3,'no ','')
,(4,'yes','red, red, blue')
GO
--This query will return what you are expecting
SELECT t.RecordNumber,[Filter],Filters_Applied,ltrim(rtrim(keys)), count(*)NumberOfRows
FROM Table1# t
CROSS APPLY dbo.StrParse (',', t.Filters_Applied)
GROUP BY t.RecordNumber,[Filter],Filters_Applied,ltrim(rtrim(keys)) HAVING count(*) >1
You didn't state your DBMS, but in Postgres this isn't that complicated:
select st.*
from sample_table st
join lateral (
select count(*) <> count(distinct trim(item)) as has_duplicates
from unnest(string_to_array(filters_applied,',')) as t(item)
) x on true
where x.has_duplicates;
Online example: http://rextester.com/TJUGJ44586
With the exception of string_to_array() the above is actually standard SQL
Customer Id. Line# Code
234 1 40
234 2 25,40
234 3 12,40,52
234 4 14
327 1 34
327 2 40,56
Need to get the count of the number 40 in each customer Id
So result should be
Customer id. Count
234 3
327 1
Can you please suggest on how to write the query for this
You can get this using like:
select customerid,
sum(case when ',' + code + ',' like '%,40,%' then 1 else 0 end)
from table t
group by customerid;
Your ned to do this suggests a flaw in your data structure. You are storing lists of numbers in a string -- that is a bad idea. After all, you shouldn't store numbers as characters.
Instead, you should have a junction/association table with one row per customerid and code. This would make a query like this easier to write and more efficient.
You will first have to create a Split function.
CREATE FUNCTION dbo.Split(#String nvarchar(4000), #Delimiter char(1))
RETURNS #Results TABLE (Items nvarchar(4000))
AS
BEGIN
DECLARE #INDEX INT
DECLARE #SLICE nvarchar(4000)
-- HAVE TO SET TO 1 SO IT DOESNT EQUAL Z
-- ERO FIRST TIME IN LOOP
SELECT #INDEX = 1
WHILE #INDEX !=0
BEGIN
-- GET THE INDEX OF THE FIRST OCCURENCE OF THE SPLIT CHARACTER
SELECT #INDEX = CHARINDEX(#Delimiter,#STRING)
-- NOW PUSH EVERYTHING TO THE LEFT OF IT INTO THE SLICE VARIABLE
IF #INDEX !=0
SELECT #SLICE = LEFT(#STRING,#INDEX - 1)
ELSE
SELECT #SLICE = #STRING
-- PUT THE ITEM INTO THE RESULTS SET
INSERT INTO #Results(Items) VALUES(#SLICE)
-- CHOP THE ITEM REMOVED OFF THE MAIN STRING
SELECT #STRING = RIGHT(#STRING,LEN(#STRING) - #INDEX)
-- BREAK OUT IF WE ARE DONE
IF LEN(#STRING) = 0 BREAK
END
RETURN
END
Now use the below query:
SELECT CustID,
Count(*)
FROM TableName
WHERE 40 IN (SELECT *
FROM Split(Code,','))
GROUP BY CustID
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