TSQL - Query to get sum of last 3 months QTY - sql-server

I am working on a query to get sum of (current + last 2 month) QTY based on a date field in the table.
CREATE table #temp_Sales
( Client varchar(10),
Sale_Month Date,
Qty int)
Insert into #temp_Sales VALUES
( 'AAAA', '2022-06-01', 5 ),
( 'AAAA', '2022-05-01', 10 ),
( 'AAAA', '2022-05-01', 2 ),
( 'AAAA', '2022-04-01', 5 ),
( 'AAAA', '2022-02-01', 15),
( 'BBBB', '2022-05-01', 2 ),
( 'BBBB', '2022-04-01', 4),
( 'BBBB', '2022-03-01', 6 ),
( 'BBBB', '2022-03-01', 10 ),
( 'BBBB', '2022-01-01', 6 ),
( 'BBBB', '2021-10-01', 10),
( 'BBBB', '2021-09-01', 2 ),
( 'BBBB', '2021-11-01', 4 ),
( 'BBBB', '2021-08-01', 6),
( 'BBBB', '2021-07-01', 8 ),
( 'CCCC', '2021-11-01', 2 ),
( 'CCCC', '2021-10-01', 3 ),
( 'CCCC', '2021-09-01', 30 ),
( 'CCCC', '2021-06-01', 4 )
Sample data:
Expected Output:
The Sale_month is not consecutive and same month can appear more than once for a client in the table.
Example : For the Client AAAA and Sale Month 2022-06-01 the qty should include the sum(QTY) of current and last 2 months ( 2022-06-01,2022-05-01 and 2022-04-01) for that client. QTY = 5 + 10 + 2 + 5 = 22
For the client BBBB and Sale month 2022-03-01 . QTY = 6 + 10 + 6 = 22
;With da AS
(SELECT *, DATEADD(MM,-2,Sale_month)as last_two_Months FROM #temp_Sales)
Select Client,Sale_month,Sum(qty) from da
WHERE Sale_month Between last_two_Months and Sale_month
GROUP BY Client,Sale_month
Order by client
Tried the above query. But not working as expected not sure how to group by using last_two_Months and Sale_month. Any help is much appreciated.

Here is one way you could tackle this.
select Sale_Month
, client
, Last3Months = max(x.PeriodSales)
from #temp_Sales s
cross apply
(
select PeriodSales = sum(Qty)
from #temp_Sales s2
where s2.Sale_Month >= dateadd(month, -2, s.Sale_Month)
and s2.Sale_Month <= s.Sale_Month
and s2.Client = s.Client
) x
group by Sale_Month
, client
order by Client
, Sale_Month desc

group on qty to eliminate duplicates, then join to self like this:
;WITH CTE AS(
select
[Client],
[Sale_Month],
SUM(QTY) QTY
from #tmp
group by
[Client],
[Sale_Month]
)
select a.[Client], a.[Sale_Month], SUM(b.QTY) QTY
from
CTE a
INNER JOIN CTE b on
a.Client = b.Client and
DATEDIFF(mm, b.[Sale_Month], a.[Sale_Month]) between 0 and 2
group by
a.[Client], a.[Sale_Month]

Firstly, if you don't already have a Calendar Table invest in one.
After that, you can create the data set you need by JOINing data from your Sales table to your calendar table, then getting the sales via a LEFT JOIN and aggregating them. Then you can use a windowed SUM for the last 3 months. Finally, as you seem to not want months that don't exist, filter those back out:
WITH Clients AS(
SELECT Client,
MIN(Sale_Month) AS MinMonth,
MAX(Sale_Month) AS MaxMonth
FROM dbo.Sales
GROUP BY Client),
ClientDates AS(
SELECT C.Client,
CT.CalendarDate
FROM Clients C
JOIN dbo.CalendarTable CT ON C.MinMonth <= CT.CalendarDate
AND C.MaxMonth >= CT.CalendarDate
WHERE CT.CalenderDay = 1),
ClientSales AS(
SELECT CD.Client,
CD.CalendarDate,
SUM(S.Qty) AS Qty
FROM ClientDates CD
LEFT JOIN dbo.Sales S ON CD.Client = S.Client
AND CD.CalendarDate = S.Sale_Month
GROUP BY CD.Client,
CD.CalendarDate),
Last3 AS(
SELECT CS.Client,
CS.CalendarDate,
CS.Qty,
SUM(CS.Qty) OVER (PARTITION BY CS.Client ORDER BY CS.CalendarDate
ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) AS Last3Months
FROM ClientSales CS)
SELECT L3.Client,
L3.CalendarDate AS Sale_Month,
L3.Last3Months
FROM Last3 L3
WHERE L3.Qty IS NOT NULL
ORDER BY Client,
Sale_Month DESC;

Related

SQL Server Window Paging Based on # of Groups

Given the following table structure
Column
Id
Name
DateCreated
with the following data
id
Name
DateCreated
1
Joe
1/13/2021
2
Fred
1/13/2021
3
Bob
1/12/2021
4
Sue
1/12/2021
5
Sally
1/10/2021
6
Alex
1/9/2021
I need SQL that will page over the data based on datecreated. The query should return the top 3 records, and any record which also shares the datecreated of the top 3.
So give the data above, we should get back Joe, Fred and Bob (as the top 3 records) plus Sue since sue has the same date as Bob.
Is there something like ROW_NUMBER that increments for each row where it encounters a different value.
For some context this query is being used to generate an agenda type view, and once we select any date we want to keep all data for that date together.
EDIT
I do have a solution but it smells:
;WITH CTE AS ( SELECT ROW_NUMBER() OVER(ORDER BY DateCreated DESC) RowNum,CAST(DateCreated AS DATE) DateCreated,Name
FROM MyTable),
PAGE AS (SELECT *
FROM CTE
WHERE RowNum<=5)
SELECT *
FROM Page
UNION
SELECT *
FROM CTE
WHERE DateCreated=(SELECT MIN(DateCreated) FROM Page)
I've used a TOP 3 WITH TIES example and a ROW_NUMBER example and a CTE to return four records:
DROP TABLE IF EXISTS #tmp
GO
CREATE TABLE #tmp (
Id INT PRIMARY KEY,
name VARCHAR(20) NOT NULL,
dateCreated DATE
)
GO
INSERT INTO #tmp VALUES
( 1, 'Joe', '13 Jan 2021' ),
( 2, 'Fred', '13 Jan 2021' ),
( 3, 'Bob', '12 Jan 2021' ),
( 4, 'Sue', '12 Jan 2021' ),
( 5, 'Sally', '10 Jan 2021' ),
( 6, 'Alex', '9 Jan 2021' )
GO
-- Gets same result
SELECT TOP 3 WITH TIES *
FROM #tmp t
ORDER BY dateCreated DESC
;WITH cte AS (
SELECT ROW_NUMBER() OVER( ORDER BY dateCreated DESC ) rn, *
FROM #tmp
)
SELECT *
FROM #tmp t
WHERE EXISTS
(
SELECT *
FROM cte c
WHERE rn <=3
AND t.dateCreated = c.dateCreated
)
My results:
As #Charlieface, we only need to replace ROW_NUMBER with DENSE_RANK. So that the ROW_NUMBER will be tied according to the same value.
When we run the query:
SELECT DENSE_RANK () OVER(ORDER BY DateCreated DESC) RowNum,CAST(DateCreated AS DATE) DateCreated,Name
FROM MyTable
The result will show as follows:
So as a result, we can set RowNum<=3 in the query to get the top 3:
;WITH CTE AS ( SELECT DENSE_RANK() OVER(ORDER BY DateCreated DESC) RowNum,CAST(DateCreated AS DATE) DateCreated,Name
FROM MyTable),
PAGE AS (SELECT *
FROM CTE
WHERE RowNum<=3)
SELECT *
FROM Page
UNION
SELECT *
FROM CTE
WHERE DateCreated=(SELECT MIN(DateCreated) FROM Page)
The First one is as yours the second one is as above. The results of the two queries are the same.
Kindly let us know if you need more infomation.

Get list of dates without entries in SQL Server

I am looking to find a solution to this problem. I have a table called LogEntry that stores information used by multiple offices, where they have to log any visitors that come in to their office on any given day. If no visitors come in, they are still required to log "No Visitors" for the day. How do I run a query that pulls all dates where an office failed to create even a "No Visitors" log?
I've looked at this question (and the article linked within), but even adapting that query, I'm only able to create a blank row for a date where an office is missing an entry for a date, not specify the actual office that did not create an entry. Is there a way to do what I'm trying to do?
declare #temp table (
CDate datetime,
loc_id varchar(50)
)
insert into #temp SELECT DISTINCT entryDate, locationID FROM LogEntry WHERE entryDate >= '05/01/2017' AND entryDate <= '07-31-2017'
;with d(date) as (
select cast('05/01/2017' as datetime)
union all
select date+1
from d
where date < '07/31/2017'
)
select DISTINCT t.loc_id, CONVERT(date, d.date)
FROM d LEFT OUTER JOIN #temp t ON d.date = t.CDate
GROUP BY t.loc_id, d.date
ORDER BY t.loc_id
As I said, this query returns me a list of dates in the date range, and all locations that submitted entries on that date, but I'd like to find a way to extract essentially the opposite information: if an office (specified by locationID) did not submit an entry on a given day, return only those locationIDs and the dates that they missed.
Sample data
EntryID | locationID | entryDate
=================================
1 1 07-01-2017
2 1 07-02-2017
3 2 07-02-2017
4 1 07-04-2017
Expected Result (for date range of 07-01 to 07-04)
locationID | missedEntryDate
============================
1 07-03-2017
2 07-01-2017
2 07-03-2017
2 07-04-2017
Your first step was good, you create a list of all dates, but you also need a list of all locations. Then you create a cross join to have all combinations and then you perform the left join to find out what is missing.
;with allDates(date) as (
select cast('05/01/2017' as datetime)
union all
select date+1
from d
where date < '07/31/2017'
), allLocations as (
SELECT DISTINCT loc_id
FROM #temp
), allCombinations as (
SELECT date, loc_id
FROM allDates
CROSS JOIN allLocations
)
SELECT AC.loc_id, AC.date
FROM allCombinations AC
LEFT JOIN #temp t
ON AC.date = t.CDate
AND AC.loc_id = t.loc_id
WHERE t.loc_id IS NULL -- didnt find a match on #temp
If your dataset is not too large you can try this:
select t.loc_id, CONVERT(date, d.date)
FROM d
-- Cross join dates to all available locs
CROSS JOIN (SELECT DISTINCT loc_id FROM #temp ) AS Locs
LEFT JOIN
( SELECT loc_id, t.CDate
FROM #temp
GROUP BY loc_id, d.date ) AS t ON d.date = t.CDate AND Locs.loc_id = t.loc_id
ORDER BY Locs.loc_id
This should be a bit faster:
;WITH cte AS (
SELECT a.LocID, RangeStart.CDate, ( CASE WHEN Input.LocID IS NULL THEN 1 ELSE 0 END ) AS IsMissing
FROM ( SELECT DISTINCT LocID FROM #temp ) AS a
CROSS JOIN ( SELECT CONVERT( DATETIME, '2017-05-01' ) AS CDate ) AS RangeStart
LEFT JOIN
( SELECT LocID, MIN( CDate ) AS CDate
FROM #temp
WHERE CDate = '2017-05-01'
GROUP BY LocID ) AS Input ON a.LocID = Input.LocID AND RangeStart.CDate = Input.CDate
UNION ALL
SELECT a.LocID, a.CDate + 1 AS CDate,
ISNULL( ItExists, 0 ) AS IsMissing
FROM cte AS a
OUTER APPLY( SELECT LocID, 1 AS ItExists FROM #temp AS b WHERE a.LocID = b.LocID AND a.CDate + 1 = b.CDate ) AS c
WHERE a.CDate < '2017-07-01'
)
SELECT * FROM cte OPTION( MAXRECURSION 0 )
You can also add an index:
CREATE INDEX IX_tmp_LocID_CDate ON #temp( LocID, CDate )
Sample data set for the second query:
CREATE TABLE #temp( LocID VARCHAR( 50 ), CDate DATETIME )
INSERT INTO #temp
VALUES
( '1', '2017-05-01' ), ( '1', '2017-05-02' ), ( '1', '2017-05-03' ), ( '1', '2017-05-04' ), ( '1', '2017-05-05' ),
( '2', '2017-05-01' ), ( '2', '2017-05-02' ), ( '2', '2017-05-03' ), ( '2', '2017-05-04' ), ( '2', '2017-05-05' )
;WITH d AS (
SELECT CAST( '05/01/2017' AS DATETIME ) AS date
UNION ALL
SELECT date + 2
FROM d
WHERE date < '2018-07-31'
)
INSERT INTO #temp
SELECT LocID, d.date
FROM ( SELECT DISTINCT LocID FROM #temp ) AS a
CROSS JOIN d
OPTION( MAXRECURSION 0 )

Query to get date rows older than a start date (not a simple WHERE)

I have a feeling this is quite simple, but I can't put my finger on the query. I'm trying to find all of the activities of an employee which corresponds to their start date in a specific location.
create table Locations (EmployeeID int, LocationID int, StartDate date);
create table Activities (EmployeeID int, ActivityID int, [Date] date);
insert into Locations values
(1, 10, '01-01-2010')
, (1, 11, '01-01-2012')
, (1, 11, '01-01-2013');
insert into Activities values
(1, 1, '02-01-2010')
, (1, 2, '04-01-2010')
, (1, 3, '06-06-2014');
Expected result:
EmployeeID LocationID StartDate EmployeeID ActivityID Date
1 10 '01-01-2010' 1 1 '02-01-2010'
1 10 '01-01-2010' 1 2 '04-01-2010'
1 11 '01-01-2013' 1 3 '06-06-2014'
So far, I have this, but it's not quite giving me the result I was hoping for. I somehow have to reference only the information from the most recent Location, which the la.StartDate <= a.Date does not filter out and includes information from older locations as well.
select *
from Locations la
inner join Activities a on la.EmployeeID = a.EmployeeID
and la.StartDate <= a.Date
Give this one a try:
with Locations as (
select
*
from (values
(1, 10, '01-01-2010')
, (1, 11, '01-01-2012')
, (1, 11, '01-01-2013')
) la (EmployeeID, LocationID, StartDate)
),
Activities as (
select
*
from (
values
(1, 1, '02-01-2010')
, (1, 2, '04-01-2010')
, (1, 3, '06-06-2014')
) a (EmployeeID, ActivityID, [Date])
)
select
la.*,
a.*
from Activities a
cross apply (
select
*
from (
select
la.*,
ROW_NUMBER() OVER (
PARTITION BY
EMPLOYEEID
ORDER BY
DATE DESC
) seqnum
from Locations la
where
la.EmployeeID = a.EmployeeID and
la.StartDate <= a.Date
) la
where
la.seqnum = 1
) la
Thank you all, but I managed to find the answer:
select *
from LocationAssociations la
inner join Activities a on la.EmployeeID = a.EmployeeID
and la.StartDate = (select max(StartDate) from LocationAssociations where StartDate >= la.StartDate and StartDate <= a.Date)

Rolling Prior13 months with Current Month Sales

Within a SQL Server 2012 database, I have a table with two columns customerid and date. I am interested in getting by year-month, a count of customers that have purchased in current month but not in prior 13 months. The table is extremely large so something efficient would be highly appreciated. Results table is shown after the input data. In essence, it is a count of customers that purchased in current month but not in prior 13 months (by year and month).
---input table-----
declare #Sales as Table ( customerid Int, date Date );
insert into #Sales ( customerid, date) values
( 1, '01/01/2012' ),
( 1, '04/01/2013' ),
( 1, '01/01/2014' ),
( 1, '01/01/2014' ),
( 1, '04/06/2014' ),
( 2, '04/01/2014' ),
( 3, '01/03/2012' ),
( 3, '01/03/2014' ),
( 4, '01/04/2012' ),
( 4, '04/04/2013' ),
( 5, '02/01/2010' ),
( 5, '02/01/2013' ),
( 5, '04/01/2014' )
select customerid, date
from #Sales;
---desired results ----
yearmth monthpurchasers monthpurchasernot13m
201002 1 1
201201 3 3
201302 1 1
201304 2 2
201401 2 1
201404 3 2
Thanks very much for looking at this!
Dev
You didn't provide the expected result, but I believe this is pretty close (at least logically):
;with g as (
select customerid, year(date)*100 + month(date) as mon
from #Sales
group by customerid, year(date)*100 + month(date)
),
x as (
select *,
count(*) over(partition by customerid order by mon
rows between 13 preceding and 1 preceding) as cnt
from g
),
y as (
select mon, count(*) as cnt from x
where cnt = 0
group by mon
)
select g.mon,
count(distinct(g.customerid)) as monthpurchasers,
isnull(y.cnt, 0) as cnt
from g
left join y on g.mon = y.mon
group by g.mon, y.cnt
order by g.mon
Tell me if this query helps. It extracts all the rows which meet your condition into a Table variable. Then, I use your query and join to this table.
declare #startDate datetime
declare #todayDate datetime
declare #tbl_Custs as Table(customerid int)
set #startDate = '04/01/2014' -- mm/dd/yyyy
set #todayDate = GETDATE()
insert into #tbl_Custs
-- purchased only this month
select customerid
from Sales
where ([date] >= #startDate and [date] <= #todayDate)
and customerid NOT in
(
-- purchased in past 13 months
select distinct customerid
from Sales
where ([date] >= DATEADD(MONTH,-13,[date])
and [date] < #startDate)
)
-- your query goes here
select year(date) as year
,month(date) as month
,count(distinct(c.customerid)) as monthpurchasers
from #tbl_Custs as c right join
Sales as s
on c.customerid = s.customerid
group by year(date) , month(date)
order by year(date) , month(date)
Below query will produce what you are looking for. I am not sure how performance will be on a big table (how big is your table?) but it is pretty straight forward so I think it will be ok. I simply calculate the 13 months earlier on CTE to find my sale window. Than join to the Sales table within that window / customer id and grouping records based on the unmatched records. You don't actually need 2 CTE's here you can do the DATEADD(mm,-13,date) on the join part of the second CTE but I thought it might be more clear this way.
P.S. If you need to change the time frame from 13 months to something else all you have to change is the DATEADD(mm,-13,date) this simply substracts 13 months from the date value.
Hope this helps or at least leads to a better solution
;WITH PurchaseWindow AS (
select customerid, date, DATEADD(mm,-13,date) minsaledate
FROM #Sales
), JoinBySaleWindow AS (
SELECT a.customerid, a.date,a.minsaledate,b.date earliersaledate
FROM PurchaseWindow a
LEFT JOIN #sales b ON a.customerid =b.customerid
--Find the sales for the customer within the last 13 months of original sale
AND b.date BETWEEN a.date AND a.minsaledate
)
SELECT DATEPART(yy,date) AS [year], DATEPART(mm, date) AS [month], COUNT(DISTINCT customerid) monthpurchases
FROM JoinBySaleWindow
--Exclude records where a sale within last 13 months occured
WHERE earliersaledate IS NULL
GROUP BY DATEPART(mm, date), DATEPART(yy,date)
Sorry about the typos they are fixed now.

SQL Server: Gap / Island, 365 day "contiguous" block

I have a table that looks like this:-
tblMeterReadings
id meter date total
1 1 03/01/2014 100.1
1 1 04/01/2014 184.1
1 1 05/01/2014 134.1
1 1 06/01/2014 132.1
1 1 07/01/2014 126.1
1 1 08/01/2014 190.1
This is an 8 day "contiguous block" from '2014-01-03' to '2014-01-08'.
In the real table there are "contiguous blocks" of years in length.
I need to select the MOST RESCENT CONTINUOUS 365 DAY BLOCK (filtered by meter column). If 365 cannot be found, then it should select next largest continuous block.
When I say CONTINUOUS I mean there must be no days missing.
This is beyond me, so if someone can solve... I will be very impressed.
using distinct to not count days with 2 sets of data
declare #gapdays int = 2 -- replace this with 365 in your case
;with x as
(
select datediff(d, '2014-01-01', [date])-dense_rank()over(order by [date]) grp
,[date]
from #t
)
select top 1 max([date]) last_date, min([date]) first_date, count(distinct [date]) days_in_a_row
from x
group by grp
having count(distinct [date]) >= #gapdays
order by max([date]) desc
There you go:
declare #tblMeterReadings table (id int, meter int, [date] date, total money)
insert into #tblMeterReadings ( id, meter, date, total )
values
(1, 1, '03/01/2014', 100.1),
(1, 1, '04/01/2014', 184.1),
(1, 1, '05/01/2014', 134.1),
(1, 1, '06/01/2014', 132.1),
(1, 1, '07/01/2014', 126.1),
(1, 1, '08/01/2014', 190.1),
(1, 1, '10/01/2014', 200.1),
(1, 1, '12/01/2014', 202.1),
(1, 1, '13/01/2014', 204.1)
;with data as (
select i = datediff(day, '2014', [date]), *
from #tblMeterReadings l
)
, islands as (
select island = l.i - row_number() over (order by i), l.*
from data l
)
, spans as (
select l = min(i), r = max(i)
from islands i
group by island
)
select *
from spans s
left join data l on s.l = l.i
left join data r on s.r = r.i
Most recent continuous block not exceeding 365 days in length will be as follows:
select top 1 *
from spans s
left join data l on s.l = l.i
left join data r on s.r = r.i
where s.l - s.r < 365
order by s.l - s.r desc, s.r desc
With recursive CTE and datepart(dayofyear, date):
with cte as
(
select id, meter, date, datepart(dayofyear, date) as x, cast(1 as int) as level, t1.date as startDate from tblMeterReadings t1
where meter = 1
and not exists(select * from tblMeterReadings t2 where (datepart(dayofyear, t1.date) - 1) = datepart(dayofyear, t2.date))
union all
select t1.id, t1.meter, t1.date, datepart(dayofyear, t1.date) as x, t2.level + 1, t2.startDate from tblMeterReadings t1
inner join cte t2 ON (datepart(dayofyear, t1.date)) = (datepart(dayofyear, t2.date) + 1)
)
select TOP 365 * from cte
where cte.startDate = (select top 1 startdate
from cte
--where Level <= 365
order by Level desc, startDate desc)
order by Level desc
OPTION ( MAXRECURSION 365 )
SQL Fiddle example

Resources