How to split each row into multiple rows - sql

My table has three rows:
ID MULTI_CODES
1 10-101-102
2 20-201-202
3 30-301-302
How to write a statement to split each row like this considering delimited code
ID SINGLE_CODE LEVEL
1 10 1
1 101 2
1 102 3
2 20 1
2 201 2
2 202 3
3 30 1
3 301 2
3 302 3

If you are on SQL Server 2016 you can use the built in string_split function.
If not you will need your own function. One that performs very well is Jeff Moden's tally table method. My altered version of this looks like this, allowing for user specified delimiters and if required specific values:
create function dbo.StringSplit
(
#str nvarchar(4000) = ' ' -- String to split.
,#delimiter as nvarchar(1) = ',' -- Delimiting value to split on.
,#num as int = null -- Which value to return.
)
returns table
as
return
(
-- Start tally table with 10 rows.
with n(n) as (select n from (values(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n(n))
-- Select the same number of rows as characters in isnull(#str,'') as incremental row numbers.
-- Cross joins increase exponentially to a max possible 10,000 rows to cover largest isnull(#str,'') length.
,t(t) as (select top (select len(isnull(#str,'')) a) row_number() over (order by (select null)) from n n1,n n2,n n3,n n4)
-- Return the position of every value that follows the specified delimiter.
,s(s) as (select 1 union all select t+1 from t where substring(isnull(#str,''),t,1) = #delimiter)
-- Return the start and length of every value, to use in the SUBSTRING function.
-- ISNULL/NULLIF combo handles the last value where there is no delimiter at the end of the string.
,l(s,l) as (select s,isnull(nullif(charindex(#delimiter,isnull(#str,''),s),0)-s,4000) from s)
select rn as ItemNumber
,Item
from(select row_number() over(order by s) as rn
,substring(isnull(#str,''),s,l) as item
from l
) a
where rn = #num -- Return a specific value where specified,
or #num is null -- Or everything where not.
)
go
And is used as follows:
declare #t table (ID int, MULTI_CODES nvarchar(50));
insert into #t values (1,'10-101-102'),(2,'20-201-202'),(3,'30-301-302');
select t.ID
,t.MULTI_CODES
,s.Item as SINGLE_CODE
,s.ItemNumber as [Level]
from #t t
outer apply dbo.StringSplit(t.MULTI_CODES,'-',null) s
order by t.ID
,s.ItemNumber;
Which outputs:
+----+-------------+-------------+-------+
| ID | MULTI_CODES | SINGLE_CODE | Level |
+----+-------------+-------------+-------+
| 1 | 10-101-102 | 10 | 1 |
| 1 | 10-101-102 | 101 | 2 |
| 1 | 10-101-102 | 102 | 3 |
| 2 | 20-201-202 | 20 | 1 |
| 2 | 20-201-202 | 201 | 2 |
| 2 | 20-201-202 | 202 | 3 |
| 3 | 30-301-302 | 30 | 1 |
| 3 | 30-301-302 | 301 | 2 |
| 3 | 30-301-302 | 302 | 3 |
+----+-------------+-------------+-------+

IF OBJECT_ID('tempdb..#TEMPtable') IS NOT NULL
Drop table #TEMPtable
;With cte(ID, MULTI_CODES)
AS
(
select 1,'10-101-102' UNION ALL
select 2,'20-201-202' UNION ALL
select 3,'30-301-302'
)
SELECT * INTO #TEMPtable FROM cte
SELECT ID, Split.a.value('.','Varchar(100)') AS MULTI_CODES,ROW_NUMBER()Over(Partition by ID Order by ID) AS LEVEL
FROM(
SELECT ID, CASt('<M>' + Replace(MULTI_CODES,'-','</M><M>') +'</M>' AS XML)As MULTI_CODES
FROM #TEMPtable
)AS A
CROSS APPLY
MULTI_CODES.nodes('/M') AS Split(A)
OutPut
ID MULTI_CODES LEVEL
----------------------
1 10 1
1 101 2
1 102 3
2 20 1
2 201 2
2 202 3
3 30 1
3 301 2
3 302 3

you can use cross apply to string_split function
select id, Value as Single_code, RowN as [Level] from #yourcodes cross apply
(
select RowN= row_number() over (order by (select null)), value
from string_split(Multi_codes, '-')
) a
Output:
+----+-------------+-------+
| id | Single_code | Level |
+----+-------------+-------+
| 1 | 10 | 1 |
| 1 | 101 | 2 |
| 1 | 102 | 3 |
| 2 | 20 | 1 |
| 2 | 201 | 2 |
| 2 | 202 | 3 |
| 3 | 30 | 1 |
| 3 | 301 | 2 |
| 3 | 302 | 3 |
+----+-------------+-------+

Related

Get some values from the table by selecting

I have a table:
| id | Number |Address
| -----| ------------|-----------
| 1 | 0 | NULL
| 1 | 1 | NULL
| 1 | 2 | 50
| 1 | 3 | NULL
| 2 | 0 | 10
| 3 | 1 | 30
| 3 | 2 | 20
| 3 | 3 | 20
| 4 | 0 | 75
| 4 | 1 | 22
| 4 | 2 | 30
| 5 | 0 | NULL
I need to get: the NUMBER of the last ADDRESS change for each ID.
I wrote this select:
select dh.id, dh.number from table dh where dh =
(select max(min(t.history)) from table t where t.id = dh.id group by t.address)
But this select not correctly handling the case when the address first changed, and then changed to the previous value. For example id=1: group by return:
| Number |
| -------- |
| NULL |
| 50 |
I have been thinking about this select for several days, and I will be happy to receive any help.
You can do this using row_number() -- twice:
select t.id, min(number)
from (select t.*,
row_number() over (partition by id order by number desc) as seqnum1,
row_number() over (partition by id, address order by number desc) as seqnum2
from t
) t
where seqnum1 = seqnum2
group by id;
What this does is enumerate the rows by number in descending order:
Once per id.
Once per id and address.
These values are the same only when the value is 1, which is the most recent address in the data. Then aggregation pulls back the earliest row in this group.
I answered my question myself, if anyone needs it, my solution:
select * from table dh1 where dh1.number = (
select max(x.number)
from (
select
dh2.id, dh2.number, dh2.address, lag(dh2.address) over(order by dh2.number asc) as prev
from table dh2 where dh1.id=dh2.id
) x
where NVL(x.address, 0) <> NVL(x.prev, 0)
);

Calculate how many rows are ahead of position in column when condition is met

How can I calculate how many people are ahead of Jane on Floor 2 (not including those on floor 1)?
+------+---------+----------+
|Index | Name | Floor |
+------+---------+----------+
| 1 | Sally | 1 |
| 2 | Sue | 1 |
| 3 | Fred | 1 |
| 4 | Wally | 2 |
| 5 | Tommy | 2 |
| 6 | Jane | 2 |
| 7 | Bart | 2 |
| 8 | Sam | 3 |
+------+---------+----------+
The expected result is 2 as there are 2 people (Wally & Tommy) ahead of Jane on floor 2.
I've tried using CHARINDEX to find the row number from a temp table that I've generated but that doesn't seem to work:
SELECT CHARINDEX('Jane', Name) as position
INTO #test
FROM tblExample
WHERE Floor = 2
select ROW_NUMBER() over (order by position) from #test
WHERE position = 1
I think a simple row_number() would do the trick
Select Value = RN-1
From (
Select *
,RN = row_number() over (partition by [floor] order by [index])
From YourTable
Where [Floor]=2
) A
Where [Name]='Jane'
You could do:
select count(*)
from t
where t.floor = 2 and
t.id < (select t2.id from t t2 where t2.name = 'Jane' and t2.floor = 2);
With an index on (floor, name, id), I would expect this to be faster than row_number().

How can I found third 'a' position in the word Malayalam.using sql char index?

CHARINDEX ( expressionToFind , expressionToSearch [ , start_location ] )
You could use:
WITH cte AS (
SELECT 'Malayalam' s
)
SELECT cte.s, s3.r
FROM cte
OUTER APPLY (SELECT CHARINDEX('a', cte.s, 0)) s1(r) -- first occurence
OUTER APPLY (SELECT CHARINDEX('a', cte.s, s1.r+1)) s2(r) -- second occurence
OUTER APPLY (SELECT CHARINDEX('a', cte.s, s2.r+1)) s3(r) -- third occurence
-- ...
db<>fiddle demo
I think you are looking for
DECLARE #Str VARCHAR(25) = 'Malayalam';
WITH CTE AS
(
SELECT Number + 1 CharPositionInString,
SUBSTRING(#Str, Number + 1, 1) Character,
ROW_NUMBER() OVER(PARTITION BY SUBSTRING(#Str, Number + 1, 1) ORDER BY Number) Occurence
FROM master..spt_values
WHERE Type = 'P'
AND
Number < LEN(#Str)
)
SELECT *
FROM CTE
ORDER BY CharPositionInString;
Returns:
+----------------------+-----------+-----------+
| CharPositionInString | Character | Occurence |
+----------------------+-----------+-----------+
| 1 | M | 1 |
| 2 | a | 1 |
| 3 | l | 1 |
| 4 | a | 2 |
| 5 | y | 1 |
| 6 | a | 3 |
| 7 | l | 2 |
| 8 | a | 4 |
| 9 | m | 2 |
+----------------------+-----------+-----------+
You can also use CHARINDEX to get the third position of character 'a' as shown below-
This will only return correct result if there are at least 3 existence of character 'a'.
DECLARE #Word VARCHAR (100) = 'Malayalam'
SELECT CHARINDEX('a',#Word,CHARINDEX('a',#Word,CHARINDEX('a',#Word,1)+1)+1)

T-SQL: Best way to replace NULL with most recent non-null value?

Assume I have this table:
+----+-------+
| id | value |
+----+-------+
| 1 | 5 |
| 2 | 4 |
| 3 | 1 |
| 4 | NULL |
| 5 | NULL |
| 6 | 14 |
| 7 | NULL |
| 8 | 0 |
| 9 | 3 |
| 10 | NULL |
+----+-------+
I want to write a query that will replace any NULL value with the last value in the table that was not null in that column.
I want this result:
+----+-------+
| id | value |
+----+-------+
| 1 | 5 |
| 2 | 4 |
| 3 | 1 |
| 4 | 1 |
| 5 | 1 |
| 6 | 14 |
| 7 | 14 |
| 8 | 0 |
| 9 | 3 |
| 10 | 3 |
+----+-------+
If no previous value existed, then NULL is OK. Ideally, this should be able to work even with an ORDER BY. So for example, if I ORDER BY [id] DESC:
+----+-------+
| id | value |
+----+-------+
| 10 | NULL |
| 9 | 3 |
| 8 | 0 |
| 7 | 0 |
| 6 | 14 |
| 5 | 14 |
| 4 | 14 |
| 3 | 1 |
| 2 | 4 |
| 1 | 5 |
+----+-------+
Or even better if I ORDER BY [value] DESC:
+----+-------+
| id | value |
+----+-------+
| 6 | 14 |
| 1 | 5 |
| 2 | 4 |
| 9 | 3 |
| 3 | 1 |
| 8 | 0 |
| 4 | 0 |
| 5 | 0 |
| 7 | 0 |
| 10 | 0 |
+----+-------+
I think this might involve some kind of analytic function - somehow partitioning over the value column - but I'm not sure where to look.
You can use a running sum to set groups and use max to fill in the null values.
select id,max(value) over(partition by grp) as value
from (select id,value,sum(case when value is not null then 1 else 0 end) over(order by id) as grp
from tbl
) t
Change the over() clause to order by value desc to get the second result in the question.
The best way has been covered by Itzik Ben-Gan here:The Last non NULL Puzzle
Below is a solution which for 10 million rows and completes around in 20 seconds on my system
SELECT
id,
value1,
CAST(
SUBSTRING(
MAX(CAST(id AS binary(4)) + CAST(value1 AS binary(4)))
OVER (ORDER BY id
ROWS UNBOUNDED PRECEDING),
5, 4)
AS int) AS lastval
FROM dbo.T1;
This solution assumes your id column is indexed
You can also try using correlated subquery
select id,
case when value is not null then value else
(select top 1 value from table
where id < t.id and value is not null order by id desc) end value
from table t
Result :
id value
1 5
2 4
3 1
4 1
5 1
6 14
7 14
8 0
9 3
10 3
If the NULLs are scattered I use a WHILE loop to fill them in
However if the NULLs are in longer consecutive strings there are faster ways to do it.
So here's one approach:
First find a record that we want to update. It has NULL in this record and no NULL in the prior record
SELECT C.VALUE, N.ID
FROM TABLE C
INNER JOIN TABLE N
ON C.ID + 1 = N.ID
WHERE C.VALUE IS NOT NULL
AND N.VALUE IS NULL;
Use that to update: (bit hazy on this syntax but you get the idea)
UPDATE N
SET VALUE = C.Value
FROM TABLE C
INNER JOIN TABLE N
ON C.ID + 1 = N.ID
WHERE C.VALUE IS NOT NULL
AND N.VALUE IS NULL;
.. now just keep doing it till you run out of rows
-- This is needed to set ##ROWCOUNT to non zero
SELECT 1;
WHILE ##ROWCOUNT <> 0
BEGIN
UPDATE N
SET VALUE = C.Value
FROM TABLE C
INNER JOIN TABLE N
ON C.ID + 1 = N.ID
WHERE C.VALUE IS NOT NULL
AND N.VALUE IS NULL;
END
The other way is to use a similiar query to get a range of id's to update. This works much faster if your NULLS are usually against consecutive id's
Here is the one simple approach using OUTER APPLY
CREATE TABLE #table(id INT, value INT)
INSERT INTO #table VALUES
(1,5),
(2,4),
(3,1),
(4,NULL),
(5,NULL),
(6,14),
(7,NULL),
(8,0),
(9,3),
(10,NULL)
SELECT t.id, ISNULL(t.value, t3.value) value
FROM #table t
OUTER APPLY(SELECT id FROM #table WHERE id = t.id AND VALUE IS NULL) t2
OUTER APPLY(SELECT TOP 1 value
FROM #table WHERE id <= t2.id AND VALUE IS NOT NULL ORDER BY id DESC) t3
OUTPUT:
id VALUE
---------
1 5
2 4
3 1
4 1
5 1
6 14
7 14
8 0
9 3
10 3
Using this sample data:
if object_id('tempdb..#t1') is not null drop table #t1;
create table #t1 (id int primary key, [value] int null);
insert #t1 values(1,5),(2,4),(3,1),(4,NULL),(5,NULL),(6,14),(7,NULL),(8,0),(9,3),(10,NULL);
I came up with:
with x(id, [value], grouper) as (
select *, row_number() over (order by id)-sum(iif([value] is null,1,0)) over (order by id)
from #t1)
select id, min([value]) over (partition by grouper)
from x;
I noticed, however, that Vamsi Prabhala beat me to it... My solution is identical to what he posted. (arghhhh!). So I thought I'd try a recursive solution. Here's a pretty efficient use of a recursive cte (provided that ID is indexed):
with sorted as (select *, seqid = row_number() over (order by id) from #t1),
firstRecord as (select top(1) * from #t1 order by id),
prev as
(
select t.id, t.[value], lastid = 1, lastvalue = null
from sorted t
where t.id = 1
union all
select t2.id, t2.[value], lastid+1, isnull(prev.[value],lastvalue)
from sorted t2
join prev on t2.id = prev.lastid+1
)
select id, [value]=isnull([value],lastvalue)--, *
from prev;
Normally I don't like recursive cte's (rCte for short) but in this case it offered an elegant solution and was faster than using the window aggregate function (sum over, min over...). Note the execution plans, the rcte on the bottom. The rCTE get's it done with two index seeks, one of which is for just one row. Unlike the window aggregate solution, the rcte does not require a sort. Running this with statistics io on; the rcte produces much less IO.
All this said, don't use either of these solutions, What the TheGameiswar posted will perform the best by far. His solution on a properly indexed id column would be lightening fast.
Following UPDATE statement can be used, please test it before use
update #table
set value = newvalue
from (
select
s.id, s.value,
(select top 1 t.value from #table t where t.id <= s.id and t.value is not null order by t.id desc) as newvalue
from #table S
) u
where #table.id = u.id and #table.value is null
stop worrying..here's the answer for you :)
SELECT *
INTO #TempIsNOtNull
FROM YourTable
WHERE value IS NOT NULL
SELECT *
INTO #TempIsNull
FROM YourTable
WHERE value IS NULL
UPDATE YourTable
SEt YourTable.value = UpdateDtls.value
FROM YourTable
JOIN (
SELECT OuterTab1.id,
#TempIsNOtNull.value
FROM #TempIsNull OuterTab1
CROSS JOIN #TempIsNOtNull
WHERE OuterTab1.id - #TempIsNOtNull.id > 0
AND (OuterTab1.id - #TempIsNOtNull.id) = ( SELECT TOP 1
OuterTab1.id - #TempIsNOtNull.id
FROM #TempIsNull InnerTab
CROSS JOIN #TempIsNOtNull
WHERE OuterTab1.id - #TempIsNOtNull.id > 0
AND OuterTab1.id = InnerTab.id
ORDER BY (OuterTab1.id - #TempIsNOtNull.id) ASC) ) AS UpdateDtls
ON (YourTable.id = UpdateDtls.id)

Select a row X times

I have a very specific sql problem.
I have a table given with order positions (each position belongs to one order, but this isn't a problem):
| Article ID | Amount |
|--------------|----------|
| 5 | 3 |
| 12 | 4 |
For the customer, I need an export with every physical item that is ordered, e.g.
| Article ID | Position |
|--------------|------------|
| 5 | 1 |
| 5 | 2 |
| 5 | 3 |
| 12 | 1 |
| 12 | 2 |
| 12 | 3 |
| 12 | 4 |
How can I build my select statement to give me this results? I think there are two key tasks:
1) Select a row X times based on the amount
2) Set the position for each physical article
You can do it like this
SELECT ArticleID, n.n Position
FROM table1 t JOIN
(
SELECT a.N + b.N * 10 + 1 n
FROM
(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) a
,(SELECT 0 AS N UNION ALL SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9) b
) n
ON n.n <= t.amount
ORDER BY ArticleID, Position
Note: subquery n generates a sequence of numbers on the fly from 1 to 100. If you do a lot of such queries you may consider to create persisted tally(numbers) table and use it instead.
Here is SQLFiddle demo
or using a recursive CTE
WITH tally AS (
SELECT 1 n
UNION ALL
SELECT n + 1 FROM tally WHERE n < 100
)
SELECT ArticleID, n.n Position
FROM table1 t JOIN tally n
ON n.n <= t.amount
ORDER BY ArticleID, Position
Here is SQLFiddle demo
Output in both cases:
| ARTICLEID | POSITION |
|-----------|----------|
| 5 | 1 |
| 5 | 2 |
| 5 | 3 |
| 12 | 1 |
| 12 | 2 |
| 12 | 3 |
| 12 | 4 |
Query:
SQLFIDDLEExample
SELECT t1.[Article ID],
t2.number
FROM Table1 t1,
master..spt_values t2
WHERE t1.Amount >= t2.number
AND t2.type = 'P'
AND t2.number <= 255
AND t2.number <> 0
Result:
| ARTICLE ID | NUMBER |
|------------|--------|
| 5 | 1 |
| 5 | 2 |
| 5 | 3 |
| 12 | 1 |
| 12 | 2 |
| 12 | 3 |
| 12 | 4 |