SQL View for top left and top right cell - sql

==> Referring to this Thread!
Referring to the output shown as best solution there, how can I get the boundary cells? That is min(StartX), min(StartY) and max(EndX) and max(EndY) OR in certain cases max(EndX+1) or max(EndY+1) if the column or row be missed out as in the case of 3,10 in the image below (green bordered are my bounding cells)
X Y PieceCells Boundary
1 1 (1,1)(2,1)(2,2)(3,2) (1,1)(3,2)
8 1 (10,1)(8,1)(8,2)(9,1)(9,2)(9,3) (8,1)(10,1)
Well I want like this:
BoundaryStartX, BoundaryStartY, BoundaryEndX, BoundaryEndY
1 1
3 2
8
1
10 3

I was able to do this pretty simply with the geometry data type.
declare #g geometry;
set #g = geometry::STGeomFromText(
'POLYGON( (1 -1, 1 -2, 2 -2, 2 -3, 4 -3, 4 -2, 3 -2, 3 -1, 1 -1) )'
, 0);
select #g, #g.STEnvelope();
Geometry is available starting in SQL2008. Also note that I converted your coordinate system to standard Cartesian (positive x axis to the right of the origin, negative y axis below); you'd do well to consider doing the same.

use tempdb;
if exists (select 1 from sys.tables where name = 'grid')
drop table grid;
if not exists (select 1 from sys.tables where name = 'tally')
begin
create table tally (i int not null);
with
a as (select 1 as [i] union select 0),
b as (select 1 as [i] from a as [a1] cross join a as [a2]),
c as (select 1 as [i] from b as [a1] cross join b as [a2]),
d as (select 1 as [i] from c as [a1] cross join c as [a2]),
e as (select 1 as [i] from d as [a1] cross join d as [a2])
insert into tally
select row_number() over (order by i) from e
create unique clustered index [CI_Tally] on tally (i)
end
create table grid (
x tinyint,
y tinyint,
cell as geometry::STGeomFromText(
'POLYGON( (' +
cast(x as varchar) + ' ' + cast(-1*y as varchar) + ', ' +
cast(x+1 as varchar) + ' ' + cast(-1*y as varchar) + ', ' +
cast(x+1 as varchar) + ' ' + cast(-1*(y+1) as varchar) + ', ' +
cast(x as varchar) + ' ' + cast(-1*(y+1) as varchar) + ', ' +
cast(x as varchar) + ' ' + cast(-1*y as varchar) +
') )'
, 0)
);
insert into grid (x, y)
values
(1,1),
(2,1),
(2,2),
(3,2),
(8,1),
(9,1),
(8,2),
(9,2),
(9,3),
(10,1);
with cte as (
select cell, row_number() over (order by x, y) as [rn]
from grid
),
cte2 as (
select cell, [rn]
from cte
where [rn] = 1
union all
select a.cell.STUnion(b.cell) as [cell], b.rn
from cte2 as a
inner join cte as b
on a.rn + 1 = b.[rn]
), cte3 as (
select cell
from cte2
where [rn] = (select count(*) from grid)
), clusters as (
select i, cell.STGeometryN(t.i) as c
from cte3 as [a]
cross join tally as [t]
where t.i <= cell.STNumGeometries()
)
select *, c.STEnvelope() from clusters
This solution solves both your original problem and this one. I like this because you can still use whatever weird coordinate system you want and it'll do what you want. All you'd have to do is modify the computed column on the grid table accordingly. I'm going to leave the computation of the corners of the envelope as an exercise to the reader. :)
By way of explanation, the computed column makes a 1x1 geometry instance out of the given x and y coordinates. From there, I essentially union all of those together which will yield a multipolygon. From there, I iterate through the individual polygons in the multipolygon to get the individual clusters. The envelope comes along for free. From here, you should be able to wrap that final select (or something very like it) in a view if you so choose.

Related

Reverse order of elements in a string

I have the following string:
1119/2/483/11021
I would like to reverse the order of the elements in that string. Desired output:
11021/483/2/1119
T-SQL Version 2014
You need an ordered split function, e.g. (inspiration):
CREATE FUNCTION dbo.SplitOrdered
(
#list nvarchar(max),
#delim nvarchar(10)
)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN
(
WITH w(n) AS (SELECT 0 FROM (VALUES (0),(0),(0),(0)) w(n)),
k(n) AS (SELECT 0 FROM w a, w b),
r(n) AS (SELECT 0 FROM k a, k b, k c, k d, k e, k f, k g, k h),
p(n) AS (SELECT TOP (COALESCE(LEN(#list), 0))
ROW_NUMBER() OVER (ORDER BY ##SPID) -1 FROM r),
spots(p) AS
(
SELECT n FROM p
WHERE (SUBSTRING(#list, n, LEN(#delim + 'x') - 1) LIKE #delim OR n = 0)
),
parts(p,val) AS
(
SELECT p, SUBSTRING(#list, p + LEN(#delim + 'x') - 1,
LEAD(p, 1, 2147483647) OVER (ORDER BY p) - p - LEN(#delim))
FROM spots AS s
)
SELECT listpos = ROW_NUMBER() OVER (ORDER BY p),
Item = LTRIM(RTRIM(val))
FROM parts
);
Then you can reassemble using STRING_AGG() (if SQL Server 2017 or better) or FOR XML PATH on lower versions:
SQL Server 2017 +
DECLARE #OriginalString nvarchar(255) = N'1119/2/483/11021';
SELECT NewString = STRING_AGG(o.Item, N'/')
WITHIN GROUP (ORDER BY listpos DESC)
FROM dbo.SplitOrdered(#OriginalString, N'/') AS o;
SQL Server < 2017
DECLARE #OriginalString nvarchar(255) = N'1119/2/483/11021';
SELECT NewString = STUFF(
(SELECT N'/' + o.Item
FROM dbo.SplitOrdered(#OriginalString, N'/') AS o
ORDER BY o.listpos DESC
FOR XML PATH(''), TYPE).value(N'./text()[1]', N'nvarchar(max)'),1,1,N'');
Example db<>fiddle
Please try the following solution based on the built-in PARSENAME() T-SQL function.
SQL
-- DDL and sample data population, start
DECLARE #tbl TABLE (ID INT IDENTITY PRIMARY KEY, Tokens VARCHAR(MAX));
INSERT INTO #tbl (Tokens) VALUES
('1119/2/483/11021'),
('1120/25/484/1102');
-- DDL and sample data population, end
SELECT tbl.*
, PARSENAME(c, 1) + '/' +
PARSENAME(c, 2) + '/' +
PARSENAME(c, 3) + '/' +
PARSENAME(c, 4) AS Result
FROM #tbl AS tbl
CROSS APPLY (VALUES (REPLACE(Tokens, '/', '.') )) AS t(c);
Output
+----+------------------+------------------+
| ID | Tokens | Result |
+----+------------------+------------------+
| 1 | 1119/2/483/11021 | 11021/483/2/1119 |
| 2 | 1120/25/484/1102 | 1102/484/25/1120 |
+----+------------------+------------------+
First, split the string and convert it into a column then order by desc and display into multiple row values into a single row. In the following code, you can set any string and split char.
Try following way.
DECLARE #S varchar(max) ,
#Split char(1),
#X xml
DECLARE #Names VARCHAR(8000)
SELECT #S = '1119/2/483/11021',
#Split = '/'
SELECT #X = CONVERT(xml,' <root> <myvalue>' +
REPLACE(#S,#Split,'</myvalue> <myvalue>') + '</myvalue> </root> ')
select #Names = COALESCE(#Names + '/', '') + Value from (
select rowno,Value from (
select ROW_NUMBER() OVER(ORDER BY d) AS rowno , Value from (
SELECT T.c.value('.','varchar(20)') as Value,0 as d
FROM #X.nodes('/root/myvalue') T(c)
) m
) r
) t order by t.rowno desc
select #Names as ReverseString
Splitting the string into sub-strings, and then joining them back up, is most likely going to be a good approach.
Some comments mention using string-reverse, but that doesnt seem to be a good approach at all in your case, since you just want to reverse the order of words within the current string, not actually reverse the entire text-string character-by-character.
PS: string_split does not guarantee the order of the chunks!

SQL split string (all possible combination)

I would like to transform this string:
A1+A2+A3.B1+B2.C1
into
A1.B1.C1
A1.B2.C1
A2.B1.C1
A2.B2.C1
A3.B1.C1
A3.B2.C1
How can I do that? (note that each dimension(= a group separate by .), could have x values, I mean it can be A1+A2.B1.C1 or A1+A2.B1+B2+B3+B4+B5.C1+C2)
Thanks
If you have only 3 columns, then just use STRING_SPLIT: number your groups from first split and then do a join 3 times and select each group on corresponding join.
with a as (
select s2.value as v, dense_rank() over(order by s1.value) as rn
from STRING_SPLIT('A1+A2+A3.B1+B2.C1', '.') as s1
cross apply STRING_SPLIT(s1.value, '+') as s2
)
select
a1.v + '.' + a2.v + '.' + a3.v as val
from a as a1
cross join a as a2
cross join a as a3
where a1.rn = 1
and a2.rn = 2
and a3.rn = 3
| val |
----------
|A1.B1.C1|
|A2.B1.C1|
|A3.B1.C1|
|A1.B2.C1|
|A2.B2.C1|
|A3.B2.C1|
If you have indefinite number of groups, then it's better to use recursive CTE instead of dynamic SQL. What you should do:
Start with all the values from the first group.
On recursion step crossjoin all the values of the next group (i.e. step group number is current group number + 1).
Select the last recursion step where you'll have the result.
Code is below:
with a as (
select s2.value as v, dense_rank() over(order by s1.value) as rn
from STRING_SPLIT('A1+A2+A3.B1+B2+B3+B4.C1+C2.D1+D2+D3', '.') as s1
cross apply STRING_SPLIT(s1.value, '+') as s2
)
, b (val, lvl) as (
/*Recursion base*/
select cast(v as nvarchar(1000)) as val, rn as lvl
from a
where rn = 1
union all
/*Increase concatenation on each iteration*/
select cast(concat(b.val, '.', a.v) as nvarchar(1000)) as val, b.lvl + 1 as lvl
from b
join a
on b.lvl + 1 = a.rn /*Recursion step*/
)
select *
from b
where lvl = (select max(rn) from a) /*You need the last step*/
order by val
I won't add a tabular result since it is quite big. But try it by yourself.
Here is SQL server version and fiddle:
with lst(s) as (select * from STRING_SPLIT('A1+A2.B1+B2+B3+B4+B5.C1+C2','.'))
select t1+'.'+t2+'.'+t3 as res from
(select * from STRING_SPLIT((select s from lst where s like 'A%'), '+')) s1(t1) cross join
(select * from STRING_SPLIT((select s from lst where s like 'B%'), '+')) s2(t2) cross join
(select * from STRING_SPLIT((select s from lst where s like 'C%'), '+')) s3(t3);
Of course you can grow it in a regular fashion if the number of dimensions grows.
Here is a Postgresql solution:
with x(s) as (select string_to_array('A1+A2.B1+B2+B3+B4+B5.C1+C2','.'))
select t1||'.'||t2||'.'||t3 as res from
unnest((select string_to_array(s[1],'+') from x)) t1 cross join
unnest((select string_to_array(s[2],'+') from x)) t2 cross join
unnest((select string_to_array(s[3],'+') from x)) t3;
result:
res |
--------|
A1.B1.C1|
A1.B2.C1|
A1.B3.C1|
A1.B4.C1|
A1.B5.C1|
A2.B1.C1|
A2.B2.C1|
A2.B3.C1|
A2.B4.C1|
A2.B5.C1|
A1.B1.C2|
A1.B2.C2|
A1.B3.C2|
A1.B4.C2|
A1.B5.C2|
A2.B1.C2|
A2.B2.C2|
A2.B3.C2|
A2.B4.C2|
A2.B5.C2|
Here my code with your help. I didn't mention, but I can also have more or less than 3 parts, so I'm using a dynamic SQL for this:
declare #FILTER varchar(max)='B+C+D.A+G.T+Y+R.E'
-- Works also with A.B.C
-- Works also with A+B+C.D.E+F
-- Works also with A+B+C.D+E+F+G+H
declare #NB int
declare #SQL varchar(max)=''
select #NB=count(*) from STRING_SPLIT(#FILTER,'.')
set #SQL='
;with T(A,B) as
(select *, row_number() over (order by (select NULL))
from STRING_SPLIT(''' + #FILTER + ''',''.'')
)
select '
;with T(V,N) as (
select *, row_number() over (order by (select NULL))
from STRING_SPLIT(#FILTER,'.')
)
select #SQL=#SQL + 'T' + cast(N as varchar(max)) + ' + ''.'' + ' from T
set #SQL=left(#SQL,len(#SQL)-1) + ' as res from'
;with T(V,N) as (
select *, row_number() over (order by (select NULL))
from STRING_SPLIT(#FILTER,'.')
)
select #SQL=#SQL + '
(select * from STRING_SPLIT((select A from T where B=' + cast(N as varchar(max)) + '), ''+'')) s' + cast(N as varchar(max)) + '(t' + cast(N as varchar(max)) + ') cross join'
from T
set #SQL=left(#SQL,len(#SQL)-len('cross join'))
exec(#SQL)

How can I retrieve first second and third word of a String in SQL?

I need a query which would extract the first second and third word of a string.
I have approximately 5 words in each row and I need only the first three words out of 5 in the same row (1 row). Example "ATV BDSG 232 continue with other words".
I need only the first three words together in one row (in the same row) like "ATV BDSG 232" as a first row. The table has about 1000 rows and at the end of it I should have 1000 rows again but each row should contain only the first three words of the string.
I found a query which works fine for extracting first two like "ATV BDSG" discussed in stack overflow. The query is
"SELECT SUBSTRING(field1, 0, CHARINDEX(' ', field1, CHARINDEX(' ', field1, 0)+1))
FROM Table"
Can we derive this for extracting first three words?
Thanks in advance
If you don't want to create a dedicated function, you can use successive CROSS APPLYs:
SELECT
T.s,
FirstSpace.i,
SecondSpace.j,
ThirdSpace.k,
CASE
When ThirdSpace.k > 0 THEN LEFT(T.s, Thirdspace.k - 1)
ELSE T.S
END AS Phrase
FROM t
CROSS APPLY (SELECT CHARINDEX(' ', T.s, 1)) AS FirstSpace(i)
CROSS APPLY (SELECT CHARINDEX(' ', T.S, FirstSpace.i + 1)) AS SecondSpace(j)
CROSS APPLY (SELECT CHARINDEX(' ', T.s, SecondSpace.j + 1)) AS ThirdSpace(k)
gives you the results you need:
| s | i | j | k | phrase |
|----------------------------------------|---|---|----|------------------|
| ATV BDSG 232 Continue with other words | 4 | 9 | 13 | ATV BDSG 232 |
Things are easy, SQL Server provide STRING_SPLIT() function make that too easy
DECLARE #Var VARCHAR(100) = 'ATV BDSG 232 Continue with other words';
SELECT Word
FROM
(
SELECT Value AS Word,
ROW_NUMBER()OVER(ORDER BY (SELECT NULL)) RN
FROM STRING_SPLIT(#Var, ' ')
) T
WHERE RN <= 3;
But since you are working on 2012 version, you need to define your own function.
You can also take the hard way, first you need to get the first word, then replace it with '' and get the second word, then do the same for the 3rd word as
DECLARE #Var VARCHAR(100) = 'ATV BDSG 232 Continue with other words';
WITH FW AS
(
SELECT LEFT(#Var, CHARINDEX(' ', #Var)) FirstWord
),
SW AS
(
SELECT LEFT(REPLACE(#Var, FirstWord, ''),
CHARINDEX(' ', REPLACE(#Var, FirstWord, ''))) SecondWord
FROM FW
)
SELECT FirstWord,
SecondWord,
LEFT(REPLACE(REPLACE(V, FirstWord, ''), SecondWord, ''),
CHARINDEX(' ', REPLACE(REPLACE(V, FirstWord, ''), SecondWord, ''))
) ThirdWord
FROM
(
SELECT *, #Var V
FROM FW CROSS APPLY SW
) T
Demo
UPDATE
If you want to select the three first words then simply
SELECT SUBSTRING(Str, 0, CHARINDEX(' ', Str, CHARINDEX(' ', Str, CHARINDEX(' ', Str, 0)+1)+1)) Words
FROM Strings
Demo
--make some test data
declare #test as nvarchar(100) = 'my test string for words';
select 1 id, cast('my test string for words' as nvarchar(max)) word into #test;
insert #test (id,word) values (2,'a b c d e f g hhh yyyyyy') ;
insert #test (id,word) values (3,' a required test string d e f g hhh yyyyyy') ;
insert #test (id,word) values (4,'a quick test') ;
insert #test (id,word) values (5,'a test') ;
insert #test (id,word) values (6,'last') ;
--break up letters, count the first 3 words
;WITH CTE AS (SELECT 1 x, substring(#test,1,1) charx
UNION ALL
SELECT X + 1, substring(#test,x + 1,1) from CTE WHERE x < len(#test)
)
select * from cte c3 where (SELECT count(0) cnt FROM CTE c1 JOIN CTE c2 on c1.x <= c3.x and c1.x + 1 = c2.x and c1.charx =' ' and c2.charx != ' ') < 3
;WITH tabx as (select id, cast(ltrim(word) as nvarchar(max)) 'word' from #test), --do some ltrim
CTE AS (
SELECT id, 1 x, substring(word,1,1) charx from tabx
UNION ALL
SELECT t.id, c.X + 1, substring(t.word,x + 1,1)
from tabx t
JOIN CTE c on c.id = t.id and x < len(t.word)
),
disj as
(select * from cte c3 where
(SELECT count(0) cnt
FROM CTE c1
JOIN CTE c2 on c1.id = c3.id and c1.id = c2.id and c1.x <= c3.x and c1.x + 1 = c2.x and c1.charx =' ' and c2.charx != ' '
) < 3
),
rj as
(select disj.id,disj.x, disj.charx z
from disj
where disj.x = 1
UNION ALL
select d.id, d.x, r.z + d.charx
FROM rj r
join disj d on r.id = d.id and r.x + 1 = d.x
)
select *
from rj r1
cross apply (select max(r2.x) TheRow from rj r2 where r1.id = r2.id) dq
where r1.x = dq.TheRow
order by r1.id;
--delete test data
drop table #test
/* This is not perfect - but interesting */
declare #t table (fullname varchar(100))
insert #t values('Mr Jones'),('Mrs Amy smith'),('Jim Smith'),('Dr Harry Web '),('Paul Fred andrew jones')
select fullname,
a.value as a ,
b.Value as b,
c.Value as c,
d.Value as d,
e.Value as e,
f.value as f
from #t
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ')) a
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value )) b
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value ) ) c
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value,c.value )) d
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value,c.value,d.value) ) e
outer apply (select top 1 value from STRING_SPLIT(fullname, ' ') where value not in (a.value,b.value ,c.value,d.value,e.value) ) f
To Select First Word -
Select top 1 Ltrim(Rtrim(value)) FROM STRING_SPLIT(#input,' ')
To Select Only Second Word -
Select Ltrim(Rtrim(value)) from STRING_SPLIT(#input,' ') Order by (Select NULL) OFFSET 1 ROWS FETCH NEXT 1 ROWS ONLY

Split a string with no delimiters into columns

I need to split a string in a column into one character each into it's own column in SQL Server 2012.
Example: if I have a column with 'ABCDE', I need to split it into 'A', 'B', 'C', 'D', 'E', with each of these into their own columns.
The length of the column to be split may vary, so I need this to be as dynamic as possible.
My question is different from the other post (Can Mysql Split a column?) since mine doesn't have any delimiters.
Thanks
You can do this like this:
DECLARE #t TABLE(id int, n VARCHAR(50))
INSERT INTO #t VALUES
(1, 'ABCDEF'),
(2, 'EFGHIJKLMNOPQ')
;WITH cte AS
(SELECT id, n, SUBSTRING(n, 1, 1) c, 1 AS ind FROM #t
UNION ALL
SELECT id, n, SUBSTRING(n, ind + 1, 1), ind + 1 FROM cte WHERE LEN(n) > ind
)
SELECT *
FROM cte
PIVOT (MAX(c) FOR ind IN([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[12],[13],[14],[15])) p
Output:
id n 1 2 3 4 5 6 7 8 9 10 12 13 14 15
1 ABCDEF A B C D E F NULL NULL NULL NULL NULL NULL NULL NULL
2 EFGHIJKLMNOPQ E F G H I J K L M N P Q NULL NULL
Here is dynamic version:
DECLARE #l INT, #c VARCHAR(MAX) = ''
SELECT #l = MAX(LEN(n)) FROM PivotTable
WHILE #l > 0
BEGIN
SET #c = ',[' + CAST(#l AS VARCHAR(MAX)) + ']' + #c
SET #l = #l - 1
END
SET #c = STUFF(#c, 1, 1,'')
DECLARE #s NVARCHAR(MAX) = '
;WITH cte AS
(SELECT id, n, SUBSTRING(n, 1, 1) c, 1 AS ind FROM PivotTable
UNION ALL
SELECT id, n, SUBSTRING(n, ind + 1, 1), ind + 1 FROM cte WHERE LEN(n) > ind
)
SELECT *
FROM cte
PIVOT (MAX(c) FOR ind IN(' + #c + ')) p'
EXEC (#s)
I am interpreting the question as putting the characters into one column ("split a string in a column into one character each into it's own column"). However, I realize that this might be ambiguous.
One method is with a recursive CTE:
with chars as (
select left(val, 1) as c, substring(val, 2, len(val)) as rest
from (select 'ABCDE' as val union all select '123') t
union all
select left(rest, 1), substring(rest, 2, len(rest))
from chars
where rest <> ''
)
select c
from chars;
Just plug in your table and column in the subquery. Note that you might want to include other columns as well.
Here is a SQL Fiddle.
If you want multiple columns and the number is not fixed, then you will need
dynamic SQL.
If you want a new column for every character you simply need:
SELECT [1] = SUBSTRING(Col, 1, 1),
[2] = SUBSTRING(Col, 2, 1),
[3] = SUBSTRING(Col, 3, 1),
[4] = SUBSTRING(Col, 4, 1),
[5] = SUBSTRING(Col, 5, 1),
[6] = SUBSTRING(Col, 6, 1),
[7] = SUBSTRING(Col, 7, 1),
[8] = SUBSTRING(Col, 8, 1),
[9] = SUBSTRING(Col, 9, 1)
FROM (VALUES ('ABCDE'), ('FGHIJKLMN')) t (Col);
Which is fine, if you have a know number of columns. If you have an unknown number of columns, then you just need to generate the same SQL with n columns. To do this you will need a numbers table, and since many people do not have one, I will do a quick demo on how to dynamically generate one.
The below will generate a sequential list of numbers, 1 - 100,000,000.
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N3 AS N1 CROSS JOIN N3 AS N2)
SELECT Number
FROM Numbers;
It simply uses a table valued constructor to generate 10 rows (N1), then cross joins these 10 rows to get 100 rows (N2), then cross joins these 100 rows to get 10,000 rows (N3) and so on and so on. It finally uses ROW_NUMBER() to get the sequential numbers.
This probably needs to be cut down for this use, I hope you are not splitting a string that is 100,000,000 characters long, but the principle applies. You can just use TOP and the maximum length of your string to limit it. For each number you can just build up the necessary repetetive SQL required, which is:
,[n] = SUBSTRING(Col, n, 1)
So you have something like:
SELECT Number,
[SQL] = ',[' + CAST(Number AS VARCHAR(10)) + '] = SUBSTRING(Col, ' + CAST(Number AS VARCHAR(10)) + ', 1)'
FROM Numbers;
Which gives something like:
Number SQL
-----------------------------------
1 ,[1] = SUBSTRING(Col, 1, 1)
2 ,[2] = SUBSTRING(Col, 2, 1)
3 ,[3] = SUBSTRING(Col, 3, 1)
4 ,[4] = SUBSTRING(Col, 4, 1)
The final step is to build up your final statement by concatenating all the text in the column SQL; the best way to do this is using SQL Server's XML Extensions.
So your final query might end up like:
DECLARE #SQL NVARCHAR(MAX) = '';
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL DROP TABLE #T;
CREATE TABLE #T (Col VARCHAR(100));
INSERT #T (Col) VALUES ('ABCDE'), ('FGHIJKLMN');
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT ROW_NUMBER() OVER(ORDER BY N1.N) FROM N3 AS N1 CROSS JOIN N3 AS N2)
SELECT #SQL = 'SELECT Col' +
( SELECT TOP (SELECT MAX(LEN(Col)) FROM #T)
',[' + CAST(Number AS VARCHAR(10)) + '] = SUBSTRING(Col, ' + CAST(Number AS VARCHAR(10)) + ', 1)'
FROM Numbers
FOR XML PATH(''), TYPE
).value('.', 'VARCHAR(MAX)') + '
FROM #T;';
EXECUTE sp_executesql #SQL;
Which gives:
Col 1 2 3 4 5 6 7 8 9
-------------------------------------------------
ABCDE A B C D E
FGHIJKLMN F G H I J K L M N
Finally, if you actually wanted to split it into rows, I would still use the same approach, with your adhoc numbers table, just join it to your original table:
IF OBJECT_ID(N'tempdb..#T', 'U') IS NOT NULL DROP TABLE #T;
CREATE TABLE #T (Col VARCHAR(100));
INSERT #T (Col) VALUES ('ABCDE'), ('FGHIJKLMN');
WITH N1 AS (SELECT N FROM (VALUES (1),(1),(1),(1),(1),(1),(1),(1),(1),(1)) n (N)),
N2 (N) AS (SELECT 1 FROM N1 AS N1 CROSS JOIN N1 AS N2),
N3 (N) AS (SELECT 1 FROM N2 AS N1 CROSS JOIN N2 AS N2),
Numbers (Number) AS (SELECT TOP (SELECT MAX(LEN(Col)) FROM #T) ROW_NUMBER() OVER(ORDER BY N1.N) FROM N3 AS N1 CROSS JOIN N3 AS N2)
SELECT t.Col,
Position = n.Number,
Character = SUBSTRING(t.Col, n.Number, 1)
FROM #T AS t
INNER JOIN Numbers AS n
ON n.Number <= LEN(t.Col)
ORDER BY t.Col, n.Number;
Which gives something like:
Col Position Character
-------------------------------
ABCDE 1 A
ABCDE 2 B
ABCDE 3 C
ABCDE 4 D
ABCDE 5 E
One way
declare #str varchar(max) = 'ABCDE'
declare #sql nvarchar(max) = ''
declare #i int = 1
while (#i <= len(#str)) begin
set #sql += case when #i > 1 then ',' else '' end + '''' + substring(#str, #i, 1) + ''''
set #i += 1
end
exec('select ' + #sql)
(If ' can appear as a char you would need to substitute '')
This is a solution for a dynamic text length.
-- Generate demo data
CREATE TABLE #temp(col nvarchar(100))
INSERT INTO #temp(col)
VALUES(N'A'),(N'ABC'),(N'DEFGHI'),(N'AB'),(N'KLOMA')
-- Split all in multiple rows
CREATE TABLE #output (col nvarchar(100),part nchar(1), pos int)
;WITH cte AS(
SELECT col, LEFT(col, 1) as part, 1 as pos
FROM #temp
UNION ALL
SELECT col, SUBSTRING(col, pos+1,1) as part, pos+1 as part
FROM cte
WHERE LEN(col) > pos
)
INSERT INTO #output(col, part, pos)
SELECT col, part, pos
FROM cte
DECLARE #sql nvarchar(max), #columnlist nvarchar(max)
-- Generate Columlist for dynamic pivot
SELECT #columnlist = COALESCE(#columnlist + N',[' + CONVERT(nvarchar(max),pos) + ']', N'[' + CONVERT(nvarchar(max),pos) + ']')
FROM #output o
WHERE o.col = (SELECT TOP (1) col FROM #output ORDER BY LEN(col) DESC)
-- Pivoting for readability
SET #sql = N'
SELECT pvt.*
FROM #output o
PIVOT (
MAX(o.part)
FOR pos IN('+#columnlist+')
) as pvt'
EXEC (#sql)
-- Cleanup
DROP TABLE #temp
DROP TABLE #output
The keypart is the cte and the pivoting afterwards. If you have any questions, just give me a short feedback.

replace value in varchar(max) field with join

I have a table that contains text field with placeholders. Something like this:
Row Notes
1. This is some notes ##placeholder130## this ##myPlaceholder##, #oneMore#. End.
2. Second row...just a ##test#.
(This table contains about 1-5k rows on average. Average number of placeholders in one row is 5-15).
Now, I have a lookup table that looks like this:
Name Value
placeholder130 Dog
myPlaceholder Cat
oneMore Cow
test Horse
(Lookup table will contain anywhere from 10k to 100k records)
I need to find the fastest way to join those placeholders from strings to a lookup table and replace with value. So, my result should look like this (1st row):
This is some notes Dog this Cat, Cow. End.
What I came up with was to split each row into multiple for each placeholder and then join it to lookup table and then concat records back to original row with new values, but it takes around 10-30 seconds on average.
You could try to split the string using a numbers table and rebuild it with for xml path.
select (
select coalesce(L.Value, T.Value)
from Numbers as N
cross apply (select substring(Notes.notes, N.Number, charindex('##', Notes.notes + '##', N.Number) - N.Number)) as T(Value)
left outer join Lookup as L
on L.Name = T.Value
where N.Number <= len(notes) and
substring('##' + notes, Number, 2) = '##'
order by N.Number
for xml path(''), type
).value('text()[1]', 'varchar(max)')
from Notes
SQL Fiddle
I borrowed the string splitting from this blog post by Aaron Bertrand
SQL Server is not very fast with string manipulation, so this is probably best done client-side. Have the client load the entire lookup table, and replace the notes as they arrived.
Having said that, it can of course be done in SQL. Here's a solution with a recursive CTE. It performs one lookup per recursion step:
; with Repl as
(
select row_number() over (order by l.name) rn
, Name
, Value
from Lookup l
)
, Recurse as
(
select Notes
, 0 as rn
from Notes
union all
select replace(Notes, '##' + l.name + '##', l.value)
, r.rn + 1
from Recurse r
join Repl l
on l.rn = r.rn + 1
)
select *
from Recurse
where rn =
(
select count(*)
from Lookup
)
option (maxrecursion 0)
Example at SQL Fiddle.
Another option is a while loop to keep replacing lookups until no more are found:
declare #notes table (notes varchar(max))
insert #notes
select Notes
from Notes
while 1=1
begin
update n
set Notes = replace(n.Notes, '##' + l.name + '##', l.value)
from #notes n
outer apply
(
select top 1 Name
, Value
from Lookup l
where n.Notes like '%##' + l.name + '##%'
) l
where l.name is not null
if ##rowcount = 0
break
end
select *
from #notes
Example at SQL Fiddle.
I second the comment that tsql is just not suited for this operation, but if you must do it in the db here is an example using a function to manage the multiple replace statements.
Since you have a relatively small number of tokens in each note (5-15) and a very large number of tokens (10k-100k) my function first extracts tokens from the input as potential tokens and uses that set to join to your lookup (dbo.Token below). It was far too much work to look for an occurrence of any of your tokens in each note.
I did a bit of perf testing using 50k tokens and 5k notes and this function runs really well, completing in <2 seconds (on my laptop). Please report back how this strategy performs for you.
note: In your example data the token format was not consistent (##_#, ##_##, #_#), I am guessing this was simply a typo and assume all tokens take the form of ##TokenName##.
--setup
if object_id('dbo.[Lookup]') is not null
drop table dbo.[Lookup];
go
if object_id('dbo.fn_ReplaceLookups') is not null
drop function dbo.fn_ReplaceLookups;
go
create table dbo.[Lookup] (LookupName varchar(100) primary key, LookupValue varchar(100));
insert into dbo.[Lookup]
select '##placeholder130##','Dog' union all
select '##myPlaceholder##','Cat' union all
select '##oneMore##','Cow' union all
select '##test##','Horse';
go
create function [dbo].[fn_ReplaceLookups](#input varchar(max))
returns varchar(max)
as
begin
declare #xml xml;
select #xml = cast(('<r><i>'+replace(#input,'##' ,'</i><i>')+'</i></r>') as xml);
--extract the potential tokens
declare #LookupsInString table (LookupName varchar(100) primary key);
insert into #LookupsInString
select distinct '##'+v+'##'
from ( select [v] = r.n.value('(./text())[1]', 'varchar(100)'),
[r] = row_number() over (order by n)
from #xml.nodes('r/i') r(n)
)d(v,r)
where r%2=0;
--tokenize the input
select #input = replace(#input, l.LookupName, l.LookupValue)
from dbo.[Lookup] l
join #LookupsInString lis on
l.LookupName = lis.LookupName;
return #input;
end
go
return
--usage
declare #Notes table ([Id] int primary key, notes varchar(100));
insert into #Notes
select 1, 'This is some notes ##placeholder130## this ##myPlaceholder##, ##oneMore##. End.' union all
select 2, 'Second row...just a ##test##.';
select *,
dbo.fn_ReplaceLookups(notes)
from #Notes;
Returns:
Tokenized
--------------------------------------------------------
This is some notes Dog this Cat, Cow. End.
Second row...just a Horse.
Try this
;WITH CTE (org, calc, [Notes], [level]) AS
(
SELECT [Notes], [Notes], CONVERT(varchar(MAX),[Notes]), 0 FROM PlaceholderTable
UNION ALL
SELECT CTE.org, CTE.[Notes],
CONVERT(varchar(MAX), REPLACE(CTE.[Notes],'##' + T.[Name] + '##', T.[Value])), CTE.[level] + 1
FROM CTE
INNER JOIN LookupTable T ON CTE.[Notes] LIKE '%##' + T.[Name] + '##%'
)
SELECT DISTINCT org, [Notes], level FROM CTE
WHERE [level] = (SELECT MAX(level) FROM CTE c WHERE CTE.org = c.org)
SQL FIDDLE DEMO
Check the below devioblog post for reference
devioblog post
To get speed, you can preprocess the note templates into a more efficient form. This will be a sequence of fragments, with each ending in a substitution. The substitution might be NULL for the last fragment.
Notes
Id FragSeq Text SubsId
1 1 'This is some notes ' 1
1 2 ' this ' 2
1 3 ', ' 3
1 4 '. End.' null
2 1 'Second row...just a ' 4
2 2 '.' null
Subs
Id Name Value
1 'placeholder130' 'Dog'
2 'myPlaceholder' 'Cat'
3 'oneMore' 'Cow'
4 'test' 'Horse'
Now we can do the substitutions with a simple join.
SELECT Notes.Text + COALESCE(Subs.Value, '')
FROM Notes LEFT JOIN Subs
ON SubsId = Subs.Id WHERE Notes.Id = ?
ORDER BY FragSeq
This produces a list of fragments with substitutions complete. I am not an MSQL user, but in most dialects of SQL you can concatenate these fragments in a variable quite easily:
DECLARE #Note VARCHAR(8000)
SELECT #Note = COALESCE(#Note, '') + Notes.Text + COALSCE(Subs.Value, '')
FROM Notes LEFT JOIN Subs
ON SubsId = Subs.Id WHERE Notes.Id = ?
ORDER BY FragSeq
Pre-processing a note template into fragments will be straightforward using the string splitting techniques of other posts.
Unfortunately I'm not at a location where I can test this, but it ought to work fine.
I really don't know how it will perform with 10k+ of lookups.
how does the old dynamic SQL performs?
DECLARE #sqlCommand NVARCHAR(MAX)
SELECT #sqlCommand = N'PlaceholderTable.[Notes]'
SELECT #sqlCommand = 'REPLACE( ' + #sqlCommand +
', ''##' + LookupTable.[Name] + '##'', ''' +
LookupTable.[Value] + ''')'
FROM LookupTable
SELECT #sqlCommand = 'SELECT *, ' + #sqlCommand + ' FROM PlaceholderTable'
EXECUTE sp_executesql #sqlCommand
Fiddle demo
And now for some recursive CTE.
If your indexes are correctly set up, this one should be very fast or very slow. SQL Server always surprises me with performance extremes when it comes to the r-CTE...
;WITH T AS (
SELECT
Row,
StartIdx = 1, -- 1 as first starting index
EndIdx = CAST(patindex('%##%', Notes) as int), -- first ending index
Result = substring(Notes, 1, patindex('%##%', Notes) - 1)
-- (first) temp result bounded by indexes
FROM PlaceholderTable -- **this is your source table**
UNION ALL
SELECT
pt.Row,
StartIdx = newstartidx, -- starting index (calculated in calc1)
EndIdx = EndIdx + CAST(newendidx as int) + 1, -- ending index (calculated in calc4 + total offset)
Result = Result + CAST(ISNULL(newtokensub, newtoken) as nvarchar(max))
-- temp result taken from subquery or original
FROM
T
JOIN PlaceholderTable pt -- **this is your source table**
ON pt.Row = T.Row
CROSS APPLY(
SELECT newstartidx = EndIdx + 2 -- new starting index moved by 2 from last end ('##')
) calc1
CROSS APPLY(
SELECT newtxt = substring(pt.Notes, newstartidx, len(pt.Notes))
-- current piece of txt we work on
) calc2
CROSS APPLY(
SELECT patidx = patindex('%##%', newtxt) -- current index of '##'
) calc3
CROSS APPLY(
SELECT newendidx = CASE
WHEN patidx = 0 THEN len(newtxt) + 1
ELSE patidx END -- if last piece of txt, end with its length
) calc4
CROSS APPLY(
SELECT newtoken = substring(pt.Notes, newstartidx, newendidx - 1)
-- get the new token
) calc5
OUTER APPLY(
SELECT newtokensub = Value
FROM LookupTable
WHERE Name = newtoken -- substitute the token if you can find it in **your lookup table**
) calc6
WHERE newstartidx + len(newtxt) - 1 <= len(pt.Notes)
-- do this while {new starting index} + {length of txt we work on} exceeds total length
)
,lastProcessed AS (
SELECT
Row,
Result,
rn = row_number() over(partition by Row order by StartIdx desc)
FROM T
) -- enumerate all (including intermediate) results
SELECT *
FROM lastProcessed
WHERE rn = 1 -- filter out intermediate results (display only last ones)