Select SUM by each year in SQL query - sql

I have these two tables:
Expense table
id expense expense_date product_id
1 10 2012-01-03 1
2 10 2014-02-01 2
3 10 2014-02-03 1
4 10 2012-07-03 1
Product table
product_id product_name purchase_date
1 car 2010-02-01
2 bike 2014-03-01
I would like to achieve the result to something like this by summing the expenses grouping by product_id where the the expense_date is between the purchase_date to it's next year:
Year Expense Product_name
1 0 car 2010-02-01 to 2011-02-01
2 10 car 2011-02-01 to 2012-02-01
3 10 car 2012-02-01 to 2013-02-01
4 0 car 2013-02-01 to 2014-02-01
5 10 car 2014-02-01 to 2015-02-01
1 10 bike 2014-03-01 to 2015-03-01

CREATE VIEW dbo.vwMaxExpenseDate
AS
SELECT product_id, MAX(expense_date) AS 'max_expense_date'
FROM Expense
GROUP BY product_id
DECLARE #PossibleYearRange TABLE
(
product_id INT,
YearStart DATETIME,
YearEnd DATETIME
);
WITH CTE
AS
(
SELECT p.product_id, max_expense_date, purchase_date, 1 As Number
FROM Product p
LEFT JOIN vwMaxExpenseDate e
ON p.product_id = e.product_id
UNION ALL
SELECT product_id, max_expense_date, purchase_date, Number + 1
FROM CTE
WHERE Number <= (YEAR(max_expense_date) - YEAR(purchase_date))
)
INSERT INTO #PossibleYearRange
(
product_id,
YearStart,
YearEnd
)
SELECT product_id,
CONVERT(DATETIME, CONVERT(VARCHAR(20), YEAR(purchase_date) + Number - 1) + '-'
+ CONVERT(VARCHAR(20), MONTH(purchase_date)) + '-'
+ CONVERT(VARCHAR(20), DAY(purchase_date))) AS 'YearStart',
CONVERT(DATETIME, CONVERT(VARCHAR(20), YEAR(purchase_date) + Number) + '-'
+ CONVERT(VARCHAR(20), MONTH(purchase_date)) + '-'
+ CONVERT(VARCHAR(20), DAY(purchase_date))) AS 'YearEnd'
FROM CTE
ORDER BY product_id ASC, NUMBER ASC
SELECT MAX(p.product_id) AS product_id, MAX(product_name) AS product_name, YearStart, YearEnd, COALESCE(SUM(expense), 0) AS TotalExpensePerYear
FROM #PossibleYearRange p
LEFT JOIN Expense e
ON p.product_id = e.product_id AND
expense_date BETWEEN YearStart AND YearEnd
INNER JOIN Product d
ON p.product_id = d.product_id
GROUP BY YearStart, YearEnd
ORDER BY MAX(p.product_id) ASC
Hope this helps! Thanks.

declare #BeginsAt as datetime
declare #numOf as int
set #BeginsAt = (select min(purchase_date) from Products)
set #BeginsAt = dateadd(year,datediff(year,0,#BeginsAt),0) -- force to 1st of Year
set #numOf = (year(getdate()) - year(#BeginsAt))+1
;with YearRange (id, StartAt, StopAt)
as (
select 1 as id, #BeginsAt, dateadd(Year,1,#BeginsAt)
union all
select (id + 1) , dateadd(Year,1,StartAt) , dateadd(Year,1,StopAt)
from YearRange
where (id + 1) <= #numOf
)
select
y.id
, coalesce(e.expense,0) expense
, p.product_name
, y.startAt
, dateadd(day,-1,y.StopAt)
from YearRange Y
left join Products P on Y.StopAt between P.purchase_date AND (select max(StopAt) from YearRange)
left join Expenses E on E.expense_date >= Y.StartAt and E.expense_date < Y.StopAt
and E.product_id = P.product_id
See this SQLFiddle demo

Related

Aggregate a subtotal column based on two dates of that same row

Situation:
I have 5 columns
id
subtotal (price of item)
order_date (purchase date)
updated_at (if refunded or any other status change)
status
Objective:
I need the order date as column 1
I need to get the subtotal for each day regardless if of the status as column 2
I need the subtotal amount for refunds for the third column.
Example:
If a purchase is made on May 1st and refunded on May 3rd. The output should look like this
+-------+----------+--------+
| date | subtotal | refund |
+-------+----------+--------+
| 05-01 | 10.00 | 0.00 |
| 05-02 | 00.00 | 0.00 |
| 05-03 | 00.00 | 10.00 |
+-------+----------+--------+
while the row will look like that
+-----+----------+------------+------------+----------+
| id | subtotal | order_date | updated_at | status |
+-----+----------+------------+------------+----------+
| 123 | 10 | 2019-05-01 | 2019-05-03 | refunded |
+-----+----------+------------+------------+----------+
Query:
Currently what I have looks like this:
Note: Timezone discrepancy therefore bring back the dates by 8 hours.
;with cte as (
select id as orderid
, CAST(dateadd(hour,-8,order_date) as date) as order_date
, CAST(dateadd(hour,-8,updated_at) as date) as updated_at
, subtotal
, status
from orders
)
select
b.dates
, sum(a.subtotal_price) as subtotal
, -- not sure how to aggregate it to get the refunds
from Orders as o
inner join cte as a on orders.id=cte.orderid
inner join (select * from cte where status = ('refund')) as b on o.id=cte.orderid
where dates between '2019-05-01' and '2019-05-31'
group by dates
And do I need to join it twice? Hopefully not since my table is huge.
This looks like a job for a Calendar Table. Bit of a stab in the dark, but:
--Overly simplistic Calendar table
CREATE TABLE dbo.Calendar (CalendarDate date);
WITH N AS(
SELECT N
FROM (VALUES(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL))N(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) -1 AS I
FROM N N1, N N2, N N3, N N4, N N5) --Many years of data
INSERT INTO dbo.Calendar
SELECT DATEADD(DAY, T.I, 0)
FROM Tally T;
GO
SELECT C.CalendarDate AS [date],
CASE C.CalendarDate WHEN V.order_date THEN subtotal ELSE 0 END AS subtotal,
CASE WHEN C.CalendarDate = V.updated_at AND V.[status] = 'refunded' THEN subtotal ELSE 0.00 END AS subtotal
FROM (VALUES(123,10.00,CONVERT(date,'20190501'),CONVERT(date,'20190503'),'refunded'))V(id,subtotal,order_date,updated_at,status)
JOIN dbo.Calendar C ON V.order_date <= C.CalendarDate AND V.updated_at >= C.CalendarDate;
GO
DROP TABLE dbo.Calendar;
Consider joining on a recursive CTE of sequential dates:
WITH dates AS (
SELECT CONVERT(datetime, '2019-01-01') AS rec_date
UNION ALL
SELECT DATEADD(d, 1, CONVERT(datetime, rec_date))
FROM dates
WHERE rec_date < '2019-12-31'
),
cte AS (
SELECT id AS orderid
, CAST(dateadd(hour,-8,order_date) AS date) as order_date
, CAST(dateadd(hour,-8,updated_at) AS date) as updated_at
, subtotal
, status
FROM orders
)
SELECT rec_date AS date,
CASE
WHEN c.order_date = d.rec_date THEN subtotal
ELSE 0
END AS subtotal,
CASE
WHEN c.updated_at = d.rec_date THEN subtotal
ELSE 0
END AS refund
FROM cte c
JOIN dates d ON d.rec_date BETWEEN c.order_date AND c.updated_at
WHERE c.status = 'refund'
option (maxrecursion 0)
GO
Rextester demo

Fill up date gap by month

I have table of products and their sales quantity in months.
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-05-01 5
B 2018-08-01 10
B 2018-10-01 12
...
I'd like to first fill in the data gap between each product's min and max dates like below:
Product Month Qty
A 2018-01-01 5
A 2018-02-01 3
A 2018-03-01 0
A 2018-04-01 0
A 2018-05-01 5
B 2018-08-01 10
B 2018-09-01 0
B 2018-10-01 12
...
Then I would need to perform an accumulation of each product's sales quantity by month.
Product Month total_Qty
A 2018-01-01 5
A 2018-02-01 8
A 2018-03-01 8
A 2018-04-01 8
A 2018-05-01 13
B 2018-08-01 10
B 2018-09-01 10
B 2018-10-01 22
...
I fumbled over the "cross join" clause, however it seems to generate some unexpected results for me. Could someone help to give a hint how I can achieve this in SQL?
Thanks a lot in advance.
I think a recursive CTE is a simple way to do this. The code is just:
with cte as (
select product, min(mon) as mon, max(mon) as end_mon
from t
group by product
union all
select product, dateadd(month, 1, mon), end_mon
from cte
where mon < end_mon
)
select cte.product, cte.mon, coalesce(qty, 0) as qty
from cte left join
t
on t.product = cte.product and t.mon = cte.mon;
Here is a db<>fiddle.
Hi i think this example can help you and perform what you excepted :
CREATE TABLE #MyTable
(Product varchar(10),
ProductMonth DATETIME,
Qty int
);
GO
CREATE TABLE #MyTableTempDate
(
FullMonth DATETIME
);
GO
INSERT INTO #MyTable
SELECT 'A', '2019-01-01', 214
UNION
SELECT 'A', '2019-02-01', 4
UNION
SELECT 'A', '2019-03-01', 50
UNION
SELECT 'B', '2019-01-01', 214
UNION
SELECT 'B', '2019-02-01', 10
UNION
SELECT 'C', '2019-04-01', 150
INSERT INTO #MyTableTempDate
SELECT '2019-01-01'
UNION
SELECT '2019-02-01'
UNION
SELECT '2019-03-01'
UNION
SELECT '2019-04-01'
UNION
SELECT '2019-05-01'
UNION
SELECT '2019-06-01'
UNION
SELECT '2019-07-01';
------------- FOR NEWER SQL SERVER VERSION > 2005
WITH MyCTE AS
(
SELECT T.Product, T.ProductMonth AS 'MMonth', T.Qty
FROM #MyTable T
UNION
SELECT T.Product, TD.FullMonth AS 'MMonth', 0 AS 'Qty'
FROM #MyTable T, #MyTableTempDate TD
WHERE NOT EXISTS (SELECT 1 FROM #MyTable TT WHERE TT.Product = T.Product AND TD.FullMonth = TT.ProductMonth)
)
-- SELECT * FROM MyCTE;
SELECT Product, MMonth, Qty, SUM( Qty) OVER(PARTITION BY Product ORDER BY Product
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) as 'TotalQty'
FROM MyCTE
ORDER BY Product, MMonth ASC;
DROP TABLE #MyTable
DROP TABLE #MyTableTempDate
I have other way to perform this in lower SQL Server Version (like 2005 and lower)
It's a SELECT on SELECT if it's your case let me know and i provide some other example.
You can create the months with a recursive CTE
DECLARE #MyTable TABLE
(
ProductID CHAR(1),
Date DATE,
Amount INT
)
INSERT INTO #MyTable
VALUES
('A','2018-01-01', 5),
('A','2018-02-01', 3),
('A','2018-05-01', 5),
('B','2018-08-01', 10),
('B','2018-10-01', 12)
DECLARE #StartDate DATE
DECLARE #EndDate DATE
SELECT #StartDate = MIN(Date), #EndDate = MAX(Date) FROM #MyTable
;WITH dates AS (
SELECT #StartDate AS Date
UNION ALL
SELECT DATEADD(Month, 1, Date)
FROM dates
WHERE Date < #EndDate
)
SELECT A.ProductID, d.Date, COALESCE(Amount,0) AS Amount, COALESCE(SUM(Amount) OVER(PARTITION BY A.ProductID ORDER BY A.ProductID, d.Date ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW),0) AS Total
FROM
(
SELECT ProductID, MIN(date) as DateStart, MAX(date) as DateEnd
FROM #MyTable
GROUP BY ProductID -- As I read in your comments that you need different min and max dates per product
) A
JOIN dates d ON d.Date >= A.DateStart AND d.Date <= A.DateEnd
LEFT JOIN #MyTable T ON A.ProductID = T.ProductID AND T.Date = d.Date
ORDER BY A.ProductID, d.Date
Try this below
IF OBJECT_ID('tempdb..#Temp') IS NOT NULL
DROP TABLE #Temp
;WITH CTE(Product,[Month],Qty)
AS
(
SELECT 'A','2018-01-01', 5 UNION ALL
SELECT 'A','2018-02-01', 3 UNION ALL
SELECT 'A','2018-05-01', 5 UNION ALL
SELECT 'B','2018-08-01', 10 UNION ALL
SELECT 'D','2018-10-01', 12
)
SELECT ct.Product,[MonthDays],ct.Qty
INTO #Temp
FROM
(
SELECT c.Product,[Month],
ISNULL(Qty,0) AS Qty
FROM CTE c
)ct
RIGHT JOIN
(
SELECT -- This code is to get month data
CONVERT(VARCHAR(10),'2018-'+ RIGHT('00'+CAST(MONTH(DATEADD(MM, s.number, CONVERT(DATETIME, 0)))AS VARCHAR),2) +'-01',120) AS [MonthDays]
FROM master.dbo.spt_values s
WHERE [type] = 'P' AND s.number BETWEEN 0 AND 11
)DT
ON dt.[MonthDays] = ct.[Month]
SELECT
MAX(Product)OVER(ORDER BY [MonthDays])AS Product,
[MonthDays],
ISNULL(Qty,0) Qty,
SUM(ISNULL(Qty,0))OVER(ORDER BY [MonthDays]) As SumQty
FROM #Temp
Result
Product MonthDays Qty SumQty
------------------------------
A 2018-01-01 5 5
A 2018-02-01 3 8
A 2018-03-01 0 8
A 2018-04-01 0 8
A 2018-05-01 5 13
A 2018-06-01 0 13
A 2018-07-01 0 13
B 2018-08-01 10 23
B 2018-09-01 0 23
D 2018-10-01 12 35
D 2018-11-01 0 35
D 2018-12-01 0 35
First of all, i would divide month and year to get easier with statistics.
I will give you an example query, not based on your table but still helpful.
--here i create the table that will be used as calendar
Create Table MA_MonthYears (
Month int not null ,
year int not null
PRIMARY KEY ( month, year) )
--/////////////////
-- here i'm creating a procedure to fill the ma_monthyears table
declare #month as int
declare #year as int
set #month = 1
set #year = 2015
while ( #year != 2099 )
begin
insert into MA_MonthYears(Month, year)
select #month, #year
if #month < 12
set #month=#month+1
else
set #month=1
if #month = 1
set #year = #year + 1
end
--/////////////////
--here you are the possible result you are looking for
select SUM(Ma_saledocdetail.taxableamount) as Sold, MA_MonthYears.month , MA_MonthYears.year , item
from MA_MonthYears left outer join MA_SaleDocDetail on year(MA_SaleDocDetail.DocumentDate) = MA_MonthYears.year
and Month(ma_saledocdetail.documentdate) = MA_MonthYears.Month
group by MA_SaleDocDetail.Item, MA_MonthYears.year , MA_MonthYears.month
order by MA_MonthYears.year , MA_MonthYears.month

MsSQL LEFT JOIN with double GROUP BY

I am trying currently using MsSQL for a product shipping DB. I have spent a long time trying to write a SQL query to get the amount of products going to each delivery location in an area, per date, in a 4 day period beginning today.
In an area means that there is another location that is parent to that location.
The tables concerned are Products and Locations and are structured as follows
Products
ProductID DeliveryDate DestinationID
1 2018-10-05 1
2 2018-10-05 2
3 2018-10-05 3
4 2018-10-06 1
5 2018-10-06 5
Locations
LocationID OwnerID
1 4
2 4
3 4
4 Null
5 6
6 Null
The output desired is as follows
DeliveryDate Destination ProductCount
2018-10-04 1 Null
2018-10-04 2 Null
2018-10-04 3 Null
2018-10-05 1 1
2018-10-05 2 1
2018-10-05 3 1
2018-10-06 1 1
2018-10-06 2 Null
2018-10-06 3 Null
2018-10-07 1 Null
2018-10-07 2 Null
2018-10-07 3 Null
What I have tried so far is this
DECLARE #startdate DATETIME
,#enddate DATETIME
SET #startdate = convert(varchar, '2018-10-04 00:00:00', 102)
SET #enddate = convert(varchar, '2018-10-07 00:00:00', 102)
;WITH DateArray
AS (
SELECT #startdate DateVal
UNION ALL
SELECT DateVal + 1
FROM DateArray
WHERE DateVal + 1 <= #enddate
)
SELECT * FROM
(SELECT da.DateVal AS DeliveryDate
FROM DateArray da) a
LEFT JOIN
(SELECT ISNULL(COUNT(p.ProductID),0) AS ProductCount,
DeliveryDate,
ISNULL(p.DestinationID,'') AS Destination
FROM Product p
AND p.DestinationID IN(SELECT LocationID FROM Locations WHERE OwnerID = 4)
GROUP BY p.DestinationID, p.DeliveryDate) AS b
ON b.DeliveryDate = a.DeliveryDate
The result ensures that all dates are present even if the ProductCount is null, however, not every Destination is shown if count is null. Shown below:
DeliveryDate Destination ProductCount
2018-10-04 Null Null
2018-10-05 1 1
2018-10-05 2 1
2018-10-05 3 1
2018-10-06 1 1
2018-10-07 Null Null
I have spent two days stubbornly trying to figure this out with many online SQL resources and scouring StackOverFlow but with no luck.
I recommend creating a numbers table or a calendar table, but you recursive query suffices for small sets.
DECLARE
#startdate DATETIME = '2018-10-04 00:00:00',
#enddate DATETIME = '2018-10-07 00:00:00';
WITH
DateArray AS
(
SELECT #startdate DateVal
UNION ALL
SELECT DateVal + 1 FROM DateArray WHERE DateVal + 1 <= #enddate
)
SELECT
DateArray.DateVal AS DeliveryDate,
Locations.LocationID AS Destination,
COUNT(Products.ProductID) AS ProductCount
FROM
DateArray
CROSS JOIN
(
SELECT * FROM Locations WHERE OwnerID = 4
)
Locations
LEFT JOIN
Products
ON Products.DeliveryDate = DateArray.DateVal
AND Products.DestinationID = Locations.LocationID
GROUP BY
DateArray.DateVal,
Locations.LocationID
Or...
SELECT
DateArray.DateVal AS DeliveryDate,
Locations.LocationID AS Destination,
Products.ProductCount AS ProductCount
FROM
DateArray
CROSS JOIN
(
SELECT * FROM Locations WHERE OwnerID = 4
)
Locations
LEFT JOIN
(
SELECT DeliveryDate, DestinationID, COUNT(*) AS ProductCount
FROM Products
GROUP BY DeliveryDate, DestinationID
)
Products
ON Products.DeliveryDate = DateArray.DateVal
AND Products.DestinationID = Locations.LocationID

How to aggregate (counting distinct items) over a sliding window in SQL Server?

I am currently using this query (in SQL Server) to count the number of unique item each day:
SELECT Date, COUNT(DISTINCT item)
FROM myTable
GROUP BY Date
ORDER BY Date
How can I transform this to get for each date the number of unique item over the past 3 days (including the current day)?
The output should be a table with 2 columns:
one columns with all dates in the original table. On the second column, we have the number of unique item per date.
for instance if original table is:
Date Item
01/01/2018 A
01/01/2018 B
02/01/2018 C
03/01/2018 C
04/01/2018 C
With my query above I currently get the unique count for each day:
Date count
01/01/2018 2
02/01/2018 1
03/01/2018 1
04/01/2018 1
and I am looking to get as result the unique count over 3 days rolling window:
Date count
01/01/2018 2
02/01/2018 3 (because items ABC on 1st and 2nd Jan)
03/01/2018 3 (because items ABC on 1st,2nd,3rd Jan)
04/01/2018 1 (because only item C on 2nd,3rd,4th Jan)
Using an apply provides a convenient way to form sliding windows
CREATE TABLE myTable
([DateCol] datetime, [Item] varchar(1))
;
INSERT INTO myTable
([DateCol], [Item])
VALUES
('2018-01-01 00:00:00', 'A'),
('2018-01-01 00:00:00', 'B'),
('2018-01-02 00:00:00', 'C'),
('2018-01-03 00:00:00', 'C'),
('2018-01-04 00:00:00', 'C')
;
CREATE NONCLUSTERED INDEX IX_DateCol
ON MyTable([Date])
;
Query:
select distinct
t1.dateCol
, oa.ItemCount
from myTable t1
outer apply (
select count(distinct t2.item) as ItemCount
from myTable t2
where t2.DateCol between dateadd(day,-2,t1.DateCol) and t1.DateCol
) oa
order by t1.dateCol ASC
Results:
| dateCol | ItemCount |
|----------------------|-----------|
| 2018-01-01T00:00:00Z | 2 |
| 2018-01-02T00:00:00Z | 3 |
| 2018-01-03T00:00:00Z | 3 |
| 2018-01-04T00:00:00Z | 1 |
There may be some performance gains by reducing the date column prior to using the apply, like so:
select
d.date
, oa.ItemCount
from (
select distinct t1.date
from myTable t1
) d
outer apply (
select count(distinct t2.item) as ItemCount
from myTable t2
where t2.Date between dateadd(day,-2,d.Date) and d.Date
) oa
order by d.date ASC
;
Instead of using select distinct in that subquery you could use group by instead but the execution plan will remain the same.
Demo at SQL Fiddle
The most straight forward solution is to join the table with itself based on dates:
SELECT t1.DateCol, COUNT(DISTINCT t2.Item) AS C
FROM testdata AS t1
LEFT JOIN testdata AS t2 ON t2.DateCol BETWEEN DATEADD(dd, -2, t1.DateCol) AND t1.DateCol
GROUP BY t1.DateCol
ORDER BY t1.DateCol
Output:
| DateCol | C |
|-------------------------|---|
| 2018-01-01 00:00:00.000 | 2 |
| 2018-01-02 00:00:00.000 | 3 |
| 2018-01-03 00:00:00.000 | 3 |
| 2018-01-04 00:00:00.000 | 1 |
GROUP BY should be faster then DISTINCT (make sure to have an index on your Date column)
DECLARE #tbl TABLE([Date] DATE, [Item] VARCHAR(100))
;
INSERT INTO #tbl VALUES
('2018-01-01 00:00:00', 'A'),
('2018-01-01 00:00:00', 'B'),
('2018-01-02 00:00:00', 'C'),
('2018-01-03 00:00:00', 'C'),
('2018-01-04 00:00:00', 'C');
SELECT t.[Date]
--Just for control. You can take this part away
,(SELECT DISTINCT t2.[Item] AS [*]
FROM #tbl AS t2
WHERE t2.[Date]<=t.[Date]
AND t2.[Date]>=DATEADD(DAY,-2,t.[Date]) FOR XML PATH('')) AS CountedItems
--This sub-select comes back with your counts
,(SELECT COUNT(DISTINCT t2.[Item])
FROM #tbl AS t2
WHERE t2.[Date]<=t.[Date]
AND t2.[Date]>=DATEADD(DAY,-2,t.[Date])) AS ItemCount
FROM #tbl AS t
GROUP BY t.[Date];
The result
Date CountedItems ItemCount
2018-01-01 AB 2
2018-01-02 ABC 3
2018-01-03 ABC 3
2018-01-04 C 1
This solution is different from other solutions. Can you check performance of this query on real data with comparison to other answers?
The basic idea is that each row can participate in the window for its own date, the day after, or the day after that. So this first expands the row out into three rows with those different dates attached and then it can just use a regular COUNT(DISTINCT) aggregating on the computed date. The HAVING clause is just to avoid returning results for dates that were solely computed and not present in the base data.
with cte(Date, Item) as (
select cast(a as datetime), b
from (values
('01/01/2018','A')
,('01/01/2018','B')
,('02/01/2018','C')
,('03/01/2018','C')
,('04/01/2018','C')) t(a,b)
)
select
[Date] = dateadd(dd, n, Date), [Count] = count(distinct Item)
from
cte
cross join (values (0),(1),(2)) t(n)
group by dateadd(dd, n, Date)
having max(iif(n = 0, 1, 0)) = 1
option (force order)
Output:
| Date | Count |
|-------------------------|-------|
| 2018-01-01 00:00:00.000 | 2 |
| 2018-01-02 00:00:00.000 | 3 |
| 2018-01-03 00:00:00.000 | 3 |
| 2018-01-04 00:00:00.000 | 1 |
It might be faster if you have many duplicate rows:
select
[Date] = dateadd(dd, n, Date), [Count] = count(distinct Item)
from
(select distinct Date, Item from cte) c
cross join (values (0),(1),(2)) t(n)
group by dateadd(dd, n, Date)
having max(iif(n = 0, 1, 0)) = 1
option (force order)
Use GETDATE() function to get current date, and DATEADD() to get the last 3 days
SELECT Date, count(DISTINCT item)
FROM myTable
WHERE [Date] >= DATEADD(day,-3, GETDATE())
GROUP BY Date
ORDER BY Date
SQL
SELECT DISTINCT Date,
(SELECT COUNT(DISTINCT item)
FROM myTable t2
WHERE t2.Date BETWEEN DATEADD(day, -2, t1.Date) AND t1.Date) AS count
FROM myTable t1
ORDER BY Date;
Demo
Rextester demo: http://rextester.com/ZRDQ22190
Since COUNT(DISTINCT item) OVER (PARTITION BY [Date]) is not supported you can use dense_rank to emulate that:
SELECT Date, dense_rank() over (partition by [Date] order by [item])
+ dense_rank() over (partition by [Date] order by [item] desc)
- 1 as count_distinct_item
FROM myTable
One thing to note is that dense_rank will count null as whereas COUNT will not.
Refer this post for more details.
Here is a simple solution that uses myTable itself as the source of grouping dates (edited for SQLServer dateadd). Note that this query assumes there will be at least one record in myTable for every date; if any date is absent, it will not appear in the query results, even if there are records for the 2 days prior:
select
date,
(select
count(distinct item)
from (select distinct date, item from myTable) as d2
where
d2.date between dateadd(day,-2,d.date) and d.date
) as count
from (select distinct date from myTable) as d
I solve this question with Math.
z (any day) = 3x + y (y is mode 3 value)
I need from 3 * (x - 1) + y + 1 to 3 * (x - 1) + y + 3
3 * (x- 1) + y + 1 = 3* (z / 3 - 1) + z % 3 + 1
In that case; I can use group by (between 3* (z / 3 - 1) + z % 3 + 1 and z)
SELECT iif(OrderDate between 3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1
and orderdate, Orderdate, 0)
, count(sh.SalesOrderID) FROM Sales.SalesOrderDetail shd
JOIN Sales.SalesOrderHeader sh on sh.SalesOrderID = shd.SalesOrderID
group by iif(OrderDate between 3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1
and orderdate, Orderdate, 0)
order by iif(OrderDate between 3 * (cast(OrderDate as int) / 3 - 1) + (cast(OrderDate as int) % 3) + 1
and orderdate, Orderdate, 0)
If you need else day group, you can use;
declare #n int = 4 (another day count)
SELECT iif(OrderDate between #n * (cast(OrderDate as int) / #n - 1) + (cast(OrderDate as int) % #n) + 1
and orderdate, Orderdate, 0)
, count(sh.SalesOrderID) FROM Sales.SalesOrderDetail shd
JOIN Sales.SalesOrderHeader sh on sh.SalesOrderID = shd.SalesOrderID
group by iif(OrderDate between #n * (cast(OrderDate as int) / #n - 1) + (cast(OrderDate as int) % #n) + 1
and orderdate, Orderdate, 0)
order by iif(OrderDate between #n * (cast(OrderDate as int) / #n - 1) + (cast(OrderDate as int) % #n) + 1
and orderdate, Orderdate, 0)

Get all customer having limited transactions

I have to extract all those customer names having transactions of less than 5000 each per month for 6 consecutive months and then have 3 transactions of 20,000 each on 7th month.
All the transactions for a customer will be stored in different rows.
Example: Considering customer A, Information for the customer will be stored as follows:
Name | TransactionDate | Amount
1. CustomerA | 27-08-2015 | 4500
2. CustomerA | 27-09-2015 | 4500
3. CustomerA | 27-10-2015 | 4500
4. CustomerA | 27-11-2015 | 4500
5. CustomerA | 27-12-2015 | 4500
6. CustomerA | 27-01-2016 | 4500
7. CustomerA | 27-02-2016 | 20000
8. CustomerA | 27-02-2016 | 20000
9. CustomerA | 27-02-2016 | 20000
Until you specify SQL flavor, I think I got a flexible and decent solution for T-SQL:
1) To have simpler queries, I have defined as persisted column to store month number is a convenient way:
create table CustomerTransaction
(
CustomerName VARCHAR(20),
TransactionDate DATE,
Amount NUMERIC(18, 2),
MonthNo AS DATEPART(yyyy, TransactionDate) * 12 + DATEPART(mm, TransactionDate) - 1 PERSISTED
)
If this cannot be used, you can employee date arithmetic (DATEDIFF), or have the exact computation inlined.
First CTE gets transaction data with a row number and a start month number for that group (customer and payment series).
For each category, small amounts and 20K (big amounts), I have selected from previous CTE applying filtering based on amount.
For each series apply the count criteria (6 small payments, followed by 3 big ones).
Join small and big payments based on customer and dates (group month is the smallest group month - 1).
The final query is the following:
declare #SmallAmountsLen INT = 6;
declare #BigAmountsLen INT = 3;
declare #SmallAmountThreshold NUMERIC(18, 2) = 5000
declare #BigAmount NUMERIC(18, 2) = 20000
;with AmountCte AS (
SELECT CustomerName, TransactionDate, Amount, MonthNo, ROW_NUMBER() OVER (PARTITION BY CustomerName ORDER BY TransactionDate) AS RowNo,
MonthNo - ROW_NUMBER() OVER (PARTITION BY CustomerName ORDER BY TransactionDate) AS GroupMonthNo
FROM CustomerTransaction
),
SmallAmountCte AS (
SELECT *
FROM AmountCte
WHERE Amount < #SmallAmountThreshold
),
BigAmountCte AS (
SELECT *
FROM AmountCte
WHERE Amount = #BigAmount
),
SmallGroupCte AS (
select CustomerName, GroupMonthNo
from SmallAmountCte
group by CustomerName, GroupMonthNo
having count(1) = #SmallAmountsLen
),
BigGroupCte AS (
select CustomerName, MonthNo
from BigAmountCte
group by CustomerName, MonthNo
having count(1) = #BigAmountsLen
)
select S.*, B.*
from SmallGroupCte S
join BigGroupCte B on B.CustomerName = S.CustomerName
where B.MonthNo = S.GroupMonthNo + #SmallAmountsLen + 1
[EDIT] Query without need of a computed column
declare #SmallAmountsLen INT = 6;
declare #BigAmountsLen INT = 3;
declare #SmallAmountThreshold NUMERIC(18, 2) = 5000
declare #BigAmount NUMERIC(18, 2) = 20000
;with AmountCte AS (
SELECT CustomerName, TransactionDate, Amount, DATEPART(yyyy, TransactionDate) * 12 + DATEPART(mm, TransactionDate) - 1 AS MonthNo,
ROW_NUMBER() OVER (PARTITION BY CustomerName ORDER BY TransactionDate) AS RowNo,
DATEPART(yyyy, TransactionDate) * 12 + DATEPART(mm, TransactionDate) - 1 - ROW_NUMBER() OVER (PARTITION BY CustomerName ORDER BY TransactionDate) AS GroupMonthNo
FROM CustomerTransaction
),
SmallAmountCte AS (
SELECT *
FROM AmountCte
WHERE Amount < #SmallAmountThreshold
),
BigAmountCte AS (
SELECT *
FROM AmountCte
WHERE Amount = #BigAmount
),
SmallGroupCte AS (
select CustomerName, GroupMonthNo
from SmallAmountCte
group by CustomerName, GroupMonthNo
having count(1) = #SmallAmountsLen
),
BigGroupCte AS (
select CustomerName, MonthNo
from BigAmountCte
group by CustomerName, MonthNo
having count(1) = #BigAmountsLen
)
select S.*, B.*
from SmallGroupCte S
join BigGroupCte B on B.CustomerName = S.CustomerName
where B.MonthNo = S.GroupMonthNo + #SmallAmountsLen + 1
Here is a simpler query to get the result, what I wanted:
WITH CTE
AS
(
SELECT * FROM
(
SELECT DENSE_RANK() OVER (PARTITION BY Name ORDER BY DATEPART(MONTH,TransactionDate)) AS SrNo,
Name,Amount,DatePart(Month,TransactionDate) AS MonthNo,TransactionDate FROM TransactionTable) AS A
WHERE
(Amount <= 5000 AND SrNo < 7) OR (Amount = 20000 AND SrNo = 7)
)
SELECT A.Name AS Account_Number,A.Transaction_Date FROM
(
SELECT Name,Amount,COUNT(SrNo) As Sr,MAX(TransactionDate) AS Transaction_Date FROM CTE
WHERE SrNo = 7 AND Amount = 20000
GROUP BY Name,Amount
HAVING COUNT(srNo) = 3) AS A
INNER JOIN
(
SELECT Name,COUNT(SrNo) AS Ss FROM CTE
WHERE SrNo < 7 AND Amount <= 5000
GROUP BY Name
HAVING COUNT(srNo) = 6) AS B
ON A.Name = B.Name