List combination of numbers which have same sum - sql

**N** is a prositive number
Need list of scenarios which have sum equal N
For example if N=4
ScenarioId Value
---------- -----
1 1
1 1
1 1
1 1
2 2
2 1
2 1
3 2
3 2
4 3
4 1
5 4
above list is required. If you sum by ScenarioId all sum must equal to N
UPDATE
Here is my own solution. however, I am not sure about the multiplication of two different number sets would not be equal at any time.
My current question is
Is there any possibilities a + b + c = d + e + f and a * b * c = d * e * f
Test link is here
DECLARE #N int = 4;
SELECT
[Value] = CAST(number + 1 as tinyint)
INTO #Values
FROM master.dbo.spt_values
WHERE number < #N
AND [Type] = 'p'
;WITH COMBINATIONS AS(
SELECT ScenarioKey = CAST(NULL AS nvarchar(MAX)), [Value], Total = 0, Multipication = 1, MemeberCount = 0
FROM #Values
UNION ALL
SELECT ScenarioKey = ISNULL(S.ScenarioKey, '') + IIF(S.ScenarioKey IS NULL, '', N'-') + CAST(P.[Value] AS nvarchar(10)), S.[Value], Total = S.Total + P.[Value], Multipication = S.Multipication * P.[Value], MemeberCount = MemeberCount + 1
FROM #Values P
JOIN COMBINATIONS AS S ON S.Total < S.[Value]
),
SCENARIOS AS(
SELECT
ScenarioKey
,ScenarioId = ROW_NUMBER() OVER(ORDER BY ScenarioKey)
,[Value]
FROM
(
SELECT
ScenarioKey
,[Value]
,Multipication
,MemeberCount
-- this will prevent dublications. because 1 * 2 * 3 = 3 * 2 * 1
-- however, I am not sure about multipication of two different number sets would not be equal any time
,RowNo = ROW_NUMBER() OVER(PARTITION BY [Value],Multipication,MemeberCount ORDER BY [Value],ScenarioKey)
FROM COMBINATIONS
WHERE Total = #N
) X
WHERE RowNo = 1 AND [Value] = #N
)
SELECT
R.ScenarioId
,[Value] = S.[value]
FROM SCENARIOS R
CROSS APPLY (SELECT [value] FROM STRING_SPLIT(R.ScenarioKey, '-')) S
DROP TABLE #Values

It's too long for comment, so I post this as an answer. I want to note, that this is a static example, but I hope it can be easily translated as a dynamic statement.
Steps are written as comments in the statement:
WITH rcte AS
(
-- Recursive query to generate all numbers from 1 to 4
SELECT 0 AS Number
UNION ALL
SELECT Number + 1
FROM rcte
WHERE Number < 4
), permutations AS (
-- All possible permutations with sum equal to 4
-- There is additional column DuplicateMarker.
-- It will be used later, because 0,0,0,4 and 0,4,0,0 are the same
SELECT
t1.Number AS Number1,
t2.Number AS Number2,
t3.Number AS Number3,
t4.Number AS Number4,
CONCAT(LTRIM(STR(t1.Number)), '.', LTRIM(STR(t2.Number)), '.', LTRIM(STR(t3.Number)), '.', LTRIM(STR(t4.Number))) AS DuplicateMarker
FROM rcte t1, rcte t2, rcte t3, rcte t4
WHERE (t1.Number + t2.Number + t3.Number + t4.Number) = 4
), duplicates AS (
-- Get data with splitted DuplicateMarker column
SELECT *
FROM permutations
CROSS APPLY (SELECT [value] FROM STRING_SPLIT(DuplicateMarker, '.')) t
), results AS (
-- Get unique combinations
-- WITHIN GROUP (ORDER BY) will order strings and 0.0.0.4 and 0.4.0.0 will be the same
SELECT DISTINCT STRING_AGG([value], '.') WITHIN GROUP (ORDER BY [value]) AS ScenarioValue
FROM duplicates
GROUP BY Number1, Number2, Number3, Number4
)
SELECT
DENSE_RANK() OVER (ORDER BY r.ScenarioValue) AS ScenarioID,
s.[value]
FROM results r
CROSS APPLY (SELECT [value] FROM STRING_SPLIT(r.ScenarioValue, '.')) s
WHERE [value] <> '0'
Output:
ScenarioID value
1 4
2 1
2 3
3 2
3 2
4 1
4 1
4 2
5 1
5 1
5 1
5 1
Update:
Thanks to #AndriyM's comment, I've made some changes and now you can eliminate string manipulations:
WITH rcte AS
(
-- Recursive query to generate all numbers from 0 to 4
SELECT 0 AS Number
UNION ALL
SELECT Number + 1
FROM rcte
WHERE Number < 4
), combinations AS (
-- All different combinations with sum equal to 4
SELECT
t1.Number AS Number1,
t2.Number AS Number2,
t3.Number AS Number3,
t4.Number AS Number4,
ROW_NUMBER() OVER (ORDER BY t1.Number, t2.Number, t3.Number, t4.NUmber) AS ScenarioID
FROM rcte t1, rcte t2, rcte t3, rcte t4
WHERE
((t1.Number + t2.Number + t3.Number + t4.Number) = 4) AND
(t1.Number <= t2.Number) AND
(t2.Number <= t3.Number) AND
(t3.Number <= t4.Number)
)
SELECT c.ScenarioID, v.[value]
FROM combinations c
CROSS APPLY (VALUES (c.NUmber1), (c.Number2), (c.Number3), (c.Number4)) AS v ([value])
WHERE v.[value] > 0
Update 2:
Approach using dynamic statement - probably not the best approach, but is based on statement from first update:
-- Set your #n value
DECLARE #n int
SET #n = 4
-- Declarations
DECLARE #combinationsSelect nvarchar(max)
DECLARE #combinationsRowNumber nvarchar(max)
DECLARE #combinationsFrom nvarchar(max)
DECLARE #combinationsWhere1 nvarchar(max)
DECLARE #combinationsWhere2 nvarchar(max)
DECLARE #combinationsValues nvarchar(max)
SET #combinationsSelect = N''
SET #combinationsRowNumber = N''
SET #combinationsFrom = N''
SET #combinationsValues = N''
SET #combinationsWhere1 = N''
SET #combinationsWhere2 = N''
-- Generate dynamic parts of the statement
;WITH numbers AS
(
SELECT 1 AS Number
UNION ALL
SELECT Number + 1
FROM Numbers
WHERE Number < #n
)
SELECT
#combinationsSelect = #combinationsSelect + N', t' + LTRIM(STR(Number)) + N'.Number AS Number' + LTRIM(STR(Number)),
#combinationsRowNumber = #combinationsRowNumber + N', t' + LTRIM(STR(Number)) + N'.Number',
#combinationsValues = #combinationsValues + N', (c.Number' + LTRIM(STR(Number)) + N')',
#combinationsFrom = #combinationsFrom + N', rcte t' + LTRIM(STR(Number)),
#combinationsWhere1 = #combinationsWhere1 + N'+ t' + LTRIM(STR(Number)) + N'.Number ',
#combinationsWhere2 = #combinationsWhere2 +
CASE
WHEN Number = 1 THEN N''
ELSE N'AND (t' + LTRIM(STR(Number-1)) + N'.Number <= t' + + LTRIM(STR(Number)) + N'.Number) '
END
FROM
numbers
SET #combinationsSelect = STUFF(#combinationsSelect, 1, 2, N'')
SET #combinationsRowNumber = STUFF(#combinationsRowNumber, 1, 2, N'')
SET #combinationsValues = STUFF(#combinationsValues, 1, 2, N'')
SET #combinationsFrom = STUFF(#combinationsFrom, 1, 2, N'')
SET #combinationsWhere1 = STUFF(#combinationsWhere1, 1, 2, N'')
SET #combinationsWhere2 = STUFF(#combinationsWhere2, 1, 4, N'')
-- Dynamic statement
DECLARE #stm nvarchar(max)
SET #stm =
N'WITH rcte AS (
SELECT 0 AS Number
UNION ALL
SELECT Number + 1
FROM rcte
WHERE Number < ' + LTRIM(STR(#n)) +
N'), combinations AS (
SELECT ' +
#combinationsSelect +
N', ROW_NUMBER() OVER (ORDER BY ' + #combinationsRowNumber + N') AS ScenarioID
FROM ' + #combinationsFrom +
N' WHERE ((' + #combinationsWhere1 + N') = ' + LTRIM(STR(#n)) + ') AND ' + #combinationsWhere2 +
N')
SELECT c.ScenarioID, v.[value]
FROM combinations c
CROSS APPLY (VALUES ' + #combinationsValues + N') AS v ([value])
WHERE v.[value] > 0'
-- Execute dynamic statement
EXEC (#stm)

If you have sample data like below
You can write query like below
Declare #N int =4
Select T.*
From #T T
cross apply (
select S, SUM(V) Total
From #T
Group By S) Totals
Where Totals.Total=#N and T.S = Totals.S

Related

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)

SQL Every combination of ID's

Quite a tricky scenario. I have a table as below. Basically I want to get all combinations of ranges from each RangeSet in SQL Server 2012.
Best I show an example of structure and desired output. The problem is the number of RangeSetID's can be dynamic and the number of RangeID's can be dynamic in each range set
RangeID RangeSetID
------------------
1 4
2 4
3 4
4 4
5 2
6 2
7 2
8 2
9 2
10 2
11 1
12 1
13 1
14 1
15 1
16 1
17 3
18 3
19 3
20 3
I need the output to recursively create the below dataset of rates:
1 5 11 17 (first from range4, first from range2, first from range1, first from range3)
1 5 11 18 (first from range4, first from range2, first from range1, second from range3)
1 5 11 19 (first from range4, first from range2, first from range1, third from range3)
1 5 11 20 (first from range4, first from range2, first from range1, fourth from range3)
1 5 12 17 (first from range4, first from range2, second from range1, first from range3)
1 5 12 18 (first from range4, first from range2, second from range1, second from range3)
1 5 12 19
1 5 12 20
And so on until I reach the last RangeID from each RangeSetID and result in
4 10 16 20 (last from range4, last from range2, last from range1, last from range3)
Which will ultimately result in the below where RateID 1 is showing the first result vertically to allow for the dynamic number of RangeSetID's
RateID RangeID
------------------
1 1
1 5
1 11
1 17
2 1
2 5
2 11
2 18
This should result in 11,000 rows (approx). I have tried CROSS JOIN's etc but I cannot get this working at all.
Any geniuses out there please?
Thanks
Guess this should help. Happy coding!
;WITH CTE
AS
(
SELECT * FROM (
SELECT ROW_NUMBER() over (order by [RangeID1] , [RangeID2], [RangeID3], [RangeID4]) as 'RateID', [RangeID1] , [RangeID2], [RangeID3], [RangeID4] FROM
(
select A.RangeID as [RangeID1], B.RangeID as [RangeID2], C.RangeID as [RangeID3], D.RangeID as [RangeID4]
from [Range] as A
inner join [Range] as B on (A.RangeID <= B.RangeID)
inner join [Range] as C on (B.RangeID <= C.RangeID)
inner join [Range] as D on (C.RangeID <= D.RangeID)
where A.RangeSetID <> B.RangeSetID
and B.RangeSetID <> C.RangeSetID
and C.RangeSetID <> D.RangeSetID
) as A) T
UNPIVOT ( RangeID FOR N IN ([RangeID1] , [RangeID2], [RangeID3], [RangeID4] ))P
)
SELECT RateID, RangeID
FROM CTE
This sort of works, but it's way overcomplicated and could do with some tweaking. Note that I changed your sample data, to include gaps to test this works properly.
DECLARE #table TABLE (
range_id INT,
range_set_id INT);
INSERT INTO #table SELECT 1, 4;
INSERT INTO #table SELECT 2, 4;
INSERT INTO #table SELECT 3, 4;
INSERT INTO #table SELECT 5, 4;
INSERT INTO #table SELECT 8, 2;
INSERT INTO #table SELECT 10, 2;
INSERT INTO #table SELECT 17, 2;
INSERT INTO #table SELECT 18, 2;
INSERT INTO #table SELECT 19, 2;
INSERT INTO #table SELECT 20, 2;
INSERT INTO #table SELECT 21, 1;
INSERT INTO #table SELECT 23, 1;
INSERT INTO #table SELECT 28, 1;
INSERT INTO #table SELECT 29, 1;
INSERT INTO #table SELECT 30, 1;
INSERT INTO #table SELECT 33, 1;
INSERT INTO #table SELECT 35, 3;
INSERT INTO #table SELECT 38, 3;
INSERT INTO #table SELECT 39, 3;
INSERT INTO #table SELECT 40, 3;
--Work out the order of the range_set_ids
WITH ordered AS (
SELECT
range_set_id,
range_id,
ROW_NUMBER() OVER (PARTITION BY range_set_id ORDER BY range_id) AS sequential_id
FROM
#table),
ranges AS (
SELECT
range_set_id,
MIN(range_id) AS range_id
FROM
#table
GROUP BY
range_set_id),
range_order AS (
SELECT
range_set_id,
ROW_NUMBER() OVER (ORDER BY range_id) AS order_id
FROM
ranges),
set_count AS (
SELECT
MAX(order_id) AS max_order_id
FROM
range_order),
start_and_end AS (
SELECT
o.range_set_id,
o.order_id,
MIN(range_id) AS min_range_id,
MAX(range_id) AS max_range_id,
COUNT(range_id) AS iterations
FROM
range_order o
INNER JOIN #table t ON t.range_set_id = o.range_set_id
GROUP BY
o.range_set_id,
o.order_id),
toggles AS (
SELECT
s.range_set_id,
s.order_id,
s.iterations AS toggle
FROM
start_and_end s
CROSS JOIN set_count c
WHERE
s.order_id = c.max_order_id
UNION ALL
SELECT
s.range_set_id,
s.order_id,
t.toggle * (s.iterations) AS toggle
FROM
toggles t
INNER JOIN start_and_end s ON s.order_id = t.order_id - 1
WHERE
s.order_id > 0),
toggle_count AS (
SELECT
MAX(toggle * s.iterations) AS max_toggle
FROM
toggles t
CROSS JOIN set_count c
INNER JOIN start_and_end s ON s.order_id = c.max_order_id),
all_combos AS (
SELECT
1 AS rate_set_id,
o.range_set_id,
1 AS sequential_id,
o.order_id,
lt.toggle AS reset_toggle,
ISNULL(t.toggle, 1) AS increment_toggle,
1 AS current_toggle
FROM
range_order o
CROSS JOIN set_count c
INNER JOIN toggles lt ON lt.order_id = o.order_id
LEFT JOIN toggles t ON t.order_id = o.order_id + 1
UNION ALL
SELECT
a.rate_set_id + 1,
a.range_set_id,
CASE
WHEN a.current_toggle = a.reset_toggle THEN 1 --flip back at the end
WHEN a.current_toggle % a.increment_toggle != 0 THEN a.sequential_id --something lower is still toggling
ELSE a.sequential_id + 1 --toggle
END,
a.order_id,
a.reset_toggle,
a.increment_toggle,
CASE
WHEN a.current_toggle < a.reset_toggle THEN a.current_toggle + 1
ELSE 1
END
FROM
all_combos a
CROSS JOIN set_count sc
CROSS JOIN toggle_count tc
WHERE
a.rate_set_id < tc.max_toggle)
SELECT
a.rate_set_id,
a.range_set_id,
o.range_id
FROM
all_combos a
INNER JOIN ordered o ON o.range_set_id = a.range_set_id AND o.sequential_id = a.sequential_id
ORDER BY
a.rate_set_id,
a.order_id
OPTION (MAXRECURSION 0);
Implemented same logic in dynamic query. This should work for you, I guess
declare #i int = 1;
declare #count int = 0;
declare #cols varchar(max) = '';
declare #select varchar(max) = 'select ';
declare #join varchar(max);
declare #where varchar(max);
declare #query varchar(max);
declare #range varchar(100);
declare #prevrange varchar(100);
declare #rangeid varchar(100);
select #count =count(distinct RangeSetID) from [Range];
while #count > 0
begin
set #range = 'Range' + cast(#i as varchar(max));
set #rangeid = 'RangeID' + cast(#i as varchar(max));
set #cols = #cols + #rangeid + ', ';
set #select = #select + #range + '.RangeID as '+#rangeid + ', ';
if #i = 1
begin
set #join = ' from [Range] as ' + #range;
set #where = 'where ' + #range + '.RangeSetID <> ';
end
else
begin
set #prevrange = 'Range' + cast((#i - 1) as varchar(max));
set #join = #join + ' inner join [Range] as ' + #range + ' on (' + #prevrange + '.RangeID <= ' + #range + '.RangeID)';
if(#count = 1)
set #where = #where + #range+ '.RangeSetID';
else
set #where = #where + #range+ '.RangeSetID and '+ #range+ '.RangeSetID <> ';
end
set #i = #i + 1;
set #count = #count - 1;
end
set #query = '
;WITH CTE
AS
(
SELECT * FROM (
SELECT ROW_NUMBER() over (order by '+ SUBSTRING(#cols, 0, LEN(#cols)) + ') as ''RateID'', '+ SUBSTRING(#cols, 0, LEN(#cols)) +' FROM
(
' + SUBSTRING(#select, 0, LEN(#select)) + char(13) + #join + char(13) + #where + '
) as A) T
UNPIVOT ( RangeID FOR N IN ('+(SUBSTRING(#cols, 0, LEN(#cols))) +' ))P
)
SELECT RateID, RangeID
FROM CTE
';
exec (#query);

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.

How to merge column this specific way in SQL

My original table is this;
TableName= NewRetail
CustomerID 1 2 3 4 5 6 7.....30
1 30 31 Null Null Null Null Null
2 24 78 35 72 Null Null Null
I want to store this table in 'Retail'
CustomerId Itemset
1 30
1 31
2 24
2 78
2 35
2 72
There are no duplicates in any row in Original(Source) Table.
Thanks. I tried using Loops but couldn't make it work. Been stuck at it since three days.
You can use table valued constructor with Cross apply to unpivot the data
SELECT CustomerID,
Itemset
FROM Yourtable
CROSS apply (VALUES ([1]),([2]),([3]),([4]),([5]),([6]),([7]),...) cs (Itemset)
WHERE Itemset IS NOT NULL
Dynamic Version
DECLARE #itemset VARCHAR(max)='',
#sql NVARCHAR(max);
WITH cte
AS (SELECT TOP 30 Row_number()OVER(ORDER BY (SELECT NULL)) RN
FROM sys.columns)
SELECT #itemset += '(' + Quotename(RN) + '),'
FROM cte
SET #itemset = LEFT(#itemset, Len(#itemset) - 1)
SET #sql = 'SELECT CustomerID,
Itemset
FROM Yourtable
CROSS apply (VALUES ' + #itemset
+ ') cs (Itemset)
WHERE Itemset IS NOT NULL '
EXEC Sp_executesql #sql
You can do this using UNION ALL:
SELECT *
FROM (
SELECT CustomerId, [1] AS ItemSet FROM NewRetail UNION ALL
SELECT CustomerId, [2] FROM NewRetail UNION ALL
SELECT CustomerId, [3] FROM NewRetail UNION ALL
SELECT CustomerId, [4] FROM NewRetail UNION ALL
SELECT CustomerId, [5] FROM NewRetail UNION ALL
SELECT CustomerId, [6] FROM NewRetail UNION ALL
SELECT CustomerId, [7] FROM NewRetail UNION ALL
...
SELECT CustomerId, [30] FROM NewRetail
)t
WHERE ItemSet IS NOT NULL
ORDER BY CustomerId
Using dynamic SQL:
;WITH Tally(N) AS(
SELECT TOP 30 ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) FROM sys.columns
)
SELECT #sql = #sql +
CASE
WHEN #sql = '' THEN 'SELECT CustomerId, [' + CONVERT(VARCHAR(2), N) + '] AS ItemSet FROM NewRetail'
ELSE ' UNION ALL' + CHAR(10) +' SELECT CustomerId, [' + CONVERT(VARCHAR(2), N) + '] FROM NewRetail'
END
FROM Tally
SELECT #sql =
'SELECT *
FROM (
' + #sql + CHAR(10) +
')t
WHERE ItemSet IS NOT NULL
ORDER BY CustomerId'
PRINT #sql
EXEC (#sql)
It is a simple UNPIVOT operation. NULLs automatically eliminated from result:
declare #t table(custid int, [1] int, [2] int, [3] int)
insert into #t values
(1, 10, 30, null),
(2, 30, 40, 50)
select custid, c from #t
unpivot(c for p in([1], [2], [3])) p
Fiddle http://sqlfiddle.com/#!6/9eecb/1733

Pivoting SQL Server result set

if I have a result returned as follows:
pkTestInstanceID Percent1 Count1 Percent2 Count2
1 25 1 75 3
2 50 2 50 2
Is there a way so it pivots in such format:
pkTestInstanceID Percent Count
1 25 1
1 75 3
2 50 2
2 50 2
Sorry if this question is totally misguided. I'm not super clear on the pivoting process. Thanks for any help.
EDIT I should probably have noted that the Percent1, Count1, Percent2 etc columns are created based off of another column (stackposition). So if stackposition has 4 rows then the percent and count will go up to percent4 count4. Is a pivot or union still possible without the knowledge of the exact number of percent and count columns in the result set.
EDIT 2: It gets a bit more complicated now...
I now realize that I have to include another item in my select statement (fkBandID). For each bandID there is a stackposition as stated above, so for bandID 96 the stackposition is 4, for 97 the stackposition is 3, for 98 the stackposition is 2 etc. so I want the result set to look as follows:
fkBandID pkTestInstanceID Band_Percent Band_Count StackPosition (not included but there for for visual example)
96 265 2 1 4
97 265 4 2 3
98 265 34 17 2
99 265 59 29 1
Here is what the creation of my second query looks like after the initial result set is brought back and with the bandID being selected including the new bandID. This is from Pradeep's answer.
http://gyazo.com/091ece1a4a1334c0f2546bccb8a6b8da
This is what the result set looks like, so as you can see there are 4 rows being created for each bandID. Is there anyway to fix this and make it look as I displayed above in the cross apply that Pradeep helped me with? Or any other solution?
http://gyazo.com/cd19634a1201362ac3aa4546f15373c9
Sorry I'm super nooby with SQL. Let me know if more info is needed.
EDIT 3
(N'DECLARE #strYearIds nvarchar(100)
SET #strYearIds = ''' + #strYearIds + N'''
DECLARE #strDemoCodeIds nvarchar(100)
SET #strDemoCodeIds = ''' + #strDemoCodeIds + N'''
DECLARE #intRosterSetId int
SET #intRosterSetId = ' + CONVERT(nvarchar, #intRosterSetId) + N'
DECLARE #intSchoolId int
SET #intSchoolId = ' + CONVERT(nvarchar, #intSchoolId) + N'
DECLARE #intTeachId int
SET #intTeachId = ' + CONVERT(nvarchar, #intTeachId) + N'
DECLARE #intGradeId int
SET #intGradeId = ' + CONVERT(nvarchar, #intGradeId) + N'
DECLARE #intDeptId int
SET #intDeptId = ' + CONVERT(nvarchar, #intDeptId) + N'
DECLARE #intCourseId int
SET #intCourseId = ' + CONVERT(nvarchar, #intCourseId) + N'
DECLARE #intPeriodId int
SET #intPeriodId = ' + CONVERT(nvarchar, #intPeriodId) + N'
DECLARE #strTestInstId nvarchar(100)
SET #strTestInstId = ''' + #strTestInstId + N'''
DECLARE #intTestTypeId int
SET #intTestTypeId = ' + CONVERT(nvarchar, #intTestTypeId) + N'
DECLARE #strSubIds nvarchar(100)
SET #strSubIds = ''' + #strSubIds + N'''
DECLARE #bitIsStrand bit
SET #bitIsStrand = ' + CONVERT(nvarchar, #bitIsStrand) + N'
DECLARE #intPerfLevelReportId int
SET #intPerfLevelReportId = ' + CONVERT(nvarchar, #intPerfLevelReportId) +
N' DECLARE #tempTests TABLE (id int)
INSERT INTO #tempTests
exec SPGetStudentTests_Local_MTI #strDemoCodeIds, #strYearIds, #intSchoolId, #intTeachId, #intGradeId,
#intRosterSetId, #intPeriodId, #intDeptId, #intCourseId, #strTestInstId, #intTestTypeId
DECLARE #tempSubs TABLE (id int)
IF #bitIsStrand = 1
BEGIN
INSERT INTO #tempSubs
SELECT pkTestSubjectID FROM MM_Test_Subjects WHERE fkCSTStrandID /*= #intSubID*/ IN (SELECT number FROM itot(#strSubIds, N'','')) AND fkTestTypeID = #intTestTypeId
END
ELSE
BEGIN
INSERT INTO #tempSubs
SELECT number FROM itot(#strSubIds, N'','')--VALUES (#intSubId)
END
SELECT bands.pkPerformanceLevelReportBandID AS ''fkBandID'', TestInstances.pkTestInstanceID AS ''TestInstanceID'', StudentScores_Subject.fkTest_SubjectID AS ''TestSubjectID'', '
+ #cols +
N'INTO ##tempTable FROM StudentScores_Subject
INNER JOIN StudentTests ON StudentScores_Subject.fkStudentTestID = StudentTests.pkStudentTestID
INNER JOIN TestInstances ON TestInstances.pkTestInstanceID = StudentTests.fkTestInstanceID
INNER JOIN CAHSEE_TestPeriods ON CAHSEE_TestPeriods.pkTestPeriodID = TestInstances.fkTestPeriodID
INNER JOIN PerformanceLevelReportBands bands ON bands.fkPerformanceLevelReportID = #intPerfLevelReportId
LEFT JOIN MMARS_Web_TestInfo_California.dbo.PerfLevelReportBandCutScores cutScores ON cutScores.fkPerformanceLevelReportBandID = bands.pkPerformanceLevelReportBandID
AND cutScores.fkGradeID = #intGradeId
AND cutScores.fkTestSubjectID IN (SELECT id FROM #tempSubs)
INNER JOIN PerfLevelReportBandComponents bandComponents ON bandComponents.fkPerformanceLevelReportBandID = bands.pkPerformanceLevelReportBandID
AND((bandComponents.ScoreValue = StudentScores_Subject.ScoreValue) OR
((CAST(StudentScores_Subject.ScoreValue AS INT) BETWEEN bandComponents.minScore and bandComponents.maxScore)
OR
(CAST(StudentScores_Subject.ScoreValue AS INT) BETWEEN cutScores.minScore and cutScores.maxScore))
)
RIGHT JOIN MM_SchoolYears ON MM_SchoolYears.pkSchoolYearID = TestInstances.fkSchoolYearID
WHERE MM_SchoolYears.pkSchoolYearID IN (SELECT number FROM itot(#strYearIds, N'',''))
AND bands.fkPerformanceLevelReportID = #intPerfLevelReportId
AND StudentScores_Subject.fkStudentTestID IN (SELECT id FROM #tempTests)
AND StudentScores_Subject.fkScoreTypeID = bandComponents.fkScoreTypeID
AND StudentScores_Subject.fkTest_SubjectID IN (SELECT id FROM #tempSubs)
--AND((bandComponents.ScoreValue = StudentScores_Subject.ScoreValue) OR
--(StudentScores_Subject.ScoreValue BETWEEN bandComponents.minScore and bandComponents.maxScore) OR
--(StudentScores_Subject.ScoreValue BETWEEN cutScores.minScore and cutScores.maxScore))
GROUP BY bands.pkPerformanceLevelReportBandID, TestInstances.pkTestInstanceID, StudentScores_Subject.fkTest_SubjectID
ORDER BY bands.pkPerformanceLevelReportBandID, TestInstances.pkTestInstanceID, StudentScores_Subject.fkTest_SubjectID')
The #cols variable is as follows:
DECLARE #cols NVARCHAR(MAX)
SELECT #cols = STUFF(( SELECT DISTINCT TOP 100 PERCENT ', SUM(CASE WHEN bands.StackPosition = ''' + STR(b.StackPosition, 1) + ''' THEN 1 ELSE 0 END) * 100.0/ CASE WHEN COUNT(pkStudentScoreID) = 0 THEN 1 ELSE COUNT(pkStudentScoreID) END AS ''Percent_' + STR(b.StackPosition, 1) + ''', SUM(CASE WHEN bands.StackPosition = ''' + STR(b.StackPosition, 1) + ''' THEN 1 ELSE 0 END) AS ''Count_' + STR(b.StackPosition, 1) + ''''
FROM PerformanceLevelReportBands AS b
WHERE b.fkPerformanceLevelReportID = #intPerfLevelReportId
ORDER BY ', SUM(CASE WHEN bands.StackPosition = ''' + STR(b.StackPosition, 1) + ''' THEN 1 ELSE 0 END) * 100.0/ CASE WHEN COUNT(pkStudentScoreID) = 0 THEN 1 ELSE COUNT(pkStudentScoreID) END AS ''Percent_' + STR(b.StackPosition, 1) + ''', SUM(CASE WHEN bands.StackPosition = ''' + STR(b.StackPosition, 1) + ''' THEN 1 ELSE 0 END) AS ''Count_' + STR(b.StackPosition, 1) + ''''
FOR XML PATH('')
), 1, 2, '')
what you are looking for is Unpivot not pivot
CREATE TABLE #piv
(
pkTestInstanceID INT,
Percent1 INT,
Count1 INT,
Percent2 INT,
Count2 INT
)
INSERT INTO #piv
VALUES ( 1,25,1,75,3),
(2,50,2,50,2)
SELECT pkTestInstanceID,
[percent],
[count]
FROM #piv AS p
CROSS APPLY ( VALUES (Percent1,Count1),
(Percent2,Count2))
AS x([percent], [count]);
If you want this to work dynamically then below code should help you.
For example i have kept no. of stackposition rows as 2 u can change it and check
DECLARE #stackposition INT=2,
#sql NVARCHAR(max),
#cnt INT=1
SET #sql =' SELECT pkTestInstanceID,
[percent],
[count]
FROM #piv AS p
CROSS APPLY ( VALUES '
WHILE #cnt <= #stackposition
BEGIN
SET #sql+='([Percent' + CONVERT(VARCHAR(10), #cnt)+ '],[Count' + CONVERT(VARCHAR(10), #cnt) + ']),'
SET #cnt+=1
END
SET #sql= LEFT(#sql, Len(#sql) - 1)
SET #sql+=') AS x([percent], [count])'
EXEC Sp_executesql
#sql
OUTPUT
pkTestInstanceID percent count
---------------- ------- -----
1 25 1
1 75 3
2 50 2
2 50 2
You don't really need to pivot here. You can do a UNION on the resultset as suggested by #bksi like below
select pkTestInstanceID, percent1 as [percent], count1 as count
from (
inner result set
) tab
UNION
select pkTestInstanceID, percent2, count2
from (
inner result set
) tab1