Running Total until specific condition is true - sql

I'm having a table representing the dealers cards and their rank. I'm now trying to make a query (as fast as possible) to set status on the game.
(As said before, only the dealer cards is shown)
W = Win
S = Stand
L = Loss
B = Blackjack (in two cards)
About the rules:
The dealer wins at 21, if it's in two cards its blackjack. If the rank is between 17 and 20 it's S = stand. Over 21 is a loss.
Ranks:
1 (ACE) - 1 or 11 rank. Counted as 11.
2-10 - 2-10 rank
11-13 (knight - king) - 10 rank
╔════╦══════╦════════╗
║ Id ║ Rank ║ Status ║
╠════╬══════╬════════╣
║ 1 ║ 1 ║ ║
║ 2 ║ 5 ║ ║
║ 3 ║ 8 ║ L ║ //24 = Loss
║ 4 ║ 3 ║ ║
║ 5 ║ 1 ║ ║
║ 6 ║ 7 ║ W ║ //21 = Win
║ 7 ║ 10 ║ ║
║ 8 ║ 1 ║ B ║ //21 = Blackjack
║ 9 ║ 10 ║ ║
╚════╩══════╩════════╝
I've tried to use a counter to check if it's blackjack and then I'm using a "RunningPoint" to check the sum of the cards.
I have now a solution bad it shows very bad performance when it's a lot of data. How would you do this and what can I do to optimize my query? When using more data I also need to use option (maxrecursion 0)
(When having 1 million rows it's not even possible to run this...)
My example: http://sqlfiddle.com/#!6/3855e/1

There's no efficient solution using plain SQL (including Windowed Aggregate Functons), at least nobody found one, yet :-)
Your recursive query performs bad because it's way too complicated, this is a simplified version:
Edit: Fixed the calculation (Fiddle)
WITH ctePoints AS
(
SELECT 1 AS id
,rank
,CASE
WHEN rank >= 10 THEN 10
WHEN rank = 1 THEN 11
ELSE rank
END AS Point
,1 AS Counter
FROM dbo.BlackJack
WHERE Id = 1
UNION ALL
SELECT t2.Id
,t2.rank
,CASE WHEN t1.Point < 17 THEN t1.Point ELSE 0 END
+ CASE
WHEN t2.rank >= 10 THEN 10
WHEN t2.rank = 1 THEN 11
ELSE t2.rank
END AS Point
,CASE WHEN t1.Point < 17 THEN t1.Counter + 1 ELSE 1 END AS Counter
FROM dbo.BlackJack AS t2
INNER JOIN ctePoints AS t1 ON t2.Id = t1.Id + 1
)
SELECT ctepoints.*
,CASE
WHEN Point < 17 THEN ''
WHEN Point < 20 THEN 'S'
WHEN Point > 21 THEN 'L'
WHEN Point = 21 AND Counter = 2 THEN 'B'
ELSE 'W'
END AS DealerStatus
FROM ctePoints
It's probably still too slow, because it processes row by row.
I usually use recursive SQL to replace cursor logic (because in my DBMS it's usually much faster) but a cursor update might actually be faster (Demo):
CREATE TABLE #BlackJack
(
id INT PRIMARY KEY CLUSTERED
,Rank INT
,DealerStatus CHAR(1)
);
insert into #BlackJack (Id, Rank)
values
(1, 1),(2, 5), (3, 8), (4, 3), (5, 1), (6, 7), (7, 10), (8, 1),(9, 10), (10, 10), (11,1);
DECLARE #Counter INT = 0
,#Point INT = 0
,#id int
,#Rank int
,#DealerStatus char(1)
DECLARE c CURSOR
FOR
SELECT id, Rank
FROM #BlackJack
ORDER BY id FOR UPDATE OF DealerStatus
OPEN c
FETCH NEXT FROM c INTO #id, #Rank
WHILE ##FETCH_STATUS = 0
BEGIN
SET #counter = #counter + 1
SET #Rank = CASE
WHEN #Rank >= 10 THEN 10
WHEN #Rank = 1 THEN 11
ELSE #Rank
END
SET #Point = #Point + #Rank
SET #DealerStatus = CASE
WHEN #Point < 17 THEN ''
WHEN #Point < 20 THEN 'S'
WHEN #Point > 21 THEN 'L'
WHEN #Point = 21 AND #Counter = 2 THEN 'B'
ELSE 'W'
END
IF #Point >= 17
BEGIN
UPDATE #BlackJack
SET DealerStatus = #DealerStatus
WHERE CURRENT OF c;
SET #Point = 0
SET #Counter = 0
END
FETCH NEXT FROM c INTO #id, #Rank
END
CLOSE c
DEALLOCATE c
SELECT * FROM #BlackJack ORDER BY id
Still #lad2025's "quirky update" is the fastest way to get the expected result, but it's using an undocumented feature and if a Service Pack breaks it there's no way to complain about it :-)

This solution is based on quirky update. More info here.
LiveDemo
Data and structures:
CREATE TABLE #BlackJack
(
id INT
,Rank INT
,running_total INT
,result NVARCHAR(100)
);
CREATE CLUSTERED INDEX IX_ROW_NUM ON #BlackJack(id);
insert into #BlackJack (Id, Rank)
values (1, 1),(2, 5), (3, 8), (4, 3), (5, 1),
(6, 7), (7, 10), (8, 1),(9, 10), (10, 10), (11,1);
Main query:
DECLARE #running_total INT = 0
,#number_of_cards INT = 0
,#prev_running_total INT = 0;
UPDATE #BlackJack
SET
#prev_running_total = #running_total
,#running_total = running_total = IIF(#running_total >= 20, 0, #running_total)
+ CHOOSE(Rank,11,2,3,4,5,6,7,8,9,10,10,10,10)
,result = CASE WHEN #running_total = 20 THEN 'S'
WHEN #running_total = 21 AND #number_of_cards = 2 THEN 'B'
WHEN #running_total = 21 THEN 'W'
WHEN #running_total > 21 THEN 'L'
ELSE NULL
END
,#number_of_cards = IIF(#prev_running_total >= 20, 0, #number_of_cards) + 1
FROM #BlackJack WITH(INDEX(IX_ROW_NUM))
OPTION (MAXDOP 1);
SELECT *
FROM #BlackJack
ORDER BY id;
Warning
If you use SQL Server < 2012 you need to replace IIF and CHOOSE with CASE. I don't check all Blackjack rules, only for provided sample. If something is wrong feel free to change CASE logic.
Second I extend base table BlackJack with auxiliary columns, but you can create any new table, if needed.
The key point is to read data sequentially based on clustered key ascending and do not allow parallel execution. Before you use it in production check how it behaves with large data set.

Related

TSQL - Sum on time column with condition

Need to create a query, which will get summary of time during which was bit set ON/OFF.
Example:
╔═══════════════════════════╗
║ TABLE ║
╠════╦════════╦═════╦═══════╣
║ ID ║ TIME ║ BIT ║ VALUE ║
╠════╬════════╬═════╬═══════╣
║ 1 ║ 13:40 ║ 1 ║ 5 ║
║ 2 ║ 13:45 ║ 1 ║ 3 ║
║ 3 ║ 13:50 ║ 1 ║ 1 ║
║ 4 ║ 13:55 ║ 0 ║ 2 ║
║ 5 ║ 14:00 ║ 0 ║ 7 ║
║ 6 ║ 14:05 ║ 1 ║ 3 ║
║ 7 ║ 14:10 ║ 1 ║ 4 ║
║ 8 ║ 14:15 ║ 0 ║ 2 ║
║ 9 ║ 14:20 ║ 1 ║ 2 ║
╚════╩════════╩═════╩═══════╝
I would like to have total summary of TIME (and VALUE - simpler one) when the BIT was SET ON:
13:40 - 13:50 = 10 mins
14:05 - 14:10 = 5 mins
14:20 = no end time, 0 mins
-----------------------------------------
15 mins
Have found:
How to sum up time field in SQL Server = Sort of good question, but there is static time as start (0:00:00) which won't work in this case
Aggregate function over a given time interval = there is aggregation but not conditioned and works on all data
I thought that this could be done as a recursive function (passing last processed datetime), which will pass the last date which was handled, and sum the datetime since the BIT is ON.
SQL query for summing the VALUE (easy one):
SELECT SUM(Value)
FROM Table
WHERE Bit = 1
How should I get total value of minutes (time), during which was the BIT set ON?
EDIT: Query which can be used for testing:
DECLARE #Table TABLE(
ID INT Identity(1,1) PRIMARY KEY,
[TIME] DATETIME NOT NULL,
[BIT] BIT NOT NULL,
[VALUE] INT NOT NULL
);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('13:40',1,5);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('13:45',1,3);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('13:50',1,1);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('13:55',0,2);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('14:00',0,7);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('14:05',1,3);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('14:10',1,4);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('14:15',0,2);
INSERT INTO #Table([TIME],[BIT],[VALUE]) VALUES('14:20',1,2);
SELECT * FROM #Table;
Use LEAD function to get time in the next row and to calculate time interval. Then just group result by [bit]
WITH t AS(
SELECT
[time],
DATEDIFF(minute, [time], LEAD([time], 1, null) OVER (ORDER BY [time])) AS interval,
[bit],
[value]
FROM table1)
SELECT [bit], CAST(DATEADD(MINUTE, SUM(interval), '00:00') AS TIME), SUM([value]) FROM t
GROUP BY [bit]
You have two issues: summing up the time and identify the adjacent values. You can handle the second with the difference of row numbers approach. You can handle the former by converting to minutes:
select bit, min(time), max(time),
sum(datediff(minute, 0, time)) as minutes,
sum(value)
from (select t.*,
row_number() over (order by id) as seqnum,
row_number() over (partition by bit order by id) as seqnum_b
from t
) t
group by (seqnum - seqnum_b), bit;
This is a "gaps and islands" problem, with a pretty standard solution. I came up with this, which is pretty much the same as Gordon's, but has an extra step to calculate the intervals. This is the only reason I am posting what is essentially a duplicate answer, I'm not sure that taking the difference in minutes from zero actually works?
DECLARE #table TABLE (id int, [time] TIME, [bit] BIT, value INT);
INSERT INTO #table SELECT 1, '13:40', 1, 5;
INSERT INTO #table SELECT 2, '13:45', 1, 3;
INSERT INTO #table SELECT 3, '13:50', 1, 1;
INSERT INTO #table SELECT 4, '13:55', 0, 2;
INSERT INTO #table SELECT 5, '14:00', 0, 7;
INSERT INTO #table SELECT 6, '14:05', 1, 3;
INSERT INTO #table SELECT 7, '14:10', 1, 4;
INSERT INTO #table SELECT 8, '14:15', 0, 2;
INSERT INTO #table SELECT 9, '14:20', 1, 2;
WITH x AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY [bit] ORDER BY id) AS a_id, ROW_NUMBER() OVER (ORDER BY id) AS b_id FROM #table),
y AS (
SELECT [bit], MIN([time]) AS min_time, MAX([time]) AS max_time, SUM(value) AS value FROM x GROUP BY a_id - b_id, [bit])
SELECT [bit], SUM(value) AS total_value, SUM(DATEDIFF(MINUTE, min_time, max_time)) AS total_minutes FROM y GROUP BY [bit];
Results:
bit total_value total_minutes
0 11 5
1 18 15
As a bonus here is a solution that only solves the actual question, i.e. how much elapsed time is there when the BIT is set to 1:
WITH x AS (SELECT id, id - DENSE_RANK() OVER(ORDER BY id) AS grp FROM #table WHERE [bit] = 1), y AS (SELECT MIN(id) AS range_start, MAX(id) AS range_end FROM x GROUP BY grp)
SELECT SUM(DATEDIFF(MINUTE, t1.[time], t2.[time])) AS minutes_elapsed FROM y INNER JOIN #table t1 ON t1.id = y.range_start INNER JOIN #table t2 ON t2.id = y.range_end;

Concatenating numbers from a single numeric column in SQL without using concat function

I have the following code for a single column table that has values from 0 to 9:
create table number(
value int
)
truncate table number
insert into number values(0);
insert into number values(1);
insert into number values(2);
insert into number values(3);
insert into number values(4);
insert into number values(5);
insert into number values(6);
insert into number values(7);
insert into number values(8);
insert into number values(9);
What I want is a query which should join the numbers without using concat (or any builtin function) function and gives me the following output in a tabular form:
0
1
2
3
4
5
6
7
8
9
10
11
12
13
14
.
.
.
.
.
999
Really having a hard time doing it for the past 5 days. Need sincere help.
You could use CROSS JOIN and simple Math formula:
SELECT [num] = n1.value * 100 + n2.value * 10 + n3.value
FROM number n1, number n2, number n3
ORDER BY num
LiveDemo
Output:
╔═════╗
║ num ║
╠═════╣
║ 0 ║
║ 1 ║
║ 2 ║
║ 3 ║
║ 4 ║
║ 5 ║
║ 6 ║
║ 7 ║
║ ... ║
║ 999 ║
╚═════╝
EDIT:
Cross join will return records(tuples) (0,0,0), (0,0,1),(0,0,2), ...,(9,9,9) then you need to apply Positional Number System formula:
example:
Images from https://en.wikipedia.org/wiki/Positional_notation
╔════════════╦═══════════╦══════════╦════════════════════╗
║ 10^2 = 100 ║ 10^1 = 10 ║ 10^0 = 1 ║ num ║
╠════════════╬═══════════╬══════════╬════════════════════╣
║ 0 * 100 ║ 0 * 10 ║ 0 * 1 ║ 0 + 0 + 0 = 0 ║
║ 0 * 100 ║ 0 * 10 ║ 1 * 1 ║ 0 + 0 + 1 = 1 ║
║ 0 * 100 ║ 0 * 10 ║ 2 * 1 ║ 0 + 0 + 2 = 2 ║
║ ... ║ ... ║ ... ║ ... ║
║ 9 * 100 ║ 9 * 10 ║ 9 * 1 ║ 900 + 90 + 9 = 999 ║
╚════════════╩═══════════╩══════════╩════════════════════╝
While #lad2025 query is good based on certain formula.It uses 3 cross join.
Simpler way (not sure,if this is best)
Declare #number table(
value int
)
insert into #number values(0);
insert into #number values(1);
insert into #number values(2);
insert into #number values(3);
insert into #number values(4);
insert into #number values(5);
insert into #number values(6);
insert into #number values(7);
insert into #number values(8);
insert into #number values(9);
Declare #MaxLimit int=999
;
WITH CTE
AS (
SELECT value
FROM #number A
UNION ALL
SELECT a.value + 10
FROM CTE A
WHERE A.value < #MaxLimit-9
)
SELECT value
FROM cte
--WHERE value <= 999
ORDER BY value

Multpilcation Aggregate in Sql Server

I am trying to create Multplication aggregate function from the below sql code.
declare #Floats as table (id int,value float)
insert into #Floats values (1,1)
insert into #Floats values (2,3)
insert into #Floats values (3,6)
SELECT *
FROM #Floats a
CROSS apply (SELECT CASE
WHEN MinVal = 0 THEN 0
WHEN Neg % 2 = 1 THEN -1 * Exp(ABSMult)
ELSE Exp(ABSMult)
END AS mul_value
FROM (SELECT
Sum(Log(Abs(NULLIF(Value, 0)))) AS ABSMult,
Sum(Sign(CASE
WHEN Value < 0 THEN 1
ELSE 0
END)) AS Neg,
Min(Abs(Value)) AS MinVal
FROM #Floats b
WHERE a.id >= b.id) foo) cs
Result
id value mul_value
-- ----- ---------
1 1 1
2 3 3
3 6 18
Here is the idea for function. Since we we need table type to pass table as input to function am creating one below
User defined table type
CREATE TYPE [dbo].[UDT_TEST] AS TABLE( value float )
Function code
CREATE FUNCTION udf_Mul(#values dbo.[UDT_TEST] readonly)
RETURNS FLOAT
AS
BEGIN
DECLARE #mul_value FLOAT
SELECT #mul_value=CASE
WHEN MinVal = 0 THEN 0
WHEN Neg % 2 = 1 THEN -1 * Exp(ABSMult)
ELSE Exp(ABSMult)
END
FROM (SELECT
--log of +ve row values
Sum(Log(Abs(NULLIF(Value, 0)))) AS ABSMult,
--count of -ve values. Even = +ve result.
Sum(Sign(CASE
WHEN Value < 0 THEN 1
ELSE 0
END)) AS Neg,
--anything * zero = zero
Min(Abs(Value)) AS MinVal
FROM #values) foo
RETURN #mul_value
END
Using the function like this.
SELECT *
FROM #Floats a
CROSS apply (SELECT dbo.udf_mul(value) ast
FROM #Floats b
WHERE a.id >= b.id) cs
Here the problem is the input dbo.udf_mul expects parameter to be of [dbo].[UDT_TEST] type but value column is of Float type.
Error :
Msg 206, Level 16, State 2, Line 7 Operand type clash: float is
incompatible with UDT_TEST Is there any way to achieve this?
I would not use scalar function for this type of operation. You end up with poor performance because query optimizer need to run it row-by-row without any optimalization. More info: SQL Server Functions: The Basics.
Second with float/decimal and LOG/EXP you will get approximation errors.
Third to pass data to function you can use XML like:
CREATE FUNCTION dbo.udf_Mul(#value xml)
RETURNS FLOAT
AS
BEGIN
DECLARE #mul_value FLOAT;
DECLARE #values AS TABLE ([value] float);
INSERT INTO #values([value])
SELECT [value] = t.c.value('(value)[1]', 'float')
FROM #value.nodes('//row') AS t(c);
SELECT #mul_value=CASE
WHEN MinVal = 0 THEN 0
WHEN Neg % 2 = 1 THEN -1 * Exp(ABSMult)
ELSE Exp(ABSMult)
END
FROM (SELECT
Sum(Log(Abs(NULLIF(Value, 0)))) AS ABSMult,
Sum(Sign(CASE
WHEN Value < 0 THEN 1
ELSE 0
END)) AS Neg,
Min(Abs(Value)) AS MinVal
FROM #values) foo
RETURN #mul_value
END
and calling it:
SELECT *
FROM Floats a
CROSS APPLY (SELECT dbo.udf_mul((SELECT [value]
FROM Floats b
WHERE a.id >= b.id
FOR XML PATH, ROOT('root')))
AS r) as cs(r);
SqlFiddleDemo
Output:
╔═════╦════════╦════════════════════╗
║ id ║ value ║ r ║
╠═════╬════════╬════════════════════╣
║ 1 ║ 1 ║ 1 ║
║ 2 ║ 3 ║ 3.0000000000000004 ║
║ 3 ║ 6 ║ 17.999999999999996 ║
║ 4 ║ 2 ║ 36 ║
╚═════╩════════╩════════════════════╝

SQL Add Running Total Column for each item in a giant table

I wanna calculate a running total based on 5 types of transactions (let's say transaction A, B, C, D, E). But I have over one thousand different products in this table and each product could have millions of transaction records on different days.
So the table looks like this:
ProductID A B C D E Running Total
1 10 0 5 0 5 20
2 15 0 0 0 0 15
3 20 5 0 10 0 35
1 10 0 0 0 0 30 (20 for product 1, plus 10 for product 1 again)
3 12 0 33 0 0 80 (35 for product 3, plus 45 for product 3 again)
The ANSI standard method is to use sum() as a window function:
select t.*,
sum(a + b + c + d + e) over (partition by productid order by <datetimecol>) as RunningTotal
from table t;
SQL tables represent unordered sets, so you need a column that specifies the ordering. I am guessing there is a date/time column somewhere for this purpose.
Most databases support this standard syntax: Oracle, SQL Server 2012+, Postgres, Teradata, and DB2.
Test Data
DECLARE #TABLE TABLE (ProductID INT, A INT, B INT, C INT, D INT, E INT)
INSERT INTO #TABLE VALUES
(1 ,10, 0, 5 , 0 , 5), -- 20
(2 ,15, 0, 0 , 0 , 0), -- 15
(3 ,20, 5, 0 , 10, 0), -- 35
(1 ,10, 0, 0 , 0 , 0), -- 30 (20 for product 1, plus 10 for product 1 again)
(3 ,12, 0, 33, 0 , 0) -- 80
Query
;WITH CTE AS
(
select *
,ROW_NUMBER() OVER (PARTITION BY ProductID ORDER BY ProductID ASC) rn
from #TABLE
)
SELECT ProductID
,A
,B
,C
,D
,E
,runningTotal
FROM CTE c
cross apply (select sum(A+B+C+D+E) as runningTotal
from CTE
where rn <= c.rn
and ProductID = c.ProductID
) as rt
Result
╔═══════════╦════╦═══╦════╦════╦═══╦══════════════╗
║ ProductID ║ A ║ B ║ C ║ D ║ E ║ runningTotal ║
╠═══════════╬════╬═══╬════╬════╬═══╬══════════════╣
║ 1 ║ 10 ║ 0 ║ 5 ║ 0 ║ 5 ║ 20 ║
║ 1 ║ 10 ║ 0 ║ 0 ║ 0 ║ 0 ║ 30 ║
║ 2 ║ 15 ║ 0 ║ 0 ║ 0 ║ 0 ║ 15 ║
║ 3 ║ 20 ║ 5 ║ 0 ║ 10 ║ 0 ║ 35 ║
║ 3 ║ 12 ║ 0 ║ 33 ║ 0 ║ 0 ║ 80 ║
╚═══════════╩════╩═══╩════╩════╩═══╩══════════════╝

update table with values in related records

I have a table which must be with next structure:
╔════╦═══════╦════╦═════╗
║ id ║ a ║ c ║ b ║
╠════╬═══════╬════╬═════╣
║ 55 ║ 56;57 ║ ║ P25 ║
║ 56 ║ ║ 56 ║ 25 ║
║ 57 ║ ║ 57 ║ 25 ║
╚════╩═══════╩════╩═════╝
where:
1) record with id=55 is a parent record and
2) records with id=56, id=57 (listed in a column and separated with semicolon) are child records
At first table is next
╔════╦═══════╦════╦═════╗
║ id ║ a ║ c ║ b ║
╠════╬═══════╬════╬═════╣
║ 55 ║ 56;57 ║ ║ ║
║ 56 ║ ║ 56 ║ ║
║ 57 ║ ║ 57 ║ ║
╚════╩═══════╩════╩═════╝
so I must to update table such as first table
For this purpose I created next CTE
with My_CTE(PId, a, c, b, newC, inde) as
(
select
ST.PID, ST.a, ST.c, ST.b, res.C,
ind = case
when ST.a != ''
then (dense_rank() over(order by ST.a))
end
from STable as ST
outer APPLY
fnSplit(ST.a) as res
where (not(ST.a = '') or not(ST.c = ''))
)
UPDATE STable
Set b =
cte.inde
From STable as st
Join My_CTE as cte on st.PID = cte.PId;
GO
As a result I have table with next values
╔════╦═══════╦════╦═════╗
║ id ║ a ║ c ║ b ║
╠════╬═══════╬════╬═════╣
║ 55 ║ 56;57 ║ ║ 25 ║
║ 56 ║ ║ 56 ║ ║
║ 57 ║ ║ 57 ║ ║
╚════╩═══════╩════╩═════╝
So I need to set values in column b for children records.
Maybe it could be established in select statement of MyCTE?
Help please
I am not entirely sure if I understand your request correctly so apologies if I have misunderstood.
So you have already managed to get the result set to here
╔════╦═══════╦════╦═════╗
║ id ║ a ║ c ║ b ║
╠════╬═══════╬════╬═════╣
║ 55 ║ 56;57 ║ ║ 25 ║
║ 56 ║ ║ 56 ║ ║
║ 57 ║ ║ 57 ║ ║
╚════╩═══════╩════╩═════╝
Now you want this to look like this
╔════╦═══════╦════╦═════╗
║ id ║ a ║ c ║ b ║
╠════╬═══════╬════╬═════╣
║ 55 ║ 56;57 ║ ║ P25 ║
║ 56 ║ ║ 56 ║ 25 ║
║ 57 ║ ║ 57 ║ 25 ║
╚════╩═══════╩════╩═════╝
Please see if this script works for you.
IF OBJECT_ID(N'tempdb..#temp')>0
DROP TABLE #temp
IF OBJECT_ID(N'tempdb..#temp1')>0
DROP TABLE #temp1
CREATE TABLE #temp (id int, a varchar(100),c varchar(100),b varchar(100))
INSERT INTO #temp VALUES ('55','56;57',' ','25')
INSERT INTO #temp VALUES ('56',' ','56',' ')
INSERT INTO #temp VALUES ('57',' ','57',' ')
SELECT * FROM #temp t
SELECT y.id, fn.string AS a,y.b
INTO #temp1
FROM #temp AS y
CROSS APPLY dbo.fnParseStringTSQL(y.a, ';') AS fn
--SELECT * FROM #temp1
UPDATE t
SET t.b=CASE WHEN CHARINDEX(';',t.a)>0 THEN 'P'+t.b ELSE t1.b END
FROM #temp t
LEFT JOIN #temp1 t1
ON t.id = t1.a
--DROP TABLE #temp
SELECT * FROM #temp t
IF OBJECT_ID(N'tempdb..#temp')>0
DROP TABLE #temp
IF OBJECT_ID(N'tempdb..#temp1')>0
DROP TABLE #temp1
Function borrowed from this link
CREATE FUNCTION [dbo].[fnParseStringTSQL]
(
#string NVARCHAR(MAX),
#separator NCHAR(1)
)
RETURNS #parsedString TABLE (string NVARCHAR(MAX))
AS
BEGIN
DECLARE #position INT
SET #position = 1
SET #string = #string + #separator
WHILE CHARINDEX(#separator, #string, #position) <> 0
BEGIN
INSERT INTO #parsedString
SELECT SUBSTRING(
#string,
#position,
CHARINDEX(#separator, #string, #position) - #position
)
SET #position = CHARINDEX(#separator, #string, #position) + 1
END
RETURN
END
This is really not an ideal data structure, but the following will do it...
CREATE TABLE #STable
(
id int primary key clustered
, a varchar(500)
, c varchar(500)
, b varchar(500)
)
INSERT INTO #STable
(id, a, c, b)
VALUES (55, '56;57', '', '25')
, (56, '', '56', '')
, (57, '', '57', '')
/* >>>>> Get all parents <<<<< */
CREATE TABLE #folks
(
sno int identity(1,1)
, id int
, a varchar(500)
)
CREATE TABLE #family
(
parent int
, child int
)
INSERT INTO #folks
(id, a)
SELECT id, a
FROM #STable
WHERE a <> ''
DECLARE #NID int
, #XID int
, #parent int
, #Children varchar(500)
, #Child int
SELECT #NID = MIN(sno), #XID = MAX(sno)
FROM #folks
/* >>>>> Loop to figure out the children <<<<< */
WHILE #NID <= #XID
BEGIN
SELECT #parent = id, #Children = a
FROM #folks
WHERE sno = #NID
WHILE LEN(#Children) > 0
BEGIN
IF CHARINDEX(';', #Children) > 0
BEGIN
SET #Child = CAST(LEFT(#Children, CHARINDEX(';', #Children) -1) as int)
SET #Children = RIGHT(#Children, LEN(#Children) - CHARINDEX(';', #Children))
INSERT INTO #family
(parent, child)
VALUES (#parent, #Child)
END
ELSE
BEGIN
SET #Child = CAST(#Children AS INT)
SET #Children = ''
INSERT INTO #family
(parent, child)
VALUES (#parent, #Child)
END
END
SET #NID = #NID + 1
END
/* >>>>> Update <<<<< */
UPDATE c
SET b = p.b
FROM #family f
INNER JOIN #STable p
on f.parent = p.id
INNER JOIN #STable c
on f.child = c.id
SELECT *
FROM #STable
DROP TABLE #STable
DROP TABLE #folks
DROP TABLE #family