Rank based on cumulative value - sql

I want to rank on ID and value columns based on ascending order of UID. Expected output has to change once value column has a different value than the previous value. Ranks has to restart on each new ID
UID ID Value Expected Output
1 1 0 1
2 1 0 1
3 1 1 2
4 1 1 2
5 1 1 2
6 1 0 3
7 1 1 4
8 1 0 5
9 1 0 5
10 1 0 5
11 2 1 1
12 2 1 1
13 2 0 2
14 2 0 2
15 2 1 3
Here is a sample dataset that I have created:
CREATE TABLE [dbo].[Data] (
[UID] [int] NOT NULL,
[ID] [int] NULL,
[Value] [int] NULL
);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (1, 1, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (2, 1, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (3, 1, 1);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (4, 1, 1);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (5, 1, 1);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (6, 1, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (7, 1, 1);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (8, 1, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (9, 1, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (10, 1, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (11, 2, 1);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (12, 2, 1);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (13, 2, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (14, 2, 0);
INSERT [dbo].[Data] ([UID], [ID], [Value]) VALUES (15, 2, 1);

I think that the simplest approach to this gaps-and-islands problem is to use lag() to retrieve the "previous" value, and then a window sum that increments everytime the value changes.
select uid, id, value,
1 + sum(case when value <> lag_value then 1 else 0 end)
over(partition by id order by uid) grp
from (
select d.*, lag(value, 1, value) over(partition by id order by uid) lag_value
from data d
) d
order by uid
Demo on DB Fiddle:
uid | id | value | grp
--: | -: | ----: | --:
1 | 1 | 0 | 1
2 | 1 | 0 | 1
3 | 1 | 1 | 2
4 | 1 | 1 | 2
5 | 1 | 1 | 2
6 | 1 | 0 | 3
7 | 1 | 1 | 4
8 | 1 | 0 | 5
9 | 1 | 0 | 5
10 | 1 | 0 | 5
11 | 2 | 1 | 1
12 | 2 | 1 | 1
13 | 2 | 0 | 2
14 | 2 | 0 | 2
15 | 2 | 1 | 3

This is a gaps and islands problem. I think that the simplest approach is to use the difference in row numbers method:
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (PARTITION BY ID ORDER BY UID) rn1,
ROW_NUMBER() OVER (PARTITION BY ID, [Value] ORDER BY UID) rn2
FROM Data
)
SELECT *, DENSE_RANK() OVER (PARTITION BY ID ORDER BY rn1 - rn2, [Value]) AS output
FROM cte
ORDER BY UID;
Demo

Related

Join on existing aggregate query to pivot result without id

On a Sql-Server instance, I have three tables:
ActionItem
Id
Name
1
Fish
2
Gravy
3
Pants
ActionData
Id
ActionId
Group
Field
Value
1
1
1
1
100
2
1
1
2
200
3
1
1
3
300
4
1
1
4
NULL
5
1
1
5
NULL
6
1
2
6
"Some Text"
7
2
1
1
50
8
2
1
2
60
9
2
1
3
70
Costing
Id
ActionId
Break
Cost
1
1
Normal
11.3
2
1
Sub
54
3
1
Premium
0.4
4
3
Normal
22
5
3
Premium
0.67
I have a query that sums the cost for each ActionItem:
select
ai.Id,
ai.Name,
sum(c.Cost)
from ActionItem ai
left join Costing c on ai.Id = c.ActionId
group by
ai.Id,
ai.Name
Nice and straight-forward:
Id
Name
(No column name)
1
Fish
65.7
2
Gravy
NULL
3
Pants
22.67
I created a pivot too:
select * from
(select [ActionId], [Group], [Field], [Value] from ActionData) src
pivot (max([Value]) for [ActionId] in ([1],[2],[3],[4])) ppp
Which gets me data in the right format:
Group
Field
1
2
3
4
1
1
100
50
NULL
NULL
1
2
200
60
NULL
NULL
1
3
300
70
NULL
NULL
1
4
NULL
NULL
NULL
NULL
1
5
NULL
NULL
NULL
NULL
2
6
"Some Text"
NULL
NULL
NULL
But I cannot join these two queries together because that PIVOT doesn't contain the ActionId ... even though I use Select * from - how can I get the ActionId col to show on my pivoted data, so I can join it to the rest of my original query?
I could not get sqlfiddle.com to work for MS SQL SERVER today but here are create and inserts if anyone's interested:
CREATE TABLE ActionItem
([Id] int, [Name] varchar(5));
INSERT INTO ActionItem
([Id], [Name])
VALUES
(1, 'Fish'),
(2, 'Gravy'),
(3, 'Pants');
CREATE TABLE ActionData
([Id] int, [ActionId] int, [Group] int, [Field] int, [Value] varchar(11));
INSERT INTO ActionData
([Id], [ActionId], [Group], [Field], [Value])
VALUES
(1, 1, 1, 1, '100'),
(2, 1, 1, 2, '200'),
(3, 1, 1, 3, '300'),
(4, 1, 1, 4, NULL),
(5, 1, 1, 5, NULL),
(6, 1, 2, 6, '"Some Text"'),
(7, 2, 1, 1, '50'),
(8, 2, 1, 2, '60'),
(9, 2, 1, 3, '70')
;
CREATE TABLE Costing (
[Id] int,
[ActionId] int,
[Break] VARCHAR(9),
[Cost] FLOAT);
INSERT INTO Costing
([Id], [ActionId], [Break], [Cost])
VALUES
('1', '1', 'Normal', '11.3'),
('2', '1', 'Sub', '54'),
('3', '1', 'Premium', '0.4'),
('4', '3', 'Normal', '22'),
('5', '3', 'Premium', '0.67');
Not sure what output you expect.
But here's an attempt to join the two queries in 1 pivot.
select pvt.*
from
(
select d.ActionId, ai.Name
--, d.[Group]
, cast(d.[Field] as varchar(30)) as [Col]
, try_cast(d.[Value] as float) as [Value]
from ActionData d
left join ActionItem ai on ai.Id = d.ActionId
where isnumeric(d.[Value]) = 1
union all
select c.ActionId, ai.Name
--, 1 as [Group]
, c.[Break] as [Col]
, sum(c.Cost) as TotalCost
from Costing c
left join ActionItem ai
on ai.Id = c.ActionId
group by c.ActionId, ai.Name, c.[Break]
) src
pivot (
max([Value])
for [Col] in ([1],[2],[3],[4],[Normal],[Premium],[Sub])
) pvt
GO
ActionId | Name | 1 | 2 | 3 | 4 | Normal | Premium | Sub
-------: | :---- | ---: | ---: | ---: | ---: | -----: | ------: | ---:
1 | Fish | 100 | 200 | 300 | null | 11.3 | 0.4 | 54
2 | Gravy | 50 | 60 | 70 | null | null | null | null
3 | Pants | null | null | null | null | 22 | 0.67 | null
db<>fiddle here

How to change a value rank in a column MS SQL

I've a table with a column which is defining a rank value for display position:
Unid | Rank | Name
10 | 1 | A
20 | 2 | B
30 | 3 | C
40 | 4 | D
50 | 5 | E
How to update the table for have Name E on the top of the list and followed by the A, B , C , D names ?
One possible solution is to use ROW_NUMBER() with appropriate ORDER BY clause:
Table:
CREATE TABLE Data (
[Unid] int,
[Rank] int,
[Name] varchar(1)
)
INSERT INTO Data ([Unid], [Rank], [Name])
VALUES
(10, 1, 'A'),
(20, 2, 'B'),
(30, 3, 'C'),
(40, 4, 'D'),
(50, 5, 'E')
Statement:
UPDATE d
SET d.[Rank] = d.[NewRank]
FROM (
SELECT
[Rank],
ROW_NUMBER() OVER (ORDER BY CASE WHEN [Name] = 'E' THEN 0 ELSE 1 END, [Name]) AS [NewRank]
FROM Data
) d
Result:
Unid Rank Name
10 2 A
20 3 B
30 4 C
40 5 D
50 1 E

how to create while loop in select?

I have two columns
I want to output query like
I try to use loop in SQL but I failed to output query
My question is: how can I repeat the values in the [Name] column by the number in the [Total] column?
This is usually handled with a recursive subquery. I derived a row order in case of duplicate names, however, that is best served using a unique key.
SQL Fiddle
MS SQL Server 2017 Schema Setup:
CREATE TABLE Names(name NVARCHAR(50), total INT)
INSERT Names VALUES('ahmed',3),('mahmoud',2),('ahmed',5)
Query 1:
;WITH Normalized AS
(
SELECT *, RowNumber = ROW_NUMBER() OVER (ORDER BY (SELECT 0)) FROM Names
)
,ReplicateAmount AS
(
SELECT name, running_total=total, total, RowNumber
FROM Normalized
UNION ALL
SELECT R.name, running_total=(R.running_total - 1), R.total , R.RowNumber
FROM ReplicateAmount R INNER JOIN Normalized N ON R.RowNumber = N.RowNumber
WHERE R.running_total > 1
)
SELECT
name,instance=RowNumber,total=1,OriginalTotal=total,running_total
FROM
ReplicateAmount
ORDER BY
RowNumber,name,total,running_total
OPTION (MAXRECURSION 0)
Results:
| name | instance | total | OriginalTotal | running_total |
|---------|----------|-------|---------------|---------------|
| ahmed | 1 | 1 | 3 | 1 |
| ahmed | 1 | 1 | 3 | 2 |
| ahmed | 1 | 1 | 3 | 3 |
| mahmoud | 2 | 1 | 2 | 1 |
| mahmoud | 2 | 1 | 2 | 2 |
| ahmed | 3 | 1 | 5 | 1 |
| ahmed | 3 | 1 | 5 | 2 |
| ahmed | 3 | 1 | 5 | 3 |
| ahmed | 3 | 1 | 5 | 4 |
| ahmed | 3 | 1 | 5 | 5 |
You can do this considerably simpler than the accepted answer.
I would suggest creating a numbers table with sequential positive integers...
CREATE TABLE dbo.Numbers(Number INT PRIMARY KEY);
INSERT INTO dbo.Numbers
SELECT TOP 10000 ROW_NUMBER() OVER (ORDER BY ##SPID)
FROM sys.all_objects o1,
sys.all_objects o2
and then just join on number <= total (as below)
SELECT name, 1 AS total
FROM Names nam
JOIN dbo.Numbers num ON num.Number <= nam.total;
If you are on 2017+ another method for "fun" is below (SQL Fiddle)
SELECT name, 1 AS total
FROM Names
CROSS APPLY STRING_SPLIT(SPACE(total - 1), ' ')
For your closed question;
CREATE TABLE #Test1(
[ID] [int] NULL,
[Cnt] [varchar](50) NULL
) ON [PRIMARY]
GO
CREATE TABLE #Test2(
[ID] [int] NULL,
[col1] [varchar](50) NULL,
[col2] [varchar](50) NULL,
[col3] [varchar](50) NULL
) ON [PRIMARY]
GO
CREATE TABLE #Test3(
[ID1] [int] NULL,
Cnt int,
Id2 int,
[col1] [varchar](50) NULL,
[col2] [varchar](50) NULL,
[col3] [varchar](50) NULL
) ON [PRIMARY]
GO
INSERT INTO #TEST1 (ID, Cnt) VALUES (1, 2)
INSERT INTO #TEST1 (ID, Cnt) VALUES (2, 3)
INSERT INTO #TEST1 (ID, Cnt) VALUES (3, 1)
INSERT INTO #TEST1 (ID, Cnt) VALUES (4, 2)
INSERT INTO #TEST1 (ID, Cnt) VALUES (5, 1)
INSERT INTO #TEST1 (ID, Cnt) VALUES (6, 4)
INSERT INTO #TEST2 (ID, Col1, Col2, Col3) VALUES (1, 'a1', 'b1', 'c1')
INSERT INTO #TEST2 (ID, Col1, Col2, Col3) VALUES (2, 'a2', 'b2', 'c2')
INSERT INTO #TEST2 (ID, Col1, Col2, Col3) VALUES (3, 'a3', 'b3', 'c3')
INSERT INTO #TEST2 (ID, Col1, Col2, Col3) VALUES (4, 'a4', 'b4', 'c4')
INSERT INTO #TEST2 (ID, Col1, Col2, Col3) VALUES (5, 'a5', 'b5', 'c5')
INSERT INTO #TEST2 (ID, Col1, Col2, Col3) VALUES (6, 'a6', 'b6', 'c6')
declare #a int = 1, #b int, #c int = 0;
while #a <= (select MAX(Id) from #Test1)
begin
set #b = (select Cnt from #Test1 where ID = #a);
set #c = 0;
while #c < #b begin
insert into #Test3 (ID1, Cnt, Id2, col1, col2, col3) select #a, #b, #a, col1, col2, col3 from #Test2 where ID = #a;
set #c += 1;
end
SET #a += 1;
end
select * from #Test3 order by 1, 2;

Correlative by group with special criteria

We need to get the result of the column NEED, need the correlative grouped by the GROUP column in the order of the ORDER column and that increases when the FLAG column changes.
GROUP ORDER FLAG NEED
1111 1 0 1
1111 2 0 1
1111 3 1 2
1111 4 1 2
1111 5 1 2
1111 6 1 2
1111 7 1 2
1111 8 0 3
1111 9 1 4
1111 10 1 4
1111 11 0 5
1111 12 0 5
1111 13 0 5
6666 1 0 1
6666 2 0 1
6666 3 1 2
6666 4 1 2
We try the following code, but we need something cleaner with support for SQL Server 2008
if object_id('tempdb..#temp2','u') is not NULL
drop table #temp2
SELECT *
,ROW_NUMBER() OVER(oRDER BY (SELECT NULL)) RN
INTO #temp2
FROM DBO.PRUEBA​
SELECT T1.*
,SUM(CASE WHEN T1.NUM_GROUP = T2.NUM_GROUP and t1.NUM_FLAG = t2.NUM_FLAG THEN 0 ELSE 1 END) OVER (PARTITION BY T1.NUM_GROUP ORDER BY T1.rn)[Rank]
FROM #temp2 T1
LEFT JOIN #temp2 T2 ON T1.rn = T2.rn+1
order by t1.NUM_GROUP, t1.NUM_ORDER
I share the creation of tables and records
CREATE TABLE DBO.PRUEBA
(
NUM_GROUP INT,
NUM_ORDER INT,
NUM_FLAG INT
)
INSERT INTO DBO.PRUEBA VALUES (1111, 1, 0)
INSERT INTO DBO.PRUEBA VALUES (1111, 2, 0)
INSERT INTO DBO.PRUEBA VALUES (1111, 3, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 4, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 5, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 6, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 7, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 8, 0)
INSERT INTO DBO.PRUEBA VALUES (1111, 9, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 10, 1)
INSERT INTO DBO.PRUEBA VALUES (1111, 11, 0)
INSERT INTO DBO.PRUEBA VALUES (1111, 12, 0)
INSERT INTO DBO.PRUEBA VALUES (1111, 13, 0)
INSERT INTO DBO.PRUEBA VALUES (6666, 1, 0)
INSERT INTO DBO.PRUEBA VALUES (6666, 2, 0)
INSERT INTO DBO.PRUEBA VALUES (6666, 3, 1)
INSERT INTO DBO.PRUEBA VALUES (6666, 4, 1)
SELECT * FROM DBO.PRUEBA
A possible optimalization is to first create the temporary table.
Instead of using a SELECT INTO.
And with a primary key that benefits the self-join that's used to get the previous NUM_FLAG.
CREATE TABLE DBO.PRUEBA
(
NUM_GROUP INT NOT NULL,
NUM_ORDER INT NOT NULL,
NUM_FLAG INT NOT NULL,
PRIMARY KEY (NUM_GROUP, NUM_ORDER)
);
GO
INSERT INTO DBO.PRUEBA
(NUM_GROUP, NUM_ORDER, NUM_FLAG)
VALUES
(1111, 1, 0)
,(1111, 2, 0)
,(1111, 3, 1)
,(1111, 4, 1)
,(1111, 5, 1)
,(1111, 6, 1)
,(1111, 7, 1)
,(1111, 8, 0)
,(1111, 9, 1)
,(1111, 10, 1)
,(1111, 11, 0)
,(1111, 12, 0)
,(1111, 13, 0)
,(6666, 1, 0)
,(6666, 2, 0)
,(6666, 3, 1)
,(6666, 4, 1)
IF OBJECT_ID('tempdb..#tmpPRUEBA', 'U') IS NOT NULL
DROP TABLE #tmpPRUEBA;
CREATE TABLE #tmpPRUEBA
(
NUM_GROUP INT NOT NULL,
RN_GROUP INT NOT NULL,
NUM_ORDER INT NOT NULL,
NUM_FLAG INT NOT NULL,
PRIMARY KEY (NUM_GROUP, RN_GROUP)
);
GO
INSERT INTO #tmpPRUEBA
(NUM_GROUP, NUM_ORDER, NUM_FLAG, RN_GROUP)
SELECT NUM_GROUP, NUM_ORDER, NUM_FLAG
, ROW_NUMBER() OVER (
PARTITION BY NUM_GROUP
ORDER BY NUM_ORDER) AS RN_GROUP
FROM DBO.PRUEBA;
GO
SELECT
t1.NUM_GROUP,
t1.NUM_ORDER,
t1.NUM_FLAG,
SUM(CASE
WHEN t1.NUM_FLAG = t2.NUM_FLAG
THEN 0
ELSE 1
END)
OVER (PARTITION BY t1.NUM_GROUP
ORDER BY t1.RN_GROUP) AS [Rank]
FROM #tmpPRUEBA t1
LEFT JOIN #tmpPRUEBA t2
ON t2.NUM_GROUP = t1.NUM_GROUP
AND t2.RN_GROUP = t1.RN_GROUP - 1;
GO
NUM_GROUP | NUM_ORDER | NUM_FLAG | Rank
--------: | --------: | -------: | ---:
1111 | 1 | 0 | 1
1111 | 2 | 0 | 1
1111 | 3 | 1 | 2
1111 | 4 | 1 | 2
1111 | 5 | 1 | 2
1111 | 6 | 1 | 2
1111 | 7 | 1 | 2
1111 | 8 | 0 | 3
1111 | 9 | 1 | 4
1111 | 10 | 1 | 4
1111 | 11 | 0 | 5
1111 | 12 | 0 | 5
1111 | 13 | 0 | 5
6666 | 1 | 0 | 1
6666 | 2 | 0 | 1
6666 | 3 | 1 | 2
6666 | 4 | 1 | 2
db<>fiddle here

select rows where ID has variation of values

I have two columns, an ID, and the other a value which is either 0 or 1. I am trying to select all Rows for the ID where it has a 0 and a 1, for example,
RowNumber ------------- ID ------- value
1 ------------------- 001 ------- 1
2 ------------------- 001 ------- 1
3 ------------------- 001 ------- 1
4 ------------------- 002 ------- 1
5 ------------------- 002 ------- 0
6 ------------------- 003 ------- 1
7 ------------------- 003 ------- 1
8 --------------------004 ------- 1
9 -------------------- 004 ------- 0
10 ------------------- 004 ------- 1
The result should select rows 4, 5, 8, 9, 10
You can use window version of COUNT:
SELECT RowNumber, ID, value
FROM (
SELECT RowNumber, ID, value,
COUNT(CASE WHEN value = 1 THEN 1 END) OVER (PARTITION BY ID) AS cntOnes,
COUNT(CASE WHEN value = 0 THEN 1 END) OVER (PARTITION BY ID) AS cntZeroes
FROM test
WHERE value IN (0,1) ) AS t
WHERE cntOnes >= 1 AND cntZeroes >= 1
COUNT(DISTINCT value) has a value of 2 if both 0, 1 values exist within the same ID slice.
DISTINCT is indeed not allowed in a windowed version of the COUNT, so you can use MIN and MAX instead.
DECLARE #T TABLE(RN int, ID int, value int);
INSERT INTO #T (RN, ID, value) VALUES
(1, 001, 1),
(2, 001, 1),
(3, 001, 1),
(4, 002, 1),
(5, 002, 0),
(6, 003, 1),
(7, 003, 1),
(8, 004, 1),
(9, 004, 0),
(10, 004, 1);
WITH
CTE
AS
(
SELECT
RN, ID, value
,MIN(value) OVER (PARTITION BY ID) AS MinV
,MAX(value) OVER (PARTITION BY ID) AS MaxV
FROM #T AS T
)
SELECT RN, ID, value
FROM CTE
WHERE MinV <> MaxV
;
Result
+----+----+-------+
| RN | ID | value |
+----+----+-------+
| 4 | 2 | 1 |
| 5 | 2 | 0 |
| 8 | 4 | 1 |
| 9 | 4 | 0 |
| 10 | 4 | 1 |
+----+----+-------+
create table #shadowTemp (
RowNumber int not null,
Id char(3) not null,
value bit not null
)
insert into #shadowTemp values ( 1,'001', 0 )
insert into #shadowTemp values ( 2,'001', 1 )
insert into #shadowTemp values ( 3,'001', 1 )
insert into #shadowTemp values ( 4,'002', 0 )
insert into #shadowTemp values ( 5,'003', 0 )
insert into #shadowTemp values ( 6,'003', 1 )
select * from #shadowTemp;
;with cte ( Id ) As (
select Id
from #shadowTemp
group by Id
having sum( value + 1 ) >= 3
)
select a.*
from
#shadowTemp a
inner join cte b on ( a.Id = b.Id )
drop table #shadowTemp