T SQL Cte delete where group by is greater than 1 - sql

I'm using SQL Server 2016. I have the below table:
SKU Mkt Week Cost Code
ABC 05 1 10 100
ABC 05 2 12 100
DEF 05 3 20 100
DEF 05 3 25 125
XYZ 08 1 10 100
XYZ 08 2 12 100
XZY 08 2 14 125
This is the desired result:
SKU Mkt Week Cost Code
ABC 05 1 10 100
ABC 05 2 12 100
DEF 05 3 25 125
XYZ 08 1 10 100
XZY 08 2 14 125
So if a SKU\Mkt\Week\Cost exist more than once, I want to keep the record where code = 125 and delete the row where the code is 100.
I'm using the below Cte:
;WITH CTE AS
(
SELECT *,
RN = ROW_NUMBER() OVER( PARTITION BY SKU, Mkt, Week
ORDER BY SKU, Mkt, Week)
FROM [table]
WHERE code = 100
)
DELETE FROM CTE
WHERE RN > 1
However, the Cte doesnot delete anything -what am I missing?

Based on the query and sample data you have provided, You need to note to this section of the cte inner query:
WHERE code = 100
when this filter applied you have the following data:
SKU Mkt Week Cost Code
ABC 05 1 10 100
ABC 05 2 12 100
DEF 05 3 20 100
which will get the 1 as Row_Number()'s output!, so running the following query will not effect any rows:
DELETE FROM CTE
WHERE RN > 1
To achieve the desired result you need to remove the WHERE section in CTE's inner query.
;WITH CTE AS
(
SELECT *,
RN = ROW_NUMBER() OVER( PARTITION BY SKU, Mkt, Week
ORDER BY SKU, Mkt, Week, Cost DESC) --Code/Cost DESC <==== Note this too
FROM [table]
--WHERE code = 100 <========== HERE, I've commented it
)
DELETE FROM CTE
WHERE RN > 1
You need to also add the Cost DESC or Code Desc to Row_Number()'s Order By section.

Ranking function will be evaluated in the select statement , which means the where clause WHERE code = 100 is evaluated before ROW_NUMBER() and so it has already removed the rows with code 125. Use order by Code as well and then apply the code=100 check when deleting from the CTE
;WITH CTE AS
(
SELECT *,
RN = ROW_NUMBER() OVER( PARTITION BY SKU, Mkt, Week
ORDER BY SKU, Mkt, Week,Code DESC)
FROM tt1
)
DELETE FROM CTE
WHERE RN > 1
AND CODE = 100

Try below query to get the desired result -
Sample data and Query
Declare #Table table
(SKU varchar(20), Mkt int, [Week] int, Cost int, Code int)
Insert into #Table
values
( 'ABC', 05 , 1, 10 , 100),
( 'ABC' , 05 , 2 , 12 , 100),
('DEF' ,05 , 3 , 20 , 100),
('DEF' ,05 , 3 ,25 , 125),
('XYZ' , 08 , 1 ,10 , 100),
('XYZ' , 08 , 2 ,12 , 100),
('XYZ' , 08, 2 ,14, 125)
;WITH CTE AS
(
SELECT *,
RN = ROW_NUMBER() OVER( PARTITION BY SKU, Mkt, Week
ORDER BY SKU, Mkt, Week, code desc)
FROM #Table
)
delete from Cte where RN > 1

Along with moving your Where statement, I believe you also want a second cte to work with the records you are identifying... In the following your first cte identifies the duplicate records while the second cte isolates them so you can perform your delete against those SKUs
Table
Create Table #tbl
(
SKU VarChar(10),
Mkt VarChar(10),
Week Int,
Cost Int,
Code Int
)
Insert Into #tbl Values
('ABC','05',1,10,100),
('ABC','05',2,12,100),
('DEF','05',3,20,100),
('DEF','05',3,25,125),
('XYZ','08',1,10,100),
('XYZ','08',2,12,100),
('XYZ','08',2,14,125)
Query
;WITH CTE AS
(
SELECT *,
RN = ROW_NUMBER() OVER( PARTITION BY SKU, Mkt, Week
ORDER BY SKU, Mkt, Week)
FROM #tbl
--WHERE code = 100
)
, cte1 As
(
Select sku from cte where rn > 1
)
DELETE c FROM CTE c inner join cte1 c1 On c.SKU = c1.SKU
WHERE c.Code = 100
Select * From #tbl
Result (Your 'desired result' example removed an XYZ record where the week was not duplicated?)
SKU Mkt Week Cost Code
ABC 05 1 10 100
ABC 05 2 12 100
DEF 05 3 25 125
XYZ 08 1 10 100
XYZ 08 2 12 100
XZY 08 2 14 125

Your CTE statement is only considering rows with code = 100. If you remove it, then CTE will rank based on all rows from the table. Using this, first find out which combination of have multiple rows. Then, among these combinations, identify rows with code = 100 and delete them.
create table #e1
(
SKU varchar(50)
,Mkt varchar(50)
,_Week int
,Cost int
,_code int
)
insert into #e1(SKU, Mkt, _Week, Cost, _code)
select 'ABC', '05', 1, 10, 100 UNION
SELECT 'ABC', '05', 2, 12, 100 union
SELECT 'DEF', '05', 3, 20, 100 UNION
SELECT 'DEF', '05', 3, 25, 125 UNION
SELECT 'XYZ', '08', 1, 10, 100 UNION
SELECT 'XYZ', '08', 2, 12, 100 UNION
SELECT 'XZY', '08', 2, 14, 125
delete s
from
#e1 s
JOIN
(
SELECT SKU, Mkt, _Week
FROM #e1
group by
SKU, Mkt, _Week
having count(1) > 1
) m
ON
s.SKU = m.sku and s.mkt = m.mkt and s._Week = m._Week
WHERE s._code = 100

Create table #tab1 (SKU varchar(50),Mkt varchar(50),[Week] varchar(50),Cost varchar(50),Code varchar(50))
insert into #tab1
select 'ABC','05','1','10','100'
union
select 'ABC','05','2','12','100'
union
select 'DEF','05','3','20','100'
union
select 'DEF','05','3','25','125'
union
select 'XYZ','08','1','10','100'
union
select 'XYZ','08','2','12','100'
union
select 'XYZ','08','2','14','125'
delete t from #tab1 t
inner join (select t1.SKU,t1.Mkt,t1.[Week],t1.Cost as Cost,t1.Code as Code,ROW_NUMBER()over(partition by t1.SKU,t1.Mkt,t1.[Week] order by t1.Cost desc,t1.Code desc ) as rno
from #tab1 t1
) c on c.SKU = t.SKU and c.Mkt = t.Mkt and c.Cost = t.Cost and c.[Week] = t.[Week] and c.Code = t.Code
where c.rno = 2
select * from #tab1
Output:
SKU Mkt Week Cost Code
ABC 05 1 10 100
ABC 05 2 12 100
DEF 05 3 25 125
XYZ 08 1 10 100
XYZ 08 2 14 125

Related

How can I select distinct by one column?

I have a table with the columns below, and I need to get the values if COD is duplicated, get the non NULL on VALUE column. If is not duplicated, it can get a NULL VALUE. Like the example:
I'm using SQL SERVER.
This is what I get:
COD ID VALUE
28 1 NULL
28 2 Supermarket
29 1 NULL
29 2 School
29 3 NULL
30 1 NULL
This is what I want:
COD ID VALUE
28 2 Supermarket
29 2 School
30 1 NULL
What I'm tryin' to do:
;with A as (
(select DISTINCT COD,ID,VALUE from CodId where ID = 2)
UNION
(select DISTINCT COD,ID,NULL from CodId where ID != 2)
)select * from A order by COD
You can try this.
DECLARE #T TABLE (COD INT, ID INT, VALUE VARCHAR(20))
INSERT INTO #T
VALUES(28, 1, NULL),
(28, 2 ,'Supermarket'),
(29, 1 ,NULL),
(29, 2 ,'School'),
(29, 3 ,NULL),
(30, 1 ,NULL)
;WITH CTE AS (
SELECT *, RN= ROW_NUMBER() OVER (PARTITION BY COD ORDER BY VALUE DESC) FROM #T
)
SELECT COD, ID ,VALUE FROM CTE
WHERE RN = 1
Result:
COD ID VALUE
----------- ----------- --------------------
28 2 Supermarket
29 2 School
30 1 NULL
Another option is to use the WITH TIES clause in concert with Row_Number()
Example
Select top 1 with ties *
from YourTable
Order By Row_Number() over (Partition By [COD] order by Value Desc)
Returns
COD ID VALUE
28 2 Supermarket
29 2 School
30 1 NULL
I would use GROUP BY and JOIN. If there is no NOT NULL value for a COD than it should be resolved using the OR in JOIN clause.
SELECT your_table.*
FROM your_table
JOIN (
SELECT COD, MAX(value) value
FROM your_table
GROUP BY COD
) gt ON your_table.COD = gt.COD and (your_table.value = gt.value OR gt.value IS NULL)
If you may have more than one non null value for a COD this will work
drop table MyTable
CREATE TABLE MyTable
(
COD INT,
ID INT,
VALUE VARCHAR(20)
)
INSERT INTO MyTable
VALUES (28,1, NULL),
(28,2,'Supermarket'),
(28,3,'School'),
(29,1,NULL),
(29,2,'School'),
(29,3,NULL),
(30,1,NULL);
WITH Dups AS
(SELECT COD FROM MyTable GROUP BY COD HAVING count (*) > 1 )
SELECT MyTable.COD,MyTable.ID,MyTable.VALUE FROM MyTable
INNER JOIN dups ON MyTable.COD = Dups.COD
WHERE value IS NOT NULL
UNION
SELECT MyTable.COD,MyTable.ID,MyTable.VALUE FROM MyTable
LEFT JOIN dups ON MyTable.COD = Dups.COD
WHERE dups.cod IS NULL

Oracle SQL - Group by a column and extract other values to columns

My idea is to do something like this:
INPUT:
ID CURRENCY AMOUNT
1 RUS 14,55
1 USD 22,22
1 PLN 444,44
2 PLN 22
Then I want to group by ID and get output:
ID CUR_1 AMOUNT_1 CUR_2 AMOUNT_2 CUR_3 AMOUNT_3
1 RUS 14,55 USD 22,22 PLN 444,44
2 PLN 22
It is important to combine the right amount with right currency. Maximal number of pairs is 3 like for an ID=1. It may vary from 1 to 3.
I tried using LISTAGG but it will generate problem with further processing of the data.
select *
from (select t.*, row_number() over (partition by id order by null) rn
from t)
pivot (max(currency) cur, sum(amount) amt for rn in (1, 2, 3))
Test:
with t(id, currency, amount) as (
select 1, 'RUS', 14.55 from dual union all
select 1, 'USD', 22.22 from dual union all
select 1, 'PLN', 444.44 from dual union all
select 2, 'PLN', 22 from dual )
select *
from (select t.*, row_number() over (partition by id order by null) rn
from t)
pivot (max(currency) cur, sum(amount) amt for rn in (1, 2, 3))
Output:
ID 1_CUR 1_AMT 2_CUR 2_AMT 3_CUR 3_AMT
---------- ----- ---------- ----- ---------- ----- ----------
1 RUS 14,55 USD 22,22 PLN 444,44
2 PLN 22
You can create a virtual table for each row using a subquery, then join the virtual tables by ID into a single row.

select distinct list of ids from table with earliest value in same table

I have the following table,
SDate Id Balance
2016-01-01 ABC 3
2016-01-01 DEF 7
2016-01-01 GHI 2
2016-02-01 ABC 6
2016-02-01 DEF 4
2016-02-01 GHI 8
2016-02-01 XYZ 12
I need to write a query that gives me a distinct list of Id's over a date range (so in this example SDate >= '2016-01-01' and SDate <= '2016-02-01') but also give me the earliest balance so the result from the table above I would like to see is,
Id Balance
ABC 3
DEF 7
GHI 2
XYZ 12
Is this possible?
UPDATE
Sorry I should have specified that for each date the Id is unique.
You can do this with a derived table that first works out the minimum SDate value for each Id value. Using this you then join back to your original table to find the Balance for the row that matches those values:
declare #t table(SDate date,Id nvarchar(3),Balance int);
insert into #t values ('2016-01-01','ABC',3),('2016-01-01','DEF',7),('2016-01-01','GHI',2),('2016-02-01','ABC',6),('2016-02-01','DEF',4),('2016-02-01','GHI',8),('2016-02-01','XYZ',12);
declare #StartDate date = '20160101';
declare #EndDate date = '20160201';
with d as
(
select Id
,min(SDate) as MinSDate
from #t
where SDate between #StartDate and #EndDate
group by id
)
select d.Id
,t.Balance
from d
inner join #t t
on(d.Id = t.Id
and d.MinSDate = t.SDate
);
Output:
Id | Balance
----+--------
ABC | 3
DEF | 7
GHI | 2
XYZ | 12
This should be possible with a window function - all you have to do is
partition by id
assign a row number, and
select the top row for each id
Example:
select id,
balance
from (
select id,
balance,
row_number() over( partition by id order by SDate ) as row_num
from table1
where SDate between '2016-01-01' and '2016-02-01'
) as a
where row_num = 1
Note: the advantage of this method is it is a lot more flexible. Say you wanted the 2 oldest records, you could just change to where row_num <= 2.
Analytic row_number() should be the fastest
select *
from (
select
t.*,
row_number() over (partition by Id order by SDate) rn
from your_table t
) t where rn = 1;
You can achieve this with a self join, which may not be the fastest or most elegant solution:
CREATE TABLE #SOPostSample
(
SDate DATE ,
Id NVARCHAR(5) ,
Balance INT
);
INSERT INTO #SOPostSample
( SDate, Id, Balance )
VALUES ( '2016-01-01', 'ABC', 3 ),
( '2016-01-01', 'DEF', 7 ),
( '2016-01-01', 'GHI', 2 ),
( '2016-02-01', 'ABC', 6 ),
( '2016-02-01', 'DEF', 4 ),
( '2016-02-01', 'GHI', 8 ),
( '2016-02-01', 'XYZ', 12 );
SELECT t1.Id ,
MIN(t2.Balance) Balance
FROM #SOPostSample t1
INNER JOIN #SOPostSample t2 ON t1.Id = t2.Id
GROUP BY t1.Id ,
t2.SDate
HAVING t2.SDate = MIN(t1.SDate);
DROP TABLE #SOPostSample;
Produces:
id Balance
============
ABC 3
DEF 7
GHI 2
XYZ 12
This works for the sample data, but please test with more data as I just wrote it quickly.
This should work, Top 1 just inserted for safety, should not be needed if SDate and Id are unique in combination
SELECT o.Id ,
( SELECT TOP 1
Balance
FROM tbl
WHERE Id = o.Id
AND SDate = MIN(o.SDate)
) Balance
FROM tbl o
GROUP BY Id
HAVING sDate BETWEEN '20160101' AND '20160201';
You can use sub-query
SELECT Id ,
( SELECT TOP 1
Balance
FROM [TableName] AS T1
WHERE T1.Id = [TableName].Id
ORDER BY SDate
) AS Balance
FROM [TableName]
GROUP BY Id;

adding a value to a column from data in next row sql

Base Table
id line_number
1 1232
2 1456
3 1832
4 2002
I wish to add values to a new table such that the next row's value becomes the value in a new column with the last row's value being same..
The final output I need to produce is:
id line_number end_line_number
1 1232 1456
2 1456 1832
3 1832 2002
4 2002 2002
The database is sql server.
Any help is sincerely appreciated.
Thanks
After SQL Server 2012, you can use LEAD like this.
;WITH BaseTable as
(
SELECT 1 id, 1232 line_number
UNION ALL SELECT 2 , 1456
UNION ALL SELECT 3, 1832
UNION ALL SELECT 4 , 2002
)
SELECT id,line_number,(LEAD(line_number,1,line_number) OVER(ORDER BY id ASC))
FROM BaseTable
For previous versions, try this
;WITH BaseTable as
(
SELECT 1 id, 1232 line_number
UNION ALL SELECT 2 , 1456
UNION ALL SELECT 3, 1832
UNION ALL SELECT 4 , 2002
), OrderedBaseTable as
(
SELECT id,line_number,ROW_NUMBER() OVER(ORDER BY id asc) rw
FROM BaseTable
)
SELECT t1.id,t1.line_number,ISNULL(t2.line_number,t1.line_number) next_line_number
FROM OrderedBaseTable t1
LEFT JOIN OrderedBaseTable t2
ON t1.rw = t2.rw - 1
Try this
With T as (
Select id, line_number, Row_Number() OVER(Order By id) + 1 As TempId From TableName)
Select T1.id, T1.line_number, ISNULL(T2.line_number,T1.line_number) As end_line_number From T T1
Left Join T T2 on T2.id = T1.TempId
SQL Fiddle Demo

How to write a query to produce counts for arbitrary value bands?

My table had 3 fields: id and unit. I want to count how many ids have <10, 10-49, 50-100 etc units. The final result should look like:
Category | countIds
<10 | 1516
10 - 49 | 710
50 - 99 | 632
etc.
This is the query that returns each id and how many units it has:
select id, count(unit) as numUnits
from myTable
group by id
How can I build on that query to give me the category, countIds result?
create temporary table ranges (
seq int primary key,
range_label varchar(10),
lower int,
upper int
);
insert into ranges values
(1, '<10', 0, 9),
(2, '10 - 49', 10, 49),
(3, '50 - 99', 50, 99)
etc.
select r.range_label, count(c.numUnits) as countIds
from ranges as r
join (
select id, count(unit) as numUnits
from myTable
group by id) as c
on c.numUnits between r.lower and r.upper
group by r.range_label
order by r.seq;
edit: changed sum() to count() above.
select category_bucket, count(*)
from (select case when category < 10 then "<10"
when category >= 10 and category <= 49 then "10 - 49"
when category >= 50 and category <= 99 then "50 - 99"
else "100+"
end category_bucket, num_units
from my_table)
group by category_bucket
A dynamically grouped solution is much harder.
SELECT id, countIds
FROM (
SELECT id
, 'LESS_THAN_TEN' CATEGORY
, COUNT(unit) countIds
FROM table1
GROUP BY ID
HAVING COUNT(UNIT) < 10
UNION ALL
SELECT id
, 'BETWEEN_10_AND_49' category
, COUNT(unit) countIds
FROM table1
GROUP BY ID
HAVING COUNT(UNIT) BETWEEN 10 AND 49
UNION ALL
SELECT id
, 'BETWEEN_50_AND_99' category
, COUNT(unit) countIds
FROM table1
GROUP BY id
HAVING COUNT(UNIT) BETWEEN 50 AND 99
) x
Giving an example for one range: (10 - 49)
select count(id) from
(select id, count(unit) as numUnits from myTable group by id)
where numUnits >= '10' && numUnits <= '49'
It's not precisely what you want, but you could use fixed ranges, like so:
select ' < ' || floor(id / 50) * 50, count(unit) as numUnits
from myTable
group by floor(id / 50) * 50
order by 1
Try this working sample in SQL Server TSQL
SET NOCOUNT ON
GO
WITH MyTable AS
(
SELECT 00 as Id, 1 Value UNION ALL
SELECT 05 , 2 UNION ALL
SELECT 10 , 3 UNION ALL
SELECT 15 , 1 UNION ALL
SELECT 20 , 2 UNION ALL
SELECT 25 , 3 UNION ALL
SELECT 30 , 1 UNION ALL
SELECT 35 , 2 UNION ALL
SELECT 40 , 3 UNION ALL
SELECT 45 , 1 UNION ALL
SELECT 40 , 3 UNION ALL
SELECT 45 , 1 UNION ALL
SELECT 50 , 3 UNION ALL
SELECT 55 , 1 UNION ALL
SELECT 60 , 3 UNION ALL
SELECT 65 , 1 UNION ALL
SELECT 70 , 3 UNION ALL
SELECT 75 , 1 UNION ALL
SELECT 80 , 3 UNION ALL
SELECT 85 , 1 UNION ALL
SELECT 90 , 3 UNION ALL
SELECT 95 , 1 UNION ALL
SELECT 100 , 3 UNION ALL
SELECT 105 , 1 UNION ALL
SELECT 110 , 3 UNION ALL
SELECT 115 , 1 Value
)
SELECT Category, COUNT (*) CountIds
FROM
(
SELECT
CASE
WHEN Id BETWEEN 0 and 9 then '<10'
WHEN Id BETWEEN 10 and 49 then '10-49'
WHEN Id BETWEEN 50 and 99 then '50-99'
WHEN Id > 99 then '>99'
ELSE '0' END as Category
FROM MyTable
) as A
GROUP BY Category
This will give you the following result
Category CountIds
-------- -----------
<10 2
>99 4
10-49 10
50-99 10