SQL Server - Compare Varchar values using IN - sql

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

Related

T-SQL query: find most frequent pair of values

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

Join table using columns which contains other table's id's separated by commas

I have FACULTY table which contain the column with other table's id's separated by commas. I want to join those with respective table.
faculty table:
id | name | course_id | subject_id
a | smith | 2,3 | 1,2
course table:
id | name
1 | bcom
2 | mcom
3 | bba
subject table:
id | name
1 | account
2 | state
3 | economics
I want to get result from these table like..
faculty.id, faculty.name, course.name(using faculty.course_id), subject.name(using faculty.subject_id)
I have tried a lot of queries and also finds from Google but it didn't gave me proper result.
I do not think the performance will be too nice but worths trying. This solution would work in SQL SERVER:
SELECT *
FROM faculty F
JOIN course C
ON ','+F.course_id+',' LIKE '%,'+CONVERT(VARCHAR,C.ID) +',%'
JOIN subject S
ON ','+F.subject_id_id+',' LIKE '%,'+CONVERT(VARCHAR,S.ID) +',%'
Based on Albin Sunnanbo's comment i would also sugget you add some many too many tables:
fcourses
facultyId
courseId
and
fsubjects
facultyId
subjectId
That way you could do a proper join :
SELECT *
FROM faculty F
JOIN fcourses FC
ON F.Id = FC.facultyId
JOIN course C
ON FC.courseId = C.ID
JOIN fsubjects FS
ON F.Id = FS.facultyId
JOIN subject S
ON FS.courseId = S.ID
You can do the following query
select * from faculty F
JOIN course C
on CHARINDEX((','+CAST(c.id as varchar(10))+','), (','+f.courseid+',')) > 0
JOIN subject s
on CHARINDEX((','+CAST(s.id as varchar(10))+','), (','+f.subjectid+',')) > 0
I've done something similar like this:
select f.id, f.lname, f.fname, U.useridlist
from TABLE1 F, TABLE2 U
where ',' || U.useridlist || ',' like '%,' || f.id || ',%'
If you can create a 'string to int table' function, I would look at the following:
Create the function
CREATE FUNCTION [dbo].[udf_ConvertIntListToTable] (#list varchar(MAX))
RETURNS #tbl TABLE (val int) AS
BEGIN
DECLARE #ix int,
#pos int,
#str varchar(MAX),
#num int
SET #pos = 1
SET #ix = 1
WHILE #ix > 0
BEGIN
SET #ix = charindex(',', #list, #pos)
IF #ix > 0
SET #str = substring(#list, #pos, #ix - #pos)
ELSE
SET #str = substring(#list, #pos, len(#list))
SET #str = ltrim(rtrim(#str))
IF #str LIKE '%[0-9]%' AND
(#str NOT LIKE '%[^0-9]%' OR
#str LIKE '[-+]%' AND
substring(#str, 2, len(#str)) NOT LIKE '[-+]%[^0-9]%')
BEGIN
SET #num = convert(int, #str)
INSERT #tbl (val) VALUES(#num)
END
SET #pos = #ix + 1
END
RETURN
END
Then query using CROSS APPLY
declare #FacultyTable table(id int PRIMARY KEY, name nvarchar(50), course_id varchar(50))
declare #CourseTable table(id int PRIMARY KEY, name nvarchar(50))
insert into #FacultyTable values(1, 'Peter Sagal', '11,22')
insert into #FacultyTable values(2, 'Carl Kasell', '22,33')
insert into #CourseTable values(11,'News')
insert into #CourseTable values(22,'News')
insert into #CourseTable values(33,'News')
insert into #CourseTable values(44,'News')
select *
from #FacultyTable f
CROSS APPLY(
SELECT *
FROM #CourseTable c
WHERE
c.id IN (SELECT * FROM dbo.udf_ConvertIntListToTable(f.course_id))
) tCourses

Parse text field and join tables

I have 2 tables.
Table Heroes - 2 records
Name NVARCHAR(50)
PowerIds NVARCHAR(50)
Name PowerIds
'Hulk' '1,3'
'Reed Richards' '2'
Table Powers - 3 records
PowerId INT
PowerDescr NVARCHAR(50)
PowerId PowerDescr
1 'Strength'
2 'Intelligence'
3 'Durability'
What would be the smartest way to achieve this in a SELECT:
Name Powers
'Hulk' 'Strength, Durability'
'Reed Richards' 'Intelligence'
I cannot change the table structure, since this is a third party product.
The smartest way would be to normalize your table. Change the Heroes table to
Name PowerId
'Hulk' 1
'Hulk' 3
'Reed Richards' 2
or remove the power from the Heroes table and add another table that holds only the reference to a hero and the powers like this
HeroID PowerID
1 1
1 3
2 2
Never store multiple data in one column!
Try This:
SELECT Name,
STUFF(
(SELECT ',' + CAST(P.PowerDescr as VARCHAR(MAX))
FROM fn_ParseCsvString(H1.PowerIds, ',') H2
INNER JOIN Powers P ON P.PowerId = H2.ParsedString
FOR XML path('')),1,1,''
) AS Strength
FROM Heroes H1
Function:
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[fn_ParseCsvString]
(
#csvString VARCHAR(MAX),
#delimiter VARCHAR(MAX)
)
RETURNS #parsedStringTable TABLE (ParsedString VARCHAR(MAX))
AS
BEGIN
DECLARE #startIndex INT, #targetedIndex INT
SELECT
#startIndex = 1
WHILE #startIndex <= LEN(#CSVString)
BEGIN
SELECT
#targetedIndex = charindex(#Delimiter, #CSVString, #startIndex)
IF #targetedIndex = 0
BEGIN
SELECT
#targetedIndex = len(#CSVString) + 1
END
INSERT #parsedStringTable
SELECT
SUBSTRING(#CSVString, #startIndex, #targetedIndex - #startIndex)
SELECT #startIndex = #targetedIndex + LEN(#Delimiter)
END
RETURN
END
GO
Here you can find a SQL Fiddle example.

Sort columns based on the order inside SQL in function

i have a query like the one below and i want records to be returned in the same order as the id's specified inside the 'in' function.
SELECT * FROM table 1 where id in(12,6,4,3,13)
Can i do this using sql alone or do i need to write my own sorting function.
create a table with your ID's, then join to table, ordering by another identity column.
Presumably if you have 10k id's they will not be manually entered, so prehaps able to build sort/join table in a different way. SHould also be more efficient than using large in
create table #tempID(idSort int identity(1,1), id int)
insert into #tempID(id)
select 12 union all
select 6 union all
select 4 union all
select 3 union all
select 13
select * from table t1
inner join #tempID t2
on t1.id = t2.id
order by t2.idSort
To create sort table dynamically, you need this function (or similar):
create FUNCTION [dbo].[comma_sep_var_intSort] (#list nvarchar(MAX))
RETURNS #tbl TABLE (idSort int identity(1,1), id 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 (id)
VALUES (cast(substring(#list, #pos + 1, #valuelen) as int))
SELECT #pos = #nextpos
END
RETURN
END
Then join like this:
declare #idList varchar(max)
set #idLIst = '12,6,4,3,13'
select * from table t1
inner join [dbo].[comma_sep_var_int](#idList) t2
on t1.id = t2.id
order by t2.idSort
you can use CASE to custom sort the records,
ORDER BY CASE WHEN ID = 12 THEN 1
WHEN ID = 6 THEN 2
WHEN ID = 4 THEN 3
WHEN ID = 3 THEN 4
WHEN ID = 13 THEN 5
ELSE 6
END, ID
SQLFiddle Demo

SQL - SUBSTRING and CHARINDEX [duplicate]

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.