Perform logic using a CTE instead of loop - sql-server

Suppose I have a table
Declare #table table
(
id int,
month_col varchar(10),
present_col bit,
absent_col bit,
leave_col bit
);
insert into #table
values (1, 'Jan', 1, 0, 0),
(2, 'Jan', 1, 0, 0),
(3, 'Jan', 0, 1, 0),
(4, 'Jan', 1, 0, 0),
(5, 'Jan', 0, 1, 0),
(6, 'Jan', 1, 0, 0),
(7, 'Jan', 1, 0, 0);
While updating some rows, I need to check all previous rows leave col to take decision for current row, this need to be done for all rows, I had achieve this with while loop like this
while(for all rows)
begin
update #table
set l = mylogic_function((select count(leave_col)
from #table
where id<currentrow.ID))
where id = currentrow.ID
end
but I want to do this logic by using a CTE.

I don't know what your function does so, for my example I created a function that takes an input and returns a 1 when that number is divisible by three. Obviously you can change the logic to what you need.
CREATE FUNCTION dbo.someFunction(#x INT) RETURNS BIT AS BEGIN RETURN (SIGN(#x%3)-1) END;
It's good that you don't want to do this using a loop BUT a solution that uses a recursive CTE (if that's what you are looking for) will likely perform poorly and will be overly complicated for what you are trying to do. Set-based is always the way to go.
To get a count of all rows before your "current" row you could simply use this syntax:
COUNT(t.leave_col) OVER (ORDER BY t.Id)
This will return the same thing as above but will be faster:
ROW_NUMBER() OVER (ORDER BY t.Id)
This query will do what you're looking for and will be faster than using a loop or recursive CTE:
-- Sample data
Declare #table table(id int,month_col varchar(10),present_col bit, absent_col bit,leave_col bit);
insert into #table values
(1,'Jan',1,0,0),
(5,'Jan',1,0,0),
(13,'Jan',0,1,0),
(14,'Jan',1,0,0),
(25,'Jan',0,1,0),
(26,'Jan',1,0,0),
(37,'Jan',1,0,0);
-- Solution
WITH Prep(leave_col_count, leave_col) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY t.Id), t.leave_col
FROM #table AS t
)
UPDATE Prep
SET leave_col = dbo.someFunction(Prep.leave_col_count)
FROM Prep;

Related

How to create a T-SQL function to read values which have been updated as part of the same [update] transaction

I need to use a function in an update of several records.
How to make a T-SQL function read values which have been updated during that update?
Below I created a simple example demonstrating what I get.
My example: table has Category column and I build code based on a category. Function finds the last one withing the category + 1. Or 10000 if it is the first record.
It would work just fine if update is a single record, but updates all records with the same value otherwise.
Below is working code:
CREATE TABLE Tbl
(
Id INT NOT NULL PRIMARY KEY,
Category CHAR(1) NOT NULL,
Code INT NULL
)
CREATE FUNCTION dbo.GetNextAvailableCode
(#Category CHAR(1))
RETURNS INT
AS
BEGIN
DECLARE #MaxCode INT
SELECT #MaxCode = MAX(Code)
FROM dbo.Tbl
WHERE Category = #Category
SET #MaxCode = ISNULL(#MaxCode + 1, 10000);
RETURN #MaxCode
END;
GO
INSERT INTO Tbl (Id, Category)
VALUES (1, 'A'), (2, 'A'), (3, 'A'),
(4, 'B'), (5, 'B'),
(6, 'C'), (7, 'C'), (8, 'C'), (9, 'C')
SELECT * FROM Tbl
UPDATE dbo.Tbl
SET Code = dbo.GetNextAvailableCode(Category)
WHERE Code IS NULL
SELECT * FROM Tbl
GO
DROP TABLE Tbl;
DROP FUNCTION dbo.GetNextAvailableCode;
Here is the result I'm getting:
What I'd like to get is the following:
But it's possible only if next function call can see already changed values...
Any idea how to implement such thing?
Since it is not possible to use function to achieve the desired effect, I re-wrote the update to be used without function and it worked!
Here is the update which produces the result I need:
;WITH data AS (
SELECT Id, Category, Code, ROW_NUMBER() OVER (PARTITION BY Category ORDER BY Id) AS RowNo
FROM dbo.Tbl
WHERE Code IS NULL
)
, maxData AS (
SELECT Category, MAX(Code) AS MaxCode
FROM dbo.Tbl
GROUP BY Category
)
UPDATE S
SET Code = ISNULL(T.MaxCode, 10000) + S.RowNo
FROM data S JOIN maxData T ON T.Category = S.Category
WHERE S.Code IS NULL
Result:

How to get the root in a hierarchy query using SQL Server from any level of Hierarchy

I would like to get the Top most Ancestor (Root) of the hierarchy from any level of data.
The following is my table.
CREATE TABLE #SMGROUP (ID INT NOT NULL, GRP NVARCHAR(40), GRPCLASS INT, PARENTGRP NVARCHAR(40), PARENTGRPCLASS INT)
INSERT INTO #SMGROUP VALUES (1, 'A', 1, NULL,NULL)
INSERT INTO #SMGROUP VALUES (1, 'B', 1, NULL,NULL)
INSERT INTO #SMGROUP VALUES (1, 'C', 1, NULL,NULL)
INSERT INTO #SMGROUP VALUES (1, 'A.1', 2, 'A',1)
INSERT INTO #SMGROUP VALUES (1, 'A.2', 2, 'A',1)
INSERT INTO #SMGROUP VALUES (1, 'A.3', 2, 'A',1)
INSERT INTO #SMGROUP VALUES (1, 'B.1', 2, 'B',1)
INSERT INTO #SMGROUP VALUES (1, 'B.2', 2, 'B',1)
INSERT INTO #SMGROUP VALUES (1, 'A.3.3', 3, 'A.3',2)
INSERT INTO #SMGROUP VALUES (1, 'A.3.3.3', 4, 'A.3.3',3)
INSERT INTO #SMGROUP VALUES (1, 'A.3.3.3.1', 5, 'A.3.3.3',4)
INSERT INTO #SMGROUP VALUES (1, 'B.1.2', 3, 'B.1',2)
INSERT INTO #SMGROUP VALUES (1, 'B.2.1', 3, 'B.2', 2)
SELECT * FROM #SMGROUP
I Would like to have the value of - 'A' if I provide 'A.1' as input, also the return value would be 'A' if I provide 'A.3.3' as input. Also the return would be 'A' if the parameter is 'A.3.3.3.1'
I have written some thing like this, but I am not sure how to continue after this.
;WITH items AS (
SELECT G.GRP ,CAST('' AS NVARCHAR(30)) AS ParentGroup,
0 AS Level
FROM #SMGROUP G
WHERE G.PARENTGRP IS NULL
UNION ALL
SELECT G.GRP, CAST(G.PARENTGRP AS NVARCHAR(30)) AS ParentGroup
, Level + 1
FROM #SMGROUP G
INNER JOIN items itms ON itms.GRP = G.PARENTGRP
)
SELECT * FROM items
You are on the right direction, you just need one last push.
Instead of using a "standard" recursive cte that traverse from root to leaf nodes, you "reverse" the process and traverse from the input node back to the root.
Then it's simply a top 1 with level desc in the order by clause:
DECLARE #GRP NVARCHAR(40) = 'A.3.3.3.1';
WITH items AS (
SELECT G.GRP,
ISNULL(G.PARENTGRP, '') AS ParentGroup,
0 AS Level
FROM #SMGROUP G
WHERE G.GRP = #GRP
UNION ALL
SELECT G.GRP,
G.PARENTGRP,
Level + 1
FROM #SMGROUP G
INNER JOIN items itms
ON itms.ParentGroup = G.GRP
)
SELECT TOP 1 Grp
FROM items
ORDER BY Level DESC

How to make results from previous rows values in TSQL or Power Query

I have a table, in the table i have for example 4 columns
(ID(for example 12 different ID repeating for 10000 rows but then +26 added for the next 10000 rows),
Date (ordered by this -can't be changed-),
Error(-1,0,1),
ItemName,
What should i do if i want to make my query warn me if there were 3 error for the same ID right after each other
(It's not sure that the rows are actually right after each other, because the whole table ordered by Date, so ID 1 can be in the first row and then the 13th and then the 25th for example) ?
According to what I understand, you need Items which has 3 errors and it is in consecutive lines on order by date.
Please refer my code and this support sql server
/*create table*/
CREATE TABLE YourTable( PK_Id int NOT NULL IDENTITY(1,1) primary key,
ID int NOT NULL,
CreatedDate datetime NOT NULL,
Error int NOT NULL,
ItemName nvarchar(100) NULL);
/*sample data 1*/
INSERT INTO YourTable VALUES (1, '2018-10-13 10:10:10', -1, 'Item-1'),
(2, '2018-10-13 10:10:15', -1, 'Item-2'),
(3, '2018-10-13 10:10:17', -1, 'Item-3'),
(4, '2018-10-13 10:10:17', -1, 'Item-4'),
(1, '2018-10-14 10:10:10', 0, 'Item-1'),
(2, '2018-10-14 10:10:15', 0, 'Item-2'),
(3, '2018-10-14 10:10:17', 0, 'Item-3'),
(4, '2018-10-14 10:10:17', 0, 'Item-4'),
(1, '2018-10-15 10:10:10', 1, 'Item-1'),
(2, '2018-10-15 10:10:15', 1, 'Item-2'),
(3, '2018-10-15 10:10:17', 1, 'Item-3'),
(4, '2018-10-15 10:10:17', 1, 'Item-4')
/*sample data 2*/
INSERT INTO YourTable VALUES (5, '2018-10-16 10:10:10', -1, 'Item-5'),
(5, '2018-10-16 10:10:15', 0, 'Item-5'),
(5, '2018-10-16 10:10:17', 1, 'Item-5')
SELECT Id, ItemName
FROM YourTable
GROUP BY Id, ItemName
HAVING COUNT(Error) = 3 /*check number of errors*/
AND SUM(Error) = 0 /*check all 3 errors*/
AND SUM(PK_Id) = MIN(PK_Id) * 3 + 3 /*check right after each error*/
This should get you started; use the LAG() function in SQL Server 2012+ to determine the value in the previous row, and the row before that (there may be a way to do this in one pass rather than two, like I'm doing). This will return the last row in a sequence of three errors (assuming -1 is the error code).
USE tempdb;
CREATE TABLE Logs (ID int, Dt datetime, error int, itemname varchar(20))
CREATE CLUSTERED INDEX cx ON LOGS (dt)
INSERT INTO LOGS
VALUES (1, '20180101', -1, 'error'),
(1, '20180102', -1, 'error'),
(1, '20180103', -1, 'error'),
(1, '20180104', 0, 'no error'),
(2, '20180102', -1, 'error'),
(2, '20180103', -1, 'error'),
(2, '20180104', 0, 'no error'),
(2, '20180105', -1, 'error')
; with c AS (
SELECT *
, LAG(error, 1,0) OVER (PARTITION BY ID ORDER BY Dt) prv1
, LAG(error, 2,0) OVER (PARTITION BY ID ORDER BY Dt) prv2
FROM Logs
)
SELECT *
FROM c
WHERE error = prv1 and error = prv2
AND error = -1
DROP TABLE Logs

SQL Server: String insertion by position

I want to insert a set of values into a given string at specified positions. I couldn't find an existing solution for this so I created the following query which does so. I am wondering if there is a way to simplify it, especially the portion which queries the cte and creates the NewString. Also, is there a way to construct the cte recursion without involving the ID column? The ID (and the rest) seems to work fine, just wondering if it could be tweaked to make it more organic/elegant or if there is a better solution overall. The different groups are really only for testing purposes; each group corresponds to a possible insertion position scenario. Thus, there will not be groups involved when I use it.
declare
#String varchar(200),
#StringLen int,
#GroupID int,
#PositionMax int
declare #Chars table (
ID int identity,
GroupID int,
Position int,
Value varchar(20)
)
select
#String = 'abcde',
#StringLen = len(#String),
#GroupID = 1
--Affix
--[P]refix
--[I]nfix
--[S]uffix
insert #Chars
select
GroupID,
Position,
Value
from (
values
(1, 0, 'X'), --P
(2, 2, 'Y'), --I
(3, 5, 'Z'), --S
(4, 0, 'X'), --P
(4, 2, 'Y'), --I
(5, 2, 'Y'), --I
(5, 5, 'Z'), --S
(6, 0, 'X'), --P
(6, 5, 'Z'), --S
(7, 0, 'X'), --P
(7, 2, 'Y'), --I
(7, 5, 'Z'), --S
(8, 2, 'Y1'), --I
(8, 4, 'Y2'), --I
(9, 0, 'X'), --P
(9, 2, 'Y1'), --I
(9, 4, 'Y2'), --I
(10, 2, 'Y1'), --I
(10, 4, 'Y2'), --I
(10, 5, 'Z'), --S
(11, 0, 'X'), --P
(11, 2, 'Y1'), --I
(11, 4, 'Y2'), --I
(11, 5, 'Z') --S
) as T(GroupID, Position, Value)
order by GroupID, Position
;with cte (
ID,
GroupID,
LeftString,
Value,
RightString,
Position
) as (
select
ID,
GroupID,
LeftString,
Value,
RightString,
Position
from (
select
row_number() over (partition by GroupID order by ID) as RowNumber,
ID,
GroupID,
cast(left(#String, Position) as varchar(200)) as LeftString,
Value,
cast(right(#String, #StringLen - Position) as varchar(200)) as RightString,
Position
from #Chars
) as C
where RowNumber = 1
union all
select
vc.ID,
vc.GroupID,
cast(left(RightString, vc.Position - cte.Position) as varchar(200)) as LeftString,
vc.Value,
cast(right(RightString, #StringLen - vc.Position) as varchar(200)) as RightString,
vc.Position
from #Chars vc
join cte cte
on cte.GroupID = vc.GroupID
and cte.ID + 1 = vc.ID
)
select
GroupID,
case
when LSLenSumMax < #StringLen
then NewString + RightString
else NewString
end as NewString
from (
select
GroupID,
max(LSLenSum) as LSLenSumMax,
RightString,
stuff((
select
LeftString + Value
from cte cte
where cte.GroupID = cteLR.GroupID
for xml path(''), type
).value('(./text())[1]', 'varchar(max)'), 1, 0, '') as NewString
from (
select
GroupID,
(select sum(len(LeftString)) from cte cteL where cteL.groupID = cte.groupID) as LSLenSum,
(select top 1 RightString from cte cteR where cteR.groupID = cte.groupID order by cteR.ID desc) as RightString
from cte cte
) as cteLR
group by
GroupID,
RightString
) as C
order by GroupID
You can implement a custom
aggregate function... Or try this: A recursive scalar function:
CREATE FUNCTION dbo.GetPrefixTo (#GroupID INT, #Position INT, #String CHAR(200))
RETURNS Char(200)
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #str CHAR(200) = NULL
DECLARE #beforePosition INT = 0
IF (SELECT COUNT(*) FROM Chars WHERE GroupID = #GroupID AND Position < #Position) > 0
BEGIN
SELECT #beforePosition = MAX(Position) FROM chars WHERE GroupID = #GroupID AND Position < #Position
select #str = RTRIM(dbo.GetPrefixTo(#GroupID, #beforePosition, substring(#String, 1, #Position))) +
+ RTrim(Value) + substring(#String, #Position + 1, len(#string) + 1)
FROM Chars WHERE GroupID = #GroupID AND Position = #Position
END
ELSE
SELECT #str = substring(#String, 1, #Position) + RTrim(Value) + substring(#String, #Position + 1, len(#string) + 1) FROM Chars
WHERE GroupID = #GroupID AND Position = #Position
RETURN #str
END
And group by GroupID and aggregate max(position):
SELECT groupID, max(Position)
, dbo.GetPrefixTo(groupID, max(Position), 'abcde')
FROM Chars
GROUP BY GroupID
This is the result:
1 0 Xabcde
2 2 abYcde
3 5 abcdeZ
4 2 XabYcde
5 5 abYcdeZ
6 5 XabcdeZ
7 5 XabYcdeZ
8 4 abY1cdY2e
9 4 XabY1cdY2e
10 5 abY1cdY2eZ
11 5 XabY1cdY2eZ
========================================================================

MSSQL 2012 - How to combine multiple rows and columns into single row

My records are look like below:
I want to combine all multiple rows and columns into single row as:
109,0,0|123,1,1|174,0,0|321,0,0........
Each row combined will separate with a pipe and there no pipe separator for the last row.
Currently I'm using MSSQL 2012. Any help are much appreciate.
Here is a method with xml:
DECLARE #t TABLE
(
idno INT ,
idfound INT,
pofound int
)
INSERT INTO #t VALUES
(109, 0, 0),
(123, 1, 1),
(174, 0, 0),
(321, 0, 0),
(456, 0, 1),
(509, 0, 0),
(654, 0, 1),
(687, 0, 1),
(789, 0, 0),
(987, 0, 0)
;WITH cte AS(SELECT CAST(idno AS VARCHAR(max)) + ',' +
CAST(idfound AS VARCHAR(max)) + ',' +
CAST(pofound AS VARCHAR(max)) AS col FROM #t)
SELECT STUFF((SELECT '|' + col FROM cte
FOR XML PATH('')), 1, 1, '')
Output:
109,0,0|123,1,1|174,0,0|321,0,0|456,0,1|509,0,0|654,0,1|687,0,1|789,0,0|987,0,0
DECLARE #Result VARCHAR(MAX) --To store the result
SELECT
#Result = CONCAT(#Result+'|',CONCAT(idno,',',idfound,',',pofound))
FROM YourTable
SELECT #Result
Here I used the #Result+'|' in the code to avoid adding pipe before the first row data
Output
109,0,0|123,1,1|174,0,0|321,0,0|456,0,1|509,0,0|654,0,1|687,0,1|789,0,0|987,0,0
Note
CONCAT treats NULL value as Empty string where as '+' operator wont, that is because i used both in the query

Resources