SQL: Find missing hierarchy Folders (Paths) in a table - sql

I have a table which contains Folders Paths.
I need to find all the "gaps" between those folders in the hierarchy.
I mean that, if the table contains these 3 folders:
'A'
'A\B\C'
'A\B\C\D\E\F\G'
I need to find the following missing folders in the hierarchy:
'A\B'
'A\B\C\D'
'A\B\C\D\E'
'A\B\C\D\E\F'
This table contains more than 250,000 records of folders, so we seek for the most efficient way to do so, otherwise the script will be stuck for long time, time we don't have.
Comment: I don't have list of all folders. What I have are the "root" folders and the "leafs" folders which I need to find the "gaps" between them in the hierarchy.
Second comment: The table can contains more than one hierarchy and we need to find the "gaps" in all of the hierarchies.
For that matter, There are 2 another int columns: "DirID" and "BaseDirID". The "DirID" column is the id column in our table. The "BaseDirID" contains the id of the first folder in the hierarchy. So all the folders (paths) from the same hierarchy share the same value in this column. Sample data for example:
DirID BaseDirID DisplayPath
1 1 'A'
2 1 'A\B\C'
3 1 'A\B\C\D\E'
4 4 'U'
5 4 'U\V\W'
6 4 'U\V\W\X\Y'
So we need to find the following data:
BaseDirID DisplayPath
1 'A\B'
1 'A\B\C\D'
4 'U\V'
4 'U\V\W\X'
Thanks in advance.

Here is one approach using Recursive CTE and split string function
;WITH existing_hierachies
AS (SELECT DirID,
BaseDirID,
DisplayPath
FROM (VALUES (1,1,'A' ),
(2,1,'A\B\C' ),
(3,1,'A\B\C\D\E' ),
(4,4,'U' ),
(5,4,'U\V\W' ),
(6,4,'U\V\W\X\Y' )) tc (DirID, BaseDirID, DisplayPath) ),
folders_list
AS (SELECT ItemNumber,
item fol,
BaseDirID
FROM (SELECT row_number()over(partition by BaseDirID order by Len(DisplayPath) DESC)rn,*
FROM existing_hierachies) a
CROSS apply dbo.[Delimitedsplit8k](DisplayPath, '\')
Where Rn = 1),
rec_cte
AS (SELECT *,
Cast(fol AS VARCHAR(4000))AS hierar
FROM folders_list
WHERE ItemNumber = 1
UNION ALL
SELECT d.*,
Cast(rc.hierar + '\' + d.fol AS VARCHAR(4000))
FROM rec_cte rc
JOIN folders_list d
ON rc.BaseDirID = d.BaseDirID
AND d.ItemNumber = rc.ItemNumber + 1)
SELECT rc.BaseDirID,
rc.hierar AS Missing_Hierarchies
FROM rec_cte rc
WHERE NOT EXISTS (SELECT 1
FROM existing_hierachies eh
WHERE eh.BaseDirID = rc.BaseDirID
AND eh.DisplayPath = rc.hierar)
Order by rc.BaseDirID
Result :
+-----------+---------------------+
| BaseDirID | Missing_Hierarchies |
+-----------+---------------------+
| 1 | A\B |
| 1 | A\B\C\D |
| 4 | U\V |
| 4 | U\V\W\X |
+-----------+---------------------+
Split string function code
CREATE FUNCTION [dbo].[DelimitedSplit8K]
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...
-- enough to cover NVARCHAR(4000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
GO
Referred from http://www.sqlservercentral.com/articles/Tally+Table/72993/

Related

How to extract a string between two of the SAME delimiters T-SQL?

I'm wanting to extract part of a string from a value which has a number of the same delimiters.
Here is an example of the data I am working with (these file paths could be even longer depending on the depth of the file):
FilePath:
Q:\12345\downloads\randomfilename.png
Q:\123_4566\downloads\randomfilename.pdf
Q:\CCCMUD\downloads\randomfilename.mp4
I want to extract part of the string between the first two delimiters ( \ ) for every row into a new column e.g.
12345
123_4566
CCCMUD
I know I need to be using SUBSTRING and CHARINDEX but I'm not sure how. I would appreciate any help. Thanks.
Use CHAR_INDEX twice:
SELECT *, SUBSTRING(path, pos1 + 1, pos2 - pos1 - 1)
FROM tests
CROSS APPLY (SELECT NULLIF(CHARINDEX('\', path), 0)) AS ca1(pos1)
CROSS APPLY (SELECT NULLIF(CHARINDEX('\', path, pos1 + 1), 0)) AS ca2(pos2)
-- NULLIF is used to convert 0 value (character not found) to NULL
Test on db<>fiddle
In all your examples, the first \ is at character 3 in the string. If so, then you can simply use:
select v.*,
substring(filepath, 4, charindex('\', filepath, 4) - 4)
from (values ('Q:\123_4566\downloads\randomfilename.pdf')) v(filepath)
DECLARE #s table (path varchar(4000));
INSERT #s(path) VALUES
('Q:\12345\downloads\randomfilename.png'),
('Q:\123_4566\downloads\randomfilename.pdf'),
('Q:\CCCMUD\downloads\randomfilename.mp4');
SELECT folder = LEFT(o, CHARINDEX('\', o) - 1) FROM
(
SELECT o = SUBSTRING(path, CHARINDEX('\', path) + 1, 4000)
FROM #s
) AS o;
Output:
folder
----------
12345
123_4566
CCCMUD
This will error, though, for paths that don't contain two \ characters. So you may want to add a filter to the inner query (or determine how you want to handle the output differently in that case):
WHERE path LIKE '%\%\%'
An easy and efficient way to do this is to use an ordinal splitter (like this one). To make sure the split value only contains numbers you could add WHERE try_cast(ds.Item as int) is not null. Something like this
splitter
CREATE FUNCTION [dbo].[DelimitedSplit8K_LEAD]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "zero base" and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT 0 UNION ALL
SELECT TOP (DATALENGTH(ISNULL(#pString,1))) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT t.N+1
FROM cteTally t
WHERE (SUBSTRING(#pString,t.N,1) = #pDelimiter OR t.N = 0)
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY s.N1),
Item = SUBSTRING(#pString,s.N1,ISNULL(NULLIF((LEAD(s.N1,1,1) OVER (ORDER BY s.N1) - 1),0)-s.N1,8000))
FROM cteStart s
;
query
select ds.*
from #s s
cross apply dbo.DelimitedSplit8K_LEAD(s.[path], '\') ds
where ds.ItemNumber=2
and try_cast(ds.Item as int) is not null;
ItemNumber Item
2 12345

SQL Sting Split into Single Column

Very new to SQL but I require some help with something that I am sure is a simple fix.
I have a single column of data within a table called 'Produce' where types of fruit are stored in a column called 'Fruit'. Some values within this column are separated by commas.
Is there an easy way to split the below so that the results come as a single column of unique entries?
E.g. Example Table
Fruit
-----
Apple
Plum
Pear, Mango
Pear
What I am hoping to return is the below:
Fruit
-----
Apple
Plum
Pear
Mango
I have tried to use the string split function but I think I have it completely. Can anyone help provide some explanation as to how to do this, please? I am using T-SQL if that helps.
Thanks in advance.
The core problem to fix would be to stop storing your values as comma separated lists. Keep your data normalized. With that being said... everyone needs a good splitter...
declare #table table (Fruit varchar(64))
insert into #table
values
('Apple'),
('Plum'),
('Pear,Mango'),
('Pear')
select distinct
Item
from
#table
cross apply
dbo.DelimitedSplit8K(Fruit,',')
OR, If you are on SQL Server 2016...
select distinct
Item
from
#table
cross apply
string_split(Fruit,',')
THE FUNCTION
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE FUNCTION [dbo].[DelimitedSplit8K] (#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
/* "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
enough to cover VARCHAR(8000)*/
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
GO
Jeff Moden Article for Function
This can be done with pure SQL, no user written functions needed.
SQL Server
WITH
fruittable
AS
( SELECT 'Apple' fruit, 1 id
UNION ALL
SELECT 'Banana,Apple', 2
UNION ALL
SELECT 'Tomato,Grapefruit,Apple', 3
UNION ALL
SELECT 'Watermelon,Persimmons', 4
),
split (fruit, id, leftover)
AS
(SELECT case when len(fruit) = 0 or fruit is null then null else left(fruit + ',', charindex(',',fruit + ',') -1 ) end AS fruit
, id
, case when len(fruit) = 0 or fruit is null then null else right(fruit + ',', len(fruit) - charindex(',',fruit + ',') + 1) end as leftover
FROM fruittable
UNION ALL
SELECT case when len(leftover) = 0 or leftover is null then null else left(leftover, charindex(',',leftover) - 1) end AS fruit
, id
, case when len(leftover) = 0 or leftover is null then null else substring(leftover, charindex(',',leftover) + 1, len(leftover)) end as leftover
FROM split
WHERE fruit IS NOT NULL)
SELECT fruit, id
FROM split where fruit is not null
order by fruit, id;
Oracle
WITH
fruittable
AS
(SELECT 'Apple' fruit, 1 id
FROM DUAL
UNION ALL
SELECT 'Banana,Apple', 2
FROM DUAL
UNION ALL
SELECT 'Tomato,Grapefruit,Apple', 3
FROM DUAL
UNION ALL
SELECT 'Watermelon,Persimmons', 4
FROM DUAL),
split (fruit, id, leftover)
AS
(SELECT SUBSTR (fruit || ',', 1, INSTR (fruit || ',', ',') - 1) AS fruit
, id
, SUBSTR (fruit || ',', INSTR (fruit || ',', ',') + 1) AS leftover
FROM fruittable
UNION ALL
SELECT SUBSTR (leftover, 1, INSTR (leftover, ',') - 1) AS fruit
, id
, SUBSTR (leftover, INSTR (leftover, ',') + 1) AS leftover
FROM split
WHERE fruit IS NOT NULL)
SELECT fruit, id
FROM split
WHERE fruit IS NOT NULL
ORDER BY fruit, id

How to replace all numbers of exactly 8 characters in length eg 12345678

I've done a good bit of searching all over so don't berate me yet.
I have a column with string values showing the name of shows.
Eg:
[Titles]
World Cup 2014
Family Guy
UFC Fight Night
9pm News and Weather
2014 Media Awards
Homeland 25242324
Simpsons 25242314
Shameless
Soccer Night 45342324 International
Rugby Live 45342324 HTML5
I wish to use a select statement to strip out the numbers where the numbers are exactly 8 characters in length.
I have only read access so cannot create functions and I'm using SQL Server 2005.
There are a number of split functions that can be found around the internet. My personal preference is the one created by Jeff Moden and enhanced by the community over the last few years. You can find his article here, http://www.sqlservercentral.com/articles/Tally+Table/72993/. Make sure you look at the comments...there are a few hundred at the time of this posting. Here is the code for that splitter.
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 0 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "zero base" and limits the number of rows right up front
-- for both a performance gain and prevention of accidental "overruns"
SELECT 0 UNION ALL
SELECT TOP (DATALENGTH(ISNULL(#pString,1))) ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
SELECT t.N+1
FROM cteTally t
WHERE (SUBSTRING(#pString,t.N,1) = #pDelimiter OR t.N = 0)
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY s.N1),
Item = SUBSTRING(#pString,s.N1,ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000))
FROM cteStart s
;
No matter which splitter you use (as long as it is a table valued function) it can work just like this.
if OBJECT_ID('tempdb..#Something') is not null
drop table #Something;
create table #Something
(
SomeValue varchar(100)
);
insert #Something
select 'World Cup 2014' union all
select 'Family Guy' union all
select 'UFC Fight Night' union all
select '9pm News and Weather' union all
select '2014 Media Awards' union all
select 'Homeland 2524232' union all
select 'Simpsons 2524231' union all
select 'Shameless' union all
select 'Soccer Night 4534232 International' union all
select 'Rugby Live 4534232 HTML5';
with ParsedData as
(
select *
from #Something s
cross apply dbo.DelimitedSplit8K(s.SomeValue, ' ')
where Item not like '[0-9][0-9][0-9][0-9][0-9][0-9][0-9]'
)
select distinct
Stuff((SELECT ' ' + Item
FROM ParsedData p2
WHERE p1.SomeValue = p2.SomeValue
ORDER BY p1.ItemNumber --The split function keeps track of the order for us already
FOR XML PATH('')), 1, 1, ' ') as Details
from ParsedData p1
create table #tmp(myString varchar(255))
insert into #tmp
values('Soccer Night 45342327 International'),('9pm News and Weather')
Select newstring = case when patindex('%[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]%',mystring) > 0 then
left(mystring, patindex('%[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]%',mystring) - 1)
+ substring(mystring,patindex('%[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]%',mystring) + 8,9999)
else mystring end
from #tmp
drop table #tmp

How to find values that contain less than three words in SQL?

How to select strings with less than three words?
For example:
id keyword
1 my name is john
2 im mike
3 david cameron
4 electra
5 goliath is my name
6 michael jordan
So i want to select the keyword column with less than 3 word count, it should be :
im mike
david cameron
electra
michael jordan
Try:
select *
from table_name
where length(keyword) - length(replace(keyword, ' ', '')) + 1 < 3
This takes the length of the whole string minus the length of the string without spaces (calculating the # of spaces), and then adds one (ie. if a string has one space that means there are 2 words not 1 word), and filters out any rows where the calculation is not < 3
Using Jeff Moden's 8K Tally-Ho CSV splitter
create function [dbo].[DelimitedSplit8K](
#pString varchar(8000) --WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
,#pDelimiter char(1)
)
returns table with schemabinding as
return
with E1(N) AS (
select N from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1) )E1(N)
), --10E+1 or 10 rows
E2(N) as (select 1 from E1 a cross join E1 b), --10E+2 or 100 rows
E4(N) as (select 1 from E2 a cross join E2 b), --10E+4 or 10,000 rows max
cteTally(N) as (
select top (isnull(datalength(#pString),0))
row_number() over (order by (select null))
from E4
),
start(N1) AS (--==== This returns N+1 (starting position of each "element" just once for each delimiter)
select 1 union all
select t.N+1 from cteTally t where substring(#pString,t.N,1) = #pDelimiter
),
Len(N1,L1) as(--==== Return start and length (for use in substring)
select s.N1,
isnull(nullif(charindex(#pDelimiter,#pString,s.N1),0) - s.N1,8000)
from start s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final element when no delimiter is found.
select
ItemNumber = row_number() over(order by l.N1),
Item = substring(#pString, l.N1, l.L1)
from Len l
;
go
and sample data as suggested in a CTE
with
data as (
select id,keyword from (values
--id keyword
(1, 'my name is john')
,(2, 'im mike')
,(3, 'david cameron')
,(4, 'electra')
,(5, 'goliath is my name')
,(6, 'michael jordan')
,(7, 'michael jordan')
)data(id,keyword)
),
this SQL
strings as (
select
data.id
,keyword
,s.Item
,s.ItemNumber
from data
cross apply dbo.DelimitedSplit8K(data.keyword,' ') as s
)
select
strings.id
from strings
where len(strings.item) > 0 -- remove zero-length string and thus doubled-up delimters
group by strings.id
having count(itemnumber) <=3;
yields as desired:
id
-----------
2
3
4
6
7
Note that the doubled-up spaces in id=7 are compressed by the removal of zero-length strings.

function in where clause

I have two scenarios. The first one is fast, but not an option. The second one uses a function and kills the indexing. The values would look similar to this 'ww,tt,tt,bb'. They can put as many codes as they want. Is there a better approach than using the function? This is a stored proc using server 2008.
Without Function:
WHERE date BETWEEN '20140701' AND '20140731' and
LEFT(id, 2) IN ('wp')
With Function:
WHERE date BETWEEN '20140701' AND '20140731' and
LEFT(id, 2) IN (SELECT* FROM Toolbox.dbo.Split_DelimitedString_fn(#string,',') )
My suggestion is to split the string before hand and insert the result into a temp table:
INSERT INTO #tmp
SELECT *
FROM Toolbox.dbo.Split_DelimitedString_fn(#string,',')
Then rewrite your query as
WHERE date BETWEEN '20140701' AND '20140731' and
LEFT(id, 2) IN (SELECT * FROM #tmp)
Declare a table variable, and insert the values:
DECLARE #SplitIds TABLE
(
SplitId varchar(10)
)
Insert into #SplitIds (SplitIds)
SELECT field FROM Toolbox.dbo.Split_DelimitedString_fn(#string,',')
You could then join against this table...
SELECT columns
FROM TableName a INNER JOIN #SplitIds b ON LEFT(id, 2) = b.SplitId
WHERE a.date BETWEEN '20140701' AND '20140731'
Not sure if this will speed the query up, but you can use the Execution plan to help and also check your indexes.
So if I understand the question correctly you want to have the 'in' clause be dynamically determined by what is in another table.
Have you tried:
WHERE date BETWEEN '20140701' AND '20140731' and
LEFT(id, 2) IN (SELECT <column> FROM Toolbox.dbo.tblname)
Second attempt:
move the Left(id,2) into the select clause and then filter on that columns
select
<current query>
,Left(id,2) as sort
from
<current query>
WHERE date BETWEEN '20140701' AND '20140731' and
sort IN (SELECT* FROM Toolbox.dbo.Split_DelimitedString_fn(#string,',') )
If I understand correctly, the performance issue centers on how to efficiently split the CSV string in SQL. If so my answer here on how to employ Jeff Moden's Tally-Ho CSV Splitter might be the ticket. Repeated here for clarity:
Function definition:
CREATE FUNCTION [dbo].[DelimitedSplit8K]
--===== Define I/O parameters
(#pString VARCHAR(8000), #pDelimiter CHAR(1))
--WARNING!!! DO NOT USE MAX DATA-TYPES HERE! IT WILL KILL PERFORMANCE!
RETURNS TABLE WITH SCHEMABINDING AS
RETURN
--===== "Inline" CTE Driven "Tally Table" produces values from 1 up to 10,000...
-- enough to cover VARCHAR(8000)
WITH E1(N) AS (
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1
), --10E+1 or 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS (--==== This provides the "base" CTE and limits the number of rows for
-- both performance gain and prevention of accidental "overruns"
SELECT TOP (ISNULL(DATALENGTH(#pString),0)) ROW_NUMBER()
OVER (ORDER BY (SELECT NULL)) FROM E4
),
cteStart(N1) AS (--==== This returns N+1 (starting position of each "element" just
-- once for each delimiter)
SELECT 1 UNION ALL
SELECT t.N+1 FROM cteTally t WHERE SUBSTRING(#pString,t.N,1) = #pDelimiter
),
cteLen(N1,L1) AS(--==== Return start and length (for use in substring)
SELECT s.N1,
ISNULL(NULLIF(CHARINDEX(#pDelimiter,#pString,s.N1),0)-s.N1,8000)
FROM cteStart s
)
--===== Do the actual split. The ISNULL/NULLIF combo handles the length for the final
-- element when no delimiter is found.
SELECT ItemNumber = ROW_NUMBER() OVER(ORDER BY l.N1),
Item = SUBSTRING(#pString, l.N1, l.L1)
FROM cteLen l
;
go
Example usage from previous answer;
with data as (
select Code,Location,Quantity,Store from ( values
('L698-W-EA', NULL, 2, 'A')
,('L82009-EA', 'A1K2, A1N2, C4Y3, CBP2', 2, 'A')
,('L80401-A-EA', 'A1S2, SHIP, R2F1, CBP5, BRP, BRP1-20', 17,'A')
,('CWD2132W-BOX-25PK', 'A-AISLE', 1, 'M')
,('GM22660003-EA', 'B1K2', 1, 'M')
)data(Code,Location,Quantity,Store)
)
,shredded as (
select Code,Location,Quantity,Store,t.*
from data
cross apply [dbo].[DelimitedSplit8K](data.Location,',') as t
)
select
pvt.Code,pvt.Quantity,pvt.Store
,cast(isnull(pvt.[1],' ') as varchar(8)) as Loc1
,cast(isnull(pvt.[2],' ') as varchar(8)) as Loc2
,cast(isnull(pvt.[3],' ') as varchar(8)) as Loc3
,cast(isnull(pvt.[4],' ') as varchar(8)) as Loc4
,cast(isnull(pvt.[5],' ') as varchar(8)) as Loc5
,cast(isnull(pvt.[6],' ') as varchar(8)) as Loc6
from shredded
pivot (max(Item) for ItemNumber in ([1],[2],[3],[4],[5],[6])) pvt;
;
go
yielding:
Code Quantity Store Loc1 Loc2 Loc3 Loc4 Loc5 Loc6
----------------- ----------- ----- -------- -------- -------- -------- -------- --------
L698-W-EA 2 A
L82009-EA 2 A A1K2 A1N2 C4Y3 CBP2
L80401-A-EA 17 A A1S2 SHIP R2F1 CBP5 BRP BRP1-20
CWD2132W-BOX-25PK 1 M A-AISLE
GM22660003-EA 1 M B1K2