Is it possible to write a sql query that is grouped based on a running total of a column? - sql

It would be easier to explain with an example. Suppose I wanted to get at most 5 items per group.
My input would be a table looking like this:
Item Count
A 2
A 3
A 3
B 4
B 4
B 5
C 1
And my desired output would look like this:
Item Count
A 5
A>5 3
B 4
B>5 9
C 1
An alternative output that I could also work with would be
Item Count RunningTotal
A 2 2
A 3 5
A 3 8
B 4 4
B 4 8
B 5 13
C 1 1
I can use ROW_NUMBER() to get the top X records in each group, however my requirement is to get the top X items for each group, not X records. My mind is drawing a blank as to how to do this.

declare #yourTable table (item char(1), [count] int)
insert into #yourTable
select 'A', 2 union all
select 'A', 3 union all
select 'A', 3 union all
select 'B', 4 union all
select 'B', 4 union all
select 'B', 5 union all
select 'C', 1
;with cte(item, count, row) as (
select *, row_number() over ( partition by item order by item, [count])
from #yourTable
)
select t1.Item, t1.Count, sum(t2.count) as RunningTotal from cte t1
join cte t2 on t1.item = t2.item and t2.row <= t1.row
group by t1.item, t1.count, t1.row
Result:
Item Count RunningTotal
---- ----------- ------------
A 2 2
A 3 5
A 3 8
B 4 4
B 4 8
B 5 13
C 1 1

Considering the clarifications from your comment, you should be able to produce the second kid of output from your post by running this query:
select t.Item
, t.Count
, (select sum(tt.count)
from mytable tt
where t.item=tt.item and (tt.creating_user_priority < t.creating_user_priority or
( tt.creating_user_priority = t.creating_user_priority and tt.created_date < t.createdDate))
) as RunningTotal
from mytable t

declare #yourTable table (item char(1), [count] int)
insert into #yourTable
select 'A', 2 union all
select 'A', 3 union all
select 'A', 3 union all
select 'B', 4 union all
select 'B', 4 union all
select 'B', 5 union all
select 'C', 1
;with cte(item, count, row) as (
select *, row_number() over ( partition by item order by item, [count])
from #yourTable
)
select t1.row, t1.Item, t1.Count, sum(t2.count) as RunningTotal
into #RunTotal
from cte t1
join cte t2 on t1.item = t2.item and t2.row <= t1.row
group by t1.item, t1.count, t1.row
alter table #RunTotal
add GrandTotal int
update rt
set GrandTotal = gt.Total
from #RunTotal rt
left join (
select Item, sum(Count) Total
from #RunTotal rt
group by Item) gt
on rt.Item = gt.Item
select Item, max(RunningTotal)
from #RunTotal
where RunningTotal <= 5
group by Item
union
select a.Item + '>5', total - five
from (
select Item, max(GrandTotal) total
from #RunTotal
where GrandTotal > 5
group by Item
) a
left join (
select Item, max(RunningTotal) five
from #RunTotal
where RunningTotal <= 5
group by Item
) b
on a.Item = b.Item
I've updated the accepted answer and got your desired result.

SELECT Item, SUM(Count)
FROM mytable t
GROUP BY Item
HAVING SUM(Count) <=5
UNION
SELECT Item, 5
FROM mytable t
GROUP BY Item
HAVING SUM(Count) >5
UNION
SELECT t2.Item + '>5', Sum(t2.Count) - 5
FROM mytable t2
GOUP BY Item
HAVING SUM(Count) > 5
ORDER BY 1, 2

select 'A' as Name, 2 as Cnt
into #tmp
union all select 'A',3
union all select 'A',3
union all select 'B',4
union all select 'B',4
union all select 'B',5
union all select 'C',1
select Name, case when sum(cnt) > 5 then 5 else sum(cnt) end Cnt
from #tmp
group by Name
union
select Name+'>5', sum(cnt)-5 Cnt
from #tmp
group by Name
having sum(cnt) > 5
Here is what I have so far. I know it's not complete but... this should be a good starting point.

I can get your second output by using a temp table and an update pass:
DECLARE #Data TABLE
(
ID INT IDENTITY(1,1) PRIMARY KEY
,Value VARCHAR(5)
,Number INT
,Total INT
)
INSERT INTO #Data (Value, Number) VALUES ('A',2)
INSERT INTO #Data (Value, Number) VALUES ('A',3)
INSERT INTO #Data (Value, Number) VALUES ('A',3)
INSERT INTO #Data (Value, Number) VALUES ('B',4)
INSERT INTO #Data (Value, Number) VALUES ('B',4)
INSERT INTO #Data (Value, Number) VALUES ('B',5)
INSERT INTO #Data (Value, Number) VALUES ('C',1)
DECLARE
#Value VARCHAR(5)
,#Count INT
UPDATE #Data
SET
#Count = Total = CASE WHEN Value = #Value THEN Number + #Count ELSE Number END
,#Value = Value
FROM #Data AS D
SELECT
Value
,Number
,Total
FROM #Data
There may be better ways, but this should work.

Related

Case when duplicate add one more letter

For example: I have a table with these records below
1 A
2 A
3 B
4 C
...
and I need to migrate these record in to another table
1 AA
2 AB
3 B
4 C
...
Meaning if the record is duplicate, it will automatically add one more letter alphabetically.
Just a slightly different approach
Example
Declare #YourTable Table (ID int,[SomeCol] varchar(50))
Insert Into #YourTable Values
(1,'A')
,(2,'A')
,(3,'B')
,(4,'C')
Select *
,NewVal = concat(SomeCol,IIF(sum(1) over (partition by SomeCol)=1,'',char(64+row_number() over ( partition by SomeCol order by ID ))) )
From #YourTable
Returns
ID SomeCol NewVal
1 A AA
2 A AB
3 B B
4 C C
EDIT - Requested UPDATE
Declare #YourTable Table (ID int,[SomeCol] varchar(50))
Insert Into #YourTable Values
(1,'A')
,(2,'A')
,(3,'B')
,(4,'C')
Select *
,NewVal = concat(SomeCol,IIF(sum(1) over (partition by SomeCol)=1,'',replace(char(63+row_number() over ( partition by SomeCol order by ID )),'#','')) )
From #YourTable
Returns
ID SomeCol NewVal
1 A A
2 A AA
3 B B
4 C C
We might be able to handle this requirement with the help of a calendar table mapping secondary letters to duplicate sequence counts:
WITH letters AS (
SELECT 1 AS seq, 'A' AS let UNION ALL
SELECT 2, 'B' UNION ALL
SELECT 3, 'C' UNION ALL
...
SELECT 26, 'Z' UNION ALL
...
),
cte AS (
SELECT id, let, ROW_NUMBER() OVER (PARTITION BY let ORDER BY id) rn,
COUNT(*) OVER (PARTITION BY let) cnt
FROM yourTable
)
SELECT t1.id, t1.let + CASE WHEN t1.cnt > 1 THEN t2.let ELSE '' END AS let
FROM cte t1
LEFT JOIN letters t2
ON t1.id = t2.seq
ORDER BY t1.id;
Demo

Complex join in sql with top 10 row

Table1:
Id Word Frequency
1 A 1
2 B 5
Table2:
Id Word SecondWord SecondFrequency
1 A A1 1
2 A A2 5
3 A A3 10
4 A A4 9
5 A A5 20
6 B B1 5
7 B B2 8
8 B B3 50
9 B B4 40
10 B B5 68
Required output
Top 3 record from “Table2” with Order by SecondFrequency Desc
Ex.
Word Frequency SecondWord SecondFrequency
A 1 A5 20
A 1 A3 10
A 1 A4 9
B 5 B5 68
B 5 B3 50
B 5 B4 40
How can i get the desire output
Use ROWNUMBER function based on second frequency for get you required result:
CREATE TABLE #Table1(Id TINYINT, Word VARCHAR(1),Frequency TINYINT)
CREATE TABLE #Table2(Id TINYINT, Word VARCHAR(1),SecondWord
VARCHAR(2),SecondFrequency TINYINT)
INSERT INTO #Table1(Id, Word ,Frequency)
SELECT 1,'A',1 UNION ALL
SELECT 2,'B',5
INSERT INTO #Table2(Id, Word ,SecondWord ,SecondFrequency)
SELECT 1,'A','A1',1 UNION ALL
SELECT 2,'A','A2',5 UNION ALL
SELECT 3,'A','A3',10 UNION ALL
SELECT 4,'A','A4',9 UNION ALL
SELECT 5,'A','A5',20 UNION ALL
SELECT 6,'B','B1',5 UNION ALL
SELECT 7,'B','B2',8 UNION ALL
SELECT 8,'B','B3',50 UNION ALL
SELECT 9,'B','B4',40 UNION ALL
SELECT 10,'B','B5',68
SELECT *
FROM
(
SELECT ROW_NUMBER() OVER(PARTITION BY #Table1.Word ORDER BY
SecondFrequency DESC ) RNo ,#Table1.Word ,#Table1.Frequency,
SecondWord ,SecondFrequency
FROM #Table1
JOIN #Table2 ON #Table1.Word = #Table2.Word
) A
WHERE RNo BETWEEN 1 AND 3
you can use Row Number. By using Row Number you can give each row with the same 'word' a number based on their SecondFrequency. those number will be reset if the 'word' is changed.
;with cte as
(
select *, ROW_NUMBER() OVER (PARTITION BY Word ORDER BY SecondFrequency DESC) AS RowNumber from table2
)
select A.Word, B.Frequency, A.SecondWord, A.SecondFrequency
from cte A left join table1 B
on A.Word = B.Word
where A.RowNumber < 4
Inner Join with Row_Number() will help in this case !!!
CREATE TABLE #Table1
(
Id INT
,Word VARCHAR(10)
,Frequency INT
)
INSERT INTO #Table1 SELECT 1,'A',1
UNION SELECT 2,'B',5
CREATE TABLE #Table2
(
Id INT
,Word VARCHAR(10)
,SecondWord VARCHAR(10)
,SecondFrequency INT
)
INSERT INTO #Table2 SELECT
1,'A','A1',1 UNION ALL SELECT
2,'A','A2',5 UNION ALL SELECT
3,'A','A3',10 UNION ALL SELECT
4,'A','A4',9 UNION ALL SELECT
5,'A','A5',20 UNION ALL SELECT
6,'B','B1',5 UNION ALL SELECT
7,'B','B2',8 UNION ALL SELECT
8,'B','B3',50 UNION ALL SELECT
9,'B','B4',40 UNION ALL SELECT
10,'B','B5',68
SELECT * FROM #Table1
SELECT * FROM #Table2
SELECT X.Word,X.Frequency,X.SecondWord,X.SecondFrequency
FROM
(SELECT T1.Word,T1.Frequency,T2.SecondWord,T2.SecondFrequency,ROW_NUMBER() OVER(PARTITION BY T1.WORD ORDER BY T2.SecondFrequency desc) as RN
FROM #Table1 T1
JOIN #Table2 T2
ON T1.Word = T2.Word
) AS X
WHERE X.RN<=3
get the top 3 rows from Table_2
join the Table_1
the syntax is : ROW_NUMBER() OVER(PARTITION BY COL1 ORDER BY COL2) AS num
COL1 is the column to group and COL2 is the column to sort , num is the sorted number to be used to limit the results
SELECT t2.Word,
t1.Frequency,
t2.SecondWord,
t2.SecondFrequency
FROM
(SELECT *
FROM
(SELECT Word,
SecondWord,
SecondFrequency,
ROW_NUMBER() over(PARTITION BY Word
ORDER BY SecondFrequency DESC) AS num
FROM Table_2) T
WHERE T.num <= 3 ) t2
JOIN Table_1 AS t1 ON t2.Word = t1.Word
ORDER BY t2.SecondFrequency DESC;

SQL Grouping by first digit from sets of record

I need your help in SQL
I have a set of records of Cost center ID below.
what I want to do is to segregate/group them by inserting column to distinguish the category.
as you can see all digits start in 7 is belong to the bold digits.
my expected out is on below image also.
You can as the below:
DECLARE #Tbl TABLE (ID INT)
INSERT INTO #Tbl
VALUES
(735121201),
(735120001),
(5442244),
(735141094),
(735141097),
(4008060),
(735117603),
(40100000),
(735142902),
(735151199),
(4010070)
;WITH TableWithRowId
AS
(
SELECT
ROW_NUMBER() OVER (ORDER BY(SELECT NULL)) RowId,
ID
FROM
#Tbl
), TempTable
AS
(
SELECT T.RowId + 1 AS RowId FROM TableWithRowId T
WHERE
LEFT(T.ID, 1) != 7
), ResultTable
AS
(
SELECT
T.RowId ,
T.ID,
DENSE_RANK() OVER (ORDER BY (SELECT TOP 1 A.RowId FROM TempTable A WHERE A.RowId > T.RowId ORDER BY A.RowId)) AS Flag
FROM TableWithRowId T
)
SELECT * FROM ResultTable
Result:
RowId ID Flag
----------- ----------- ----------
1 735121201 1
2 735120001 1
3 5442244 1
4 735141094 2
5 735141097 2
6 4008060 2
7 735117603 3
8 40100000 3
9 735142902 4
10 735151199 4
11 4010070 4
The following query is similer with NEER's
;WITH test_table(CenterID)AS(
SELECT '735121201' UNION ALL
SELECT '735120001' UNION ALL
SELECT '5442244' UNION ALL
SELECT '735141094' UNION ALL
SELECT '735141097' UNION ALL
SELECT '4008060' UNION ALL
SELECT '735117603' UNION ALL
SELECT '40100000' UNION ALL
SELECT '735142902' UNION ALL
SELECT '735151199' UNION ALL
SELECT '4010070'
),t1 AS (
SELECT *,ROW_NUMBER()OVER(ORDER BY(SELECT 1)) AS rn,CASE WHEN LEFT(t.CenterID,1)='7' THEN 1 ELSE 0 END AS isSeven
FROM test_table AS t
),t2 AS(
SELECT t1.*,ROW_NUMBER()OVER(ORDER BY t1.rn) AS toFilter
FROM t1 LEFT JOIN t1 AS pt ON pt.rn=t1.rn-1
WHERE pt.CenterID IS NULL OR (t1.isSeven=1 AND pt.isSeven=0)
)
SELECT t1.CenterID,x.toFilter FROM t1
CROSS APPLY(SELECT TOP 1 t2.toFilter FROM t2 WHERE t2.rn<=t1.rn ORDER BY rn desc) x
CenterID toFilter
--------- --------------------
735121201 1
735120001 1
5442244 1
735141094 2
735141097 2
4008060 2
735117603 3
40100000 3
735142902 4
735151199 4
4010070 4

SQL - Group by to get sum but also return a row if the sum is 0

I have the following table:
ID BuyOrSell Total
4 B 10
4 B 11
4 S 13
4 S 29
8 B 20
9 S 23
What I am trying to do is to have sum of B and S columns for each ID and if there is not a B or S for an ID have a row with 0, so expected output would be
ID BuyOrSell Total
4 B 21
4 S 42
8 B 20
8 S 0
9 S 23
9 B 0
I have tried this and it is kind of doing what I am after but not exactly:
DECLARE #Temp Table (ID int, BuyOrSell VARCHAR(1), charge Decimal)
INSERT INTO #Temp
SELECT 4, 'B', 10 UNION ALL
SELECT 4, 'B', 11 UNION ALL
SELECT 4, 'S', 13 UNION ALL
SELECT 4, 'S', 29 UNION ALL
SELECT 8, 'B', 20 UNION ALL
SELECT 9, 'S', 23
;With Results AS
(
SELECT ID,
BuyOrSell,
SUM(charge) AS TOTAL
FROM #Temp
Group by ID, BuyOrSell
)
Select t.*,max(
case when BuyOrSell = 'B' then 'Bfound'
end) over (partition by ID) as ref
,max(
case when BuyOrSell = 'S' then 'Sfound'
end) over (partition by ID) as ref
FROM Results t;
Thanks
Try this:
;WITH CTE(ID, BuyOrSell) AS(
SELECT
ID, T.BuyOrSell
FROM #Temp
CROSS JOIN(
SELECT 'B' UNION ALL SELECT 'S'
)T(BuyOrSell)
GROUP BY ID, T.BuyOrSell
)
SELECT
C.ID,
C.BuyOrSell,
Total = ISNULL(SUM(T.charge), 0)
FROM CTE C
LEFT JOIN #Temp T
ON T.ID = C.ID
AND T.BuyOrSell = C.BuyOrSell
GROUP BY C.ID, C.BuyOrSell
ORDER BY C.ID, C.BuyOrSell
#03Usr, despite that your question has been answered, please try this:
SELECT two.ID,
two.BuyOrSell,
ISNULL (one.Total, 0) Total
FROM
(SELECT ID,
BuyOrSell,
SUM (Total) Total
FROM #Temp
GROUP BY ID, BuyOrSell) one
LEFT OUTER JOIN
(SELECT ID,
BuyOrSell
FROM #Temp
GROUP BY ID,
BuyOrSell) two
ON one.ID = two.ID
AND one.BuyOrSell = two.BuyOrSell;
Here is a solution with tricky join:
SELECT t1.ID,
v.l as BuyOrSell,
SUM(CASE WHEN t1.BuyOrSell = v.l THEN t1.charge ELSE 0 END) AS Total
FROM #Temp t1
JOIN (VALUES('B'),('S')) v(l)
ON t1.BuyOrSell = CASE WHEN EXISTS(SELECT * FROM #Temp t2
WHERE t2.ID = t1.ID AND t2.BuyOrSell <> t1.BuyOrSell)
THEN v.l ELSE t1.BuyOrSell END
GROUP BY t1.ID, v.l
ORDER BY t1.ID, v.l
Output:
ID l Total
4 B 21
4 S 42
8 B 20
8 S 0
9 B 0
9 S 23

Filter unique records from a database while removing double not-null values

This is kind of hard to explain in words but here is an example of what I am trying to do in SQL. I have a query which returns the following records:
ID Z
--- ---
1 A
1 <null>
2 B
2 E
3 D
4 <null>
4 F
5 <null>
I need to filter this query so that each unique record (based on ID) appears only once in the output and if there are multiple records for the same ID, the output should contain the record with the value of Z column being non-null. If there is only a single record for a given ID and it has value of null for column Z the output still should return that record. So the output from the above query should look like this:
ID Z
--- ---
1 A
2 B
2 E
3 D
4 F
5 <null>
How would you do this in SQL?
You can use GROUP BY for that:
SELECT
ID, MAX(Z) -- Could be MIN(Z)
FROM MyTable
GROUP BY ID
Aggregate functions ignore NULLs, returning them only when all values on the group are NULL.
If you need to return both 2-B and 2-E rows:
SELECT *
FROM YourTable t1
WHERE Z IS NOT NULL
OR NOT EXISTS
(SELECT * FROM YourTable t2
WHERE T2.ID = T1.id AND T2.z IS NOT NULL)
SELECT ID
,Z
FROM YourTable
WHERE Z IS NOT NULL
DECLARE #T TABLE ( ID INT, Z CHAR(1) )
INSERT INTO #T
( ID, Z )
VALUES ( 1, 'A' ),
( 1, NULL )
, ( 2, 'B' ) ,
( 2, 'E' ),
( 3, 'D' ) ,
( 4, NULL ),
( 4, 'F' ),
( 5, NULL )
SELECT *
FROM #T
; WITH c AS (SELECT ID, r=COUNT(*) FROM #T GROUP BY ID)
SELECT t.ID, Z
FROM #T t JOIN c ON t.ID = c.ID
WHERE c.r =1
UNION ALL
SELECT t.ID, Z
FROM #T t JOIN c ON t.ID = c.ID
WHERE c.r >=2
AND z IS NOT NULL
This example assumes you want two rows returned for ID = 2.
with tmp (id, cnt_val) as
(select id,
sum(case when z is not null then 1 else 0 end)
from t
group by id)
select t.id, t.z
from t
inner join tmp on t.id = tmp.id
where tmp.cnt_val > 0 and t.z is not null
or tmp.cnt_val = 0 and t.z is null
WITH CTE
AS (
SELECT id
,z
,ROW_NUMBER() OVER (
PARTITION BY id ORDER BY coalesce(z, '') DESC
) rn
FROM #T
)
SELECT id
,z
FROM CTE
WHERE rn = 1