Parse text field and join tables - sql

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.

Related

How to get only Capital letters from given value

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.

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

Convert Comma Delimited String to bigint in SQL Server

I have a varchar string of delimited numbers separated by commas that I want to use in my SQL script but I need to compare with a bigint field in the database. Need to know to convert it:
DECLARE #RegionID varchar(200) = null
SET #RegionID = '853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
SELECT a.ClassAdID, -- 1
a.AdURL, -- 2
a.AdTitle, -- 3
a.ClassAdCatID, -- 4
b.ClassAdCat, -- 5
a.Img1, -- 6
a.AdText, -- 7
a.MemberID, -- 9
a.Viewed, -- 10
c.Domain, -- 11
a.CreateDate -- 12
FROM ClassAd a
INNER JOIN ClassAdCat b ON b.ClassAdCAtID = a.ClassAdCAtID
INNER JOIN Region c ON c.RegionID = a.RegionID
AND a.PostType = 'CPN'
AND DATEDIFF(d, GETDATE(), ExpirationDate) >= 0
AND a.RegionID IN (#RegionID)
AND Viewable = 'Y'
This fails with the following error:
Error converting data type varchar to bigint.
RegionID In the database is a bigint field.. need to convert the varchar to bigint.. any ideas..?
Many thanks in advance,
neojakey
create this function:
CREATE function [dbo].[f_split]
(
#param nvarchar(max),
#delimiter char(1)
)
returns #t table (val nvarchar(max), seq int)
as
begin
set #param += #delimiter
;with a as
(
select cast(1 as bigint) f, charindex(#delimiter, #param) t, 1 seq
union all
select t + 1, charindex(#delimiter, #param, t + 1), seq + 1
from a
where charindex(#delimiter, #param, t + 1) > 0
)
insert #t
select substring(#param, f, t - f), seq from a
option (maxrecursion 0)
return
end
change this part:
AND a.RegionID IN (select val from dbo.f_split(#regionID, ','))
Change this for better overall performance:
AND DATEDIFF(d, 0, GETDATE()) <= ExpirationDate
Your query does not know that those are separate values, you can use dynamic sql for this:
DECLARE #RegionID varchar(200) = null
SET #RegionID = '853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
declare #sql nvarchar(Max)
set #sql = 'SELECT a.ClassAdID, -- 1
a.AdURL, -- 2
a.AdTitle, -- 3
a.ClassAdCatID, -- 4
b.ClassAdCat, -- 5
a.Img1, -- 6
a.AdText, -- 7
a.MemberID, -- 9
a.Viewed, -- 10
c.Domain, -- 11
a.CreateDate -- 12
FROM ClassAd a
INNER JOIN ClassAdCat b ON b.ClassAdCAtID = a.ClassAdCAtID
INNER JOIN Region c ON c.RegionID = a.RegionID
AND a.PostType = ''CPN''
AND DATEDIFF(d, GETDATE(), ExpirationDate) >= 0
AND a.RegionID IN ('+#RegionID+')
AND Viewable = ''Y'''
exec sp_executesql #sql
I use this apporach sometimes and find it very good.
It transfors your comma-separated string into an AUX table (called #ARRAY) and then query the main table based on the AUX table:
declare #RegionID varchar(50)
SET #RegionID = '853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
declare #S varchar(20)
if LEN(#RegionID) > 0 SET #RegionID = #RegionID + ','
CREATE TABLE #ARRAY(region_ID VARCHAR(20))
WHILE LEN(#RegionID) > 0 BEGIN
SELECT #S = LTRIM(SUBSTRING(#RegionID, 1, CHARINDEX(',', #RegionID) - 1))
INSERT INTO #ARRAY (region_ID) VALUES (#S)
SELECT #RegionID = SUBSTRING(#RegionID, CHARINDEX(',', #RegionID) + 1, LEN(#RegionID))
END
select * from your_table
where regionID IN (select region_ID from #ARRAY)
It avoids you from ahving to concatenate the query string and then use EXEC to execute it, which I dont think it is a very good approach.
if you need to run the code twice you will need to drop the temp table
I think the answer should be kept simple.
Try using CHARINDEX like this:
DECLARE #RegionID VARCHAR(200) = NULL
SET #RegionID =
'853,834,16,467,841,460,495,44,859,457,437,836,864,434,86,838,458,472,832,433,142,154,159,839,831,469,442,275,840,299,446,220,300,225,227,447,301,450,230,837,441,835,302,477,855,411,395,279,303'
SELECT 1
WHERE Charindex('834', #RegionID) > 0
SELECT 1
WHERE Charindex('999', #RegionID) > 0
When CHARINDEX finds the value in the large string variable, it will return it's position, otherwise it return 0.
Use this as a search tool.
The easiest way to change this query is to replace the IN function with a string function. Here is what I consider the safest approach using LIKE (which is portable among databases):
AND ','+#RegionID+',' like '%,'+cast(a.RegionID as varchar(255))+',%'
Or CHARINDEX:
AND charindex(','+cast(a.RegionID as varchar(255))+',', ','+#RegionID+',') > 0
However, if you are explicitly putting the list in your code, why not use a temporary table?
declare #RegionIds table (RegionId int);
insert into #RegionIds
select 853 union all
select 834 union all
. . .
select 303
Then you can use the table in the IN clause:
AND a.RegionId in (select RegionId from #RegionIds)
or in a JOIN clause.
I like Diego's answer some, but I think my modification is a little better because you are declaring a table variable and not creating an actual table. I know the "in" statement can be a little slow, so I did an inner join since I needed some info from the Company table anyway.
declare #companyIdList varchar(1000)
set #companyIdList = '1,2,3'
if LEN(#companyIdList) > 0 SET #companyIdList = #companyIdList + ','
declare #CompanyIds TABLE (CompanyId bigint)
declare #S varchar(20)
WHILE LEN(#companyIdList) > 0 BEGIN
SELECT #S = LTRIM(SUBSTRING(#companyIdList, 1, CHARINDEX(',', #companyIdList) - 1))
INSERT INTO #CompanyIds (CompanyId) VALUES (#S)
SELECT #companyIdList = SUBSTRING(#companyIdList, CHARINDEX(',', #companyIdList) + 1, LEN(#companyIdList))
END
select d.Id, d.Name, c.Id, c.Name
from [Division] d
inner join [Company] c on d.CompanyId = c.Id
inner join #CompanyIds cids on c.Id = cids.CompanyId

SQL Server - Compare Varchar values using IN

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

Split string and return data in multiple columns

First of all many thanks to the site creator and most importantly helping guru's on this site.
I have the same problem splitting string from a field and displaying it in multiple columns example my table has got three columns
dbo.tests
Fname ID wTest Loc
ABC 1 "XYZ,PTO,LKMD,HGGFFD," R1
BCE 2 "PTO,XYZ,LKMD,," R1
LKJ 3 "XYZ" R3
JKL 4 "XYZ,PTO,LKMD,HGGFFD,PKL" R2
The output for the select statement should display the data as follows: (Dynamically generate number of columns based on maximum columns required from wTest string and fill the empty columsn with null or some value.
Returns:
Fname ID Loc wTest wTest1 wTest2,wTest3,Wtest4...
ABC 1 R1 XYZ PTO LKMD HGGFFD Null
BCE 2 R1 PTO XYZ LKMD Null Null
LKJ 3 R3 XYZ Null Null Null Null
JKL 4 R2 XYZ PTO LKMD HGGFFD PKL
Two close function I came accross are as follows:
CREATE FUNCTION dbo.Split (#sep char(1), #s varchar(512))
RETURNS table 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 512 END) AS s FROM Pieces )
with testTable AS
( SELECT 1 AS Id, N'how now brown cow' AS txt
UNION ALL
SELECT 2, N'she sells sea shells upon the sea shore' UNION ALL
SELECT 3, N'red lorry yellow lorry' UNION ALL
SELECT 4, N'the quick brown fox jumped over the lazy dog' )
SELECT display_term, COUNT(*) As Cnt
FROM testTable CROSS APPLY sys.dm_fts_parser('"' + txt + '"', 1033, 0,0)
GROUP BY display_term
HAVING COUNT(*) > 1 ORDER BY Cnt DESC
Any help in this regard is highly appreciated.
Zain...
zainali2006#hotmail.co.uk
Someone suggested this, but I am having difficulties applying for my purpose....
Returns #Tbl_IDs
Table (Id int identity(1,1),
Data Varchar(500)) As
Begin
--Remove the leading delimiter if any
while (substring(#IDs,1,1) =#Delimiter)
set #IDs = substring(#IDs, 2,len(#IDs)-1)
-- Append comma
--Set #IDs = #IDs + #Delimiter
set #IDs = REPLACE(RTRIM(LTRIM(REPLACE(#IDs,#Delimiter,' '))),' ',#Delimiter)
-- Indexes to keep the position of searching
Declare #Pos1 Int
Declare #pos2 Int
Declare #RowNum Int
-- Start from first character
Set #Pos1=1
Set #Pos2=1
While #Pos1>0
Begin
Set #Pos1 = CharIndex(#Delimiter,#IDs,#Pos1)
Insert #Tbl_IDs Values (Substring(#IDs,#Pos2,#Pos1-#Pos2))
-- Go to next non comma character
Set #Pos2=#Pos1+1
-- Search from the next charcater
Set #Pos1 = #Pos1+1
End
Return
End
Another one I came across quite interesting and simple but not sure how to use in my select statement:
DECLARE #NextString NVARCHAR(40)
DECLARE #Pos INT
DECLARE #NextPos INT
DECLARE #String NVARCHAR(40)
DECLARE #Delimiter NVARCHAR(40)
SET #String ='SQL,TUTORIALS,,TCF'
SET #Delimiter = ','
SET #String = #String + #Delimiter
SET #Pos = charindex(#Delimiter,#String)
WHILE (#pos <> 0)
BEGIN
SET #NextString = substring(#String,1,#Pos - 1)
SELECT #NextString -- Show Results
SET #String = substring(#String,#pos+1,len(#String))
SET #pos = charindex(#Delimiter,#String)
END