Reassemble concatenated list with values from lookup table - sql-server-2008

I have 2 tables, T1 and T2. I want to join these 2 tables and return only 2 rows of data, replacing the integers in Item with their lookup values from T2.
Table T1
Item Date
------ ---------
1;4;5; 3/13/2013
1;2;3; 3/13/2013
Table T2
ID Desc
---- ------
1 Tree
2 Grass
3 Sand
4 Water
5 Bridge
Expected results:
Item Date
------------------ ---------
Tree;Water;Bridge; 3/13/2013
Tree;Grass;Sand; 3/13/2013

First, create a Split function which returns an integer and an order-preserving sequence number. Here is one example:
ALTER FUNCTION dbo.SplitInts
(
#List VARCHAR(MAX),
#Delimiter VARCHAR(32)
)
RETURNS TABLE
AS
RETURN
(
SELECT rn = ROW_NUMBER() OVER (ORDER BY Number),
Item = CONVERT(INT, Item)
FROM (SELECT Number, Item = LTRIM(RTRIM(SUBSTRING(#List, Number,
CHARINDEX(#Delimiter, #List + #Delimiter, Number) - Number)))
FROM (SELECT ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_objects) AS n(Number)
WHERE Number <= CONVERT(INT, LEN(#List))
AND SUBSTRING(#Delimiter + #List, Number, 1) = #Delimiter
) AS y
);
GO
Then the following query does what you're after:
DECLARE #t1 TABLE
(
Item VARCHAR(MAX),
[Date] DATE -- terrible column name!
);
INSERT #t1 VALUES('1;4;5;','20130313'),('1;2;3;','20130313');
-- please use unambiguous date formats!
DECLARE #t2 TABLE
(
ID INT, -- another bad column name - what kind of ID?
[Desc] VARCHAR(255) -- another bad column name, this is a keyword!
);
INSERT #t2 VALUES(1,'Tree'),(2,'Grass'),
(3,'Sand'),(4,'Water'),(5,'Bridge');
;WITH x AS
(
SELECT t1.Item, Date, t2ID = i.Item, i.rn, n = t2.[Desc]
FROM #t1 AS t1 CROSS APPLY dbo.SplitInts(t1.Item, ';') AS i
INNER JOIN #t2 AS t2 ON i.Item = t2.ID
)
SELECT DISTINCT Item = (
SELECT n + ';' FROM x AS x2
WHERE x.Item = x2.Item
ORDER BY x2.rn FOR XML PATH,
TYPE).value(N'./type()[1]', N'varchar(max)'), [Date]
FROM x;
Strongly recommend you research normalization. A semi-colon-separated list is a terrible way to cram together independent values.

Related

How to repeat a row N times based on a value within the row

I have a csv file containing three columns, class,malecount and femalecount as an input table.
My output should contain two columns named Class and Gender.
The malecount and femalecount values indicates how many times a row should be repeated. i.e. for Class = A and malecount=2, the row (A,M) should appear twice, and for Class = C and femalecount=3, the row (C,F) should appear three times. Check the following image to see the full output.
enter image description here
DDL & DML for the table:
create table mytable (class text, malecount int, femalecount int);
insert into mytable (class, malecount, femalecount) values
( 'A',2,1),
('B',3,1),
('C',0,3),
('D',2,4);
WITH RECURSIVE
-- define maximal amount of rows per class per gender to be generated
cte1 AS ( SELECT MAX(GREATEST(malecount, femalecount)) max_count
FROM test),
-- generate natural numbers till max. amount found above
cte2 AS ( SELECT 1 num
UNION ALL
SELECT num+1
FROM cte1
CROSS JOIN cte2
WHERE cte2.num <= cte1.max_count)
-- generate rows for male
SELECT test.class, 'm' gender
FROM test
JOIN cte2 ON cte2.num <= test.malecount
UNION ALL
-- generate rows for female
SELECT test.class, 'f'
FROM test
JOIN cte2 ON cte2.num <= test.femalecount
-- final sorting
ORDER BY gender DESC, class
https://dbfiddle.uk/?rdbms=mysql_8.0&fiddle=694dbb214e4c0cd5524800c56a02dc65
I used the LPAD function and then trimmed off the last comma
from there I had a comma delimited string like M,M,M and F,F
then I used json_table to extract the M,M,M into three rows and the F,F into two rows etc.
here is the fiddle https://www.db-fiddle.com/f/jEXes6AttKvc9GKx1mKY2/1
Schema (MySQL v8.0)
create table mytable (class text, malecount int, femalecount int);
insert into mytable (class, malecount, femalecount) values
( 'A',2,1),
('B',3,1),
('C',0,3),
('D',2,4);
Query #1
with t as (select class,
LPAD(' ', malecount * 2 + 1, 'M,') malecount,
LPAD(' ', femalecount * 2 + 1, 'F,') femalecount
from mytable),
t2 as(
select class,
LEFT(malecount,length(malecount)-1) malecount,
LEFT(femalecount,length(malecount)-1) femalecount
from t)
select t2.class, j.name
from t2
join json_table(
replace(json_array(t2.malecount), ',', '","'),
'$[*]' columns (name varchar(50) path '$')
) j where j.name = 'M'
union all
select t2.class, k.name
from t2
join json_table(
replace(json_array(t2.femalecount), ',', '","'),
'$[*]' columns (name varchar(50) path '$')
) k where k.name = 'F';
class
name
A
M
A
M
B
M
B
M
B
M
D
M
D
M
A
F
B
F
D
F
D
F
View on DB Fiddle
You can use Recursive CTE as the following:
with recursive cte as
(
select *,0 as repeats from
(select class, malecount as cnt, 'M' as Gender from Tbl
union
select class, femalecount as cnt, 'F' as Gender from Tbl
) D
union all
select class,cnt,Gender, repeats+1 from cte
where repeats<cnt-1
)
select class, gender from cte
where cnt>0
order by gender desc,class;
See a demo from db-fiddle.
You can use the function,the expression:
CASE input_expression
WHEN when_expression THEN
result_expression [...n ] [
ELSE
else_result_expression

Store values in different variables in SQL, separated by (Comma) ","

I need to separate values and store them in different variables in SQL,
for example
a='3100,3101,3102,....'
And the output should be
x=3100
y=3101
z=3102
.
.
.
create function [dbo].[udf_splitstring] (#tokens varchar(max),
#delimiter varchar(5))
returns #split table (
token varchar(200) not null )
as
begin
declare #list xml
select #list = cast('<a>'
+ replace(#tokens, #delimiter, '</a><a>')
+ '</a>' as xml)
insert into #split
(token)
select ltrim(t.value('.', 'varchar(200)')) as data
from #list.nodes('/a') as x(t)
return
end
GO
declare #cad varchar(100)='3100,3101,3102'
select *,ROW_NUMBER() over (order by token ) as rn from udf_splitstring(#cad,',')
token rn
3100 1
3101 2
3102 3
The results of the Parse TVF can easily be incorporated into a JOIN, or an IN
Declare #a varchar(max)='3100,3101,3102'
Select * from [dbo].[udf-Str-Parse](#a,',')
Returns
RetSeq RetVal
1 3100
2 3101
3 3102
The UDF if needed (much faster than recursive, loops, and xml)
CREATE FUNCTION [dbo].[udf-Str-Parse] (#String varchar(max),#Delimiter varchar(25))
Returns Table
As
Return (
with cte1(N) As (Select 1 From (Values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) N(N)),
cte2(N) As (Select Top (IsNull(DataLength(#String),0)) Row_Number() over (Order By (Select NULL)) From (Select N=1 From cte1 a,cte1 b,cte1 c,cte1 d) A ),
cte3(N) As (Select 1 Union All Select t.N+DataLength(#Delimiter) From cte2 t Where Substring(#String,t.N,DataLength(#Delimiter)) = #Delimiter),
cte4(N,L) As (Select S.N,IsNull(NullIf(CharIndex(#Delimiter,#String,s.N),0)-S.N,8000) From cte3 S)
Select RetSeq = Row_Number() over (Order By A.N)
,RetVal = LTrim(RTrim(Substring(#String, A.N, A.L)))
From cte4 A
);
--Orginal Source http://www.sqlservercentral.com/articles/Tally+Table/72993/
--Much faster than str-Parse, but limited to 8K
--Select * from [dbo].[udf-Str-Parse-8K]('Dog,Cat,House,Car',',')
--Select * from [dbo].[udf-Str-Parse-8K]('John||Cappelletti||was||here','||')
I suggest you to use following query, it's much faster than other functions like cross apply and udf.
SELECT
Variables
,S_DATA
FROM (
SELECT
Variables
,CASE WHEN LEN(LIST2)>0 THEN LTRIM(RTRIM(SUBSTRING(LIST2, NUMBER+1, CHARINDEX(',', LIST2, NUMBER+1)-NUMBER - 1)))
ELSE NULL
END AS S_DATA
,NUMBER
FROM(
SELECT Variables
,','+COMMA_SEPARETED_COLUMN+',' LIST2
FROM Tb1
)DT
LEFT OUTER JOIN TB N ON (N.NUMBER < LEN(DT.LIST2)) OR (N.NUMBER=1 AND DT.LIST2 IS NULL)
WHERE SUBSTRING(LIST2, NUMBER, 1) = ',' OR LIST2 IS NULL
) DT2
WHERE S_DATA<>''
and also you should create a table 'NUMBER' before running the above query.
CREATE TABLE TB (Number INT)
DECLARE #I INT=0
WHILE #I<1000
BEGIN
INSERT INTO TB VALUES (#I)
SET #I=#I+1
END

SQL IDENTITY column based on another column

Is there a way to define an identity column on another column? What I want to accomplish is a table that holds positions of an order and these orders can be put there anytime. So it could be that there are already lets say three positions in the table and it would look somewhat like this:
OrderNumber | OrderPosition
10001 1
10001 2
10001 3
And now I want to add another position without calculating the right value for the OrderPosition column. This is because I want to write new positions for multiple orders into the table and would like to avoid cursoring over the individual orders. I would prefer a solution wher OrderPosition is an identity column that is reseeded based on the OrderNumber column. So that If i add an order position for a new order it would start with 1 and if I add another position for order 10001 it would continue with 4.
Write a Scalar Function that returns the MAX(OrderPosition) based on OrderNumber. Then reference that function in the insert statement of orders
your requirement will not work for identity column.
You need to create custom logic to get from the normal columns and on combination based new no will generate.. like (read comments, only choose one logic)
declare #t table(OrderNumber int, OrderPosition int)
insert into #t values (10001, 1),(10001, 2),(10001, 3),(10001, 4)
select * from #t
--now insert new record with old orderno
declare #seq int = 1
declare #ordernumberNew int = 10001
--Eigher you can use :- insert to more understand
if( exists(select orderposition from #t where OrderNumber = #ordernumberNew ))
begin
set #seq = (select max(OrderPosition) + 1 from #t where OrderNumber = #ordernumberNew )
end
insert into #t values (#ordernumberNew , #seq )
select * from #t
--or another twist of above statement, insert directly as
insert into #t
values
(
#ordernumberNew,
case when exists (select orderposition from #t where OrderNumber = #ordernumberNew )
then (select max(OrderPosition) + 1 from #t where OrderNumber = #ordernumberNew )
else 1 end
)
select * from #t
--Now enter the not exist order no
set #ordernumberNew = 10006
insert into #t
values
(
#ordernumberNew,
case when exists (select orderposition from #t where OrderNumber = #ordernumberNew )
then (select max(OrderPosition) + 1 from #t where OrderNumber = #ordernumberNew )
else 1 end
)
select * from #t

How to find the longest sequence of letter in a string

I want to find out the longest sequence of letter in a string
e.g. in the word Honorificabcdwert , the output will be abcd.
How can I do it?
My idea is to get the Ascii and then count the sequence until it breaks at some point. But I was able to proceed with only
DECLARE #t TABLE(ID INT IDENTITY,String VARCHAR(100))
INSERT INTO #t SELECT 'Honorificabcdwert'
;with Get_Individual_Chars_Cte AS
(
SELECT
ID
,Row_ID =ROW_NUMBER() Over(PARTITION by ID Order by ID)
,SUBSTRING(String,Number,1) AS [Char]
,ASCII(SUBSTRING(String,Number,1)) AS [Ascii Value]
FROM #t
INNER JOIN master.dbo.spt_values ON
Number BETWEEN 1 AND LEN(String)
AND type='P'
)
Select * from Get_Individual_Chars_Cte
After this I don't know what to do. Help needed for this or any other way of doing so.
Will this help
DECLARE #t TABLE(ID INT IDENTITY,String VARCHAR(100))
INSERT INTO #t
SELECT 'Honorificabcdwert' UNION ALL
SELECT 'AbCdEfxy' UNION ALL
SELECT 'abc1234defg' UNION ALL
SELECT 'XYZABCPPCKLMIDBABC' UNION ALL
SELECT 'MNOP$%^&~()MNOPQRS;:'
SELECT ID, OriginalString,Sequence
FROM (SELECT ID, REPLACE(string,'%','') AS Sequence,OriginalString,
ROW_NUMBER() OVER(PARTITION BY ID ORDER BY LEN(string) DESC, string) AS rn
FROM (SELECT OriginalString = b.String, CASE WHEN b.String LIKE a.strings THEN a.strings ELSE NULL END AS string,
b.ID, ROW_NUMBER() OVER(PARTITION BY ID ORDER BY LEN(strings) DESC, strings) AS rn
FROM (SELECT COALESCE('%' + b.strings+a.strings + '%','%' + a.strings + '%') AS strings
FROM (SELECT SUBSTRING('ABCDEFGHIJKLMNOPQRSTUVWXYZ',t1.N,t2.N-t1.N+1) AS strings, t1.N
FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),
(9),(10),(11),(12),(13),(14),(15),
(16),(17),(18),(19),(20),(21),(22),
(23),(24),(25),(26)) t1(N)
CROSS JOIN (VALUES(1),(2),(3),(4),(5),(6),(7),(8),
(9),(10),(11),(12),(13),(14),(15),
(16),(17),(18),(19),(20),(21),(22),
(23),(24),(25),(26)) t2(N)
WHERE t1.N <= t2.N) a
LEFT OUTER JOIN (SELECT REVERSE(SUBSTRING('ZYXWVUTSRQPONMLKJIHGFEDCBA',1,N)) AS strings, 1 AS ID
FROM (VALUES(1),(2),(3),(4),(5),(6),(7),(8),
(9),(10),(11),(12),(13),(14),(15),
(16),(17),(18),(19),(20),(21),(22),
(23),(24),(25),(26)) t1(N)
UNION ALL SELECT '', 1) b ON a.N = b.ID) a
CROSS JOIN #t b) a ) a
WHERE a.rn = 1
ORDER BY a.ID
Result
ID OriginalString Sequence
1 Honorificabcdwert ABCD
2 AbCdEfxy ABCDEF
3 abc1234defg DEFG
4 XYZABCPPCKLMIDBABC XYZABC
5 MNOP$%^&~()MNOPQRS;: MNOPQRS
Based on your inputs provided in the course of discussion with #Martin Smith, the program is being developed. Please test it and let me know if it satisfies your requirement.
For consecutive rows with characters rising in alphabetical order (equating alphabetical order with ASCII order here) ROW_NUMBER() OVER (ORDER BY Row_ID) - [Ascii Value] will be the same.
This is not sufficient on its own however as for the string ABCZE that would put E in the same group as ABC so then you need a second operation to find gaps in that grouping sequence.
Something like the following should do it.
DECLARE #t TABLE(ID INT IDENTITY,String VARCHAR(100))
INSERT INTO #t SELECT 'Honorificabcdwfrt'
;with Get_Individual_Chars_Cte AS
(
SELECT
ID
,Row_ID =ROW_NUMBER() Over(PARTITION by ID Order by ID)
,SUBSTRING(String,number,1) AS [Char]
,ASCII(SUBSTRING(String,number,1)) AS [Ascii Value]
FROM #t
INNER JOIN master.dbo.spt_values ON
number BETWEEN 1 AND LEN(String)
AND type='P'
)
, T1 AS
(
Select *,
ROW_NUMBER() OVER (ORDER BY Row_ID) - [Ascii Value] AS RN
from Get_Individual_Chars_Cte
), T2 AS
(
SELECT *,
ROW_NUMBER() OVER (ORDER BY Row_ID) -
ROW_NUMBER() OVER (PARTITION BY RN ORDER BY Row_ID) AS Grp
FROM T1
)
SELECT TOP 1 WITH TIES *
FROM T2
ORDER BY COUNT(*) OVER (PARTITION BY RN, Grp) DESC

Get a random value from a range in MS SQL?

Let suppose my table can have values from 000 to 999 (three digits and less than 1000)
Some of this values are filled. Let's suppose currently my table has
000,002,005,190 (001,004,003,006,..189,191,..,999 can be inserted into table)
and these values are randomly allocated 000 and 002 is in table but 001 is not in table yet.
How can I get the values that I can insert into table yet.
DECLARE #t TABLE
(VALUE CHAR(3))
INSERT #t
VALUES
('000'),('002'),('005'),('190')
;WITH rnCTE
AS
(
SELECT -1 + ROW_NUMBER() OVER (ORDER BY TYPE, number, name) AS rn
FROM master.dbo.spt_values
)
SELECT RIGHT('000' + CAST( rn AS VARCHAR(11)),3)
FROM rnCTE
WHERE NOT EXISTS ( SELECT 1 FROM #t
WHERE VALUE = rn
)
AND rn < 1000
EDIT
This query works by generating the complete list of possible numbers from a system table (master.dbo.spt_values) which is guaranteed to contain more than 1000 rows inside the CTE rnCTE. -1 is added to ROW_NUMBER to have the values start at 0 rather than 1.
The outer query zero pads the numbers for display, returning only those which are not in the source data and are less than 1000.
DECLARE #t TABLE(id INT)
INSERT INTO #t (id)
VALUES
(1),(19),(3)
;WITH numbers AS (
SELECT ROW_NUMBER() OVER(ORDER BY o.object_id,o2.object_id) RN FROM sys.objects o
CROSS JOIN sys.objects o2
), NotExisted AS(
SELECT * FROM numbers WHERE RN NOT IN (SELECT ID FROM #t)
AND RN<1000)
SELECT TOP 1 RN FROM NotExisted ORDER BY NEWID()
You will have to write a T-SQL to first query and find the gaps. There is no ready made SQL that will give you the gaps directly.