Related
I have a table with 2 columns, here is a sample:
ID
DateTime
1
2022-04-01 13:19:15
1
2022-04-01 13:20:19
1
2022-04-01 15:01:37
2
2022-04-01 10:08:21
2
2022-04-01 12:09:32
2
2022-04-01 15:07:25
I am trying to build a SQL query in Snowflake that returns all of the IDs that have a minimum of 2 or more records within ANY rolling 5 minute window. For the example data provided, ID 1 would be returned but ID 2 would not since all times for that ID are more than 5 minutes apart.
When attempting to find solutions to this problem, it seems that a recursive CTE may be the correct approach to solve this, but I can't seem to understand exactly how to approach this.
Any support or guidance would be appreciated!
Using CTE for data -
with data_cte (id, date1) as
(
select * from values
(1,'2022-04-01 13:19:15'::timestamp),
(1,'2022-04-01 13:20:19'::timestamp),
(1,'2022-04-01 15:01:37'::timestamp),
(2,'2022-04-01 10:08:21'::timestamp),
(2,'2022-04-01 12:09:32'::timestamp),
(2,'2022-04-01 15:07:25'::timestamp)
), twindow_cte as (
select
id,
nvl(
TIMESTAMPDIFF('minute',date1,
lead(date1) over (partition by id order by null)), 0) tdiff from
data_cte)
select distinct id from twindow_cte a where
1< (select count(*) from twindow_cte b
where b.tdiff <=5 and b.id = a.id);
ID
1
I have this query for retrieving rows from a SQL Server table:
SELECT
aid,
research_area_category_id,
CAST(research_area as VARCHAR(100)) [research_area],
COUNT(*) [Paper_Count]
FROM
sub_aminer_paper
GROUP BY
aid,
research_area_category_id,
CAST(research_area as VARCHAR(100))
HAVING
aid IN (SELECT
aid
FROM
sub_aminer_paper
GROUP BY
aid
HAVING
MIN(p_year) = 1990 AND MAX(p_year) = 2014 AND COUNT(pid) BETWEEN 10 AND 40
)
ORDER BY aid ASC, Paper_Count DESC
which returns this output:
aid research_area_category_id research_area Paper_Count
2937 33 markov chain 3
2937 33 markov decision process 1
2937 1 optimization problem 1
2937 27 real time application 1
2937 32 software product lines 1
11120 29 aspect oriented programming 4
11120 1 graph cut 2
11120 1 optimization problem 2
11120 32 uml class diagrams 1
11120 25 chinese word segmentation 1
11120 29 dynamic programming 1
11120 19 face recognition 1
11120 1 approximation algorithm 1
12403 2 differential equation 7
12403 1 data structure 2
12403 34 design analysis 1
12403 9 object detection 1
12403 27 operating system 1
12403 1 problem solving 1
12403 21 archiving system 1
12403 2 calculus 1
Now this is returning the output including all of rows concerned with respective aid's whereas I need only first 3 rows for each aid ORDER BY Paper_Count DESC i.e. rows containing value of Paper_Count 3, 1, 1 for aid 2937, 4,2,2 for 11120 and 7,2,2 for 12403.
Please help! Thanks.
one way is to apply row_number() over(partition by aid order by Paper_Count desc) as rn on your resultset and then select all records with rn<=3
with cte
as
(
SELECT
aid,
research_area_category_id,
CAST(research_area as VARCHAR(100)) [research_area],
COUNT(*) [Paper_Count]
FROM
sub_aminer_paper
GROUP BY
aid,
research_area_category_id,
CAST(research_area as VARCHAR(100))
HAVING
aid IN (SELECT
aid
FROM
sub_aminer_paper
GROUP BY
aid
HAVING
MIN(p_year) = 1990 AND MAX(p_year) = 2014 AND COUNT(pid) BETWEEN 10 AND 40
)
ORDER BY aid ASC, Paper_Count DESC
)
,
cte1
AS
(
SELECT * ,
ROW_NUMBER() OVER (PARTITION BY aid ORDER BY Paper_Count DESC) AS rn
FROM cte
)
SELECT * FROM cte1 WHERE rn<=3
In our company, our clients perform various activities that we log in different tables - Interview attendance, Course Attendance, and other general activities.
I have a database view that unions data from all of these tables giving us the ActivityView that looks like this.
As you can see some activities overlap - for example while attending an interview, a client may have been performing a CV update activity.
+----------------------+---------------+---------------------+-------------------+
| activity_client_id | activity_type | activity_start_date | activity_end_date |
+----------------------+---------------+---------------------+-------------------+
| 112 | Interview | 2015-06-01 09:00 | 2015-06-01 11:00 |
| 112 | CV updating | 2015-06-01 09:30 | 2015-06-01 11:30 |
| 112 | Course | 2015-06-02 09:00 | 2015-06-02 16:00 |
| 112 | Interview | 2015-06-03 09:00 | 2015-06-03 10:00 |
+----------------------+---------------+---------------------+-------------------+
Each client has a "Sign Up Date", recorded on the client table, which is when they joined our programme. Here it is for our sample client:
+-----------+---------------------+
| client_id | client_sign_up_date |
+-----------+---------------------+
| 112 | 2015-05-20 |
+-----------+---------------------+
I need to create a report that will show the following columns:
+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
We need this report in order to see how effective our programme is. An important aim of the programme is that we get every client to complete at least 5 hours of activity as quickly as possible.
So this report will tell us how long from sign up does it take each client to achieve this figure.
What makes this even trickier is that when we calculate 5 hours of total activity, we must discount overlapping activities:
In the sample data above the client attended an interview between 09:00 and 11:00.
On the same day they also performed CV updating activity from 09:30 to 11:30.
For our calculation, this would give them total activity for the day of 2.5 hours (150 minutes) - we would only count 30 minutes of the CV updating as the Interview overlaps it up to 11:00.
So the report for our sample client would give the following result:
+-----------+---------------------+--------------------------------------------+
| client_id | client_sign_up_date | date_client_completed_5_hours_of_activity |
+-----------+---------------------+--------------------------------------------+
| 112 | 2015-05-20 | 2015-06-02 |
+-----------+---------------------+--------------------------------------------+
So my question is how can I create the report using a select statement ?
I can work out how to do this by writing a stored procedure that will loop through the view and write the result to a report table.
But I would much prefer to avoid a stored procedure and have a select statement that will give me the report on the fly.
I am using SQL Server 2005.
See SQL Fiddle here.
with tbl as (
-- this will generate daily merged ovelaping time
select distinct
a.id
,(
select min(x.starttime)
from act x
where x.id=a.id and ( x.starttime between a.starttime and a.endtime
or a.starttime between x.starttime and x.endtime )
) start1
,(
select max(x.endtime)
from act x
where x.id=a.id and ( x.endtime between a.starttime and a.endtime
or a.endtime between x.starttime and x.endtime )
) end1
from act a
), tbl2 as
(
-- this will add minute and total minute column
select
*
,datediff(mi,t.start1,t.end1) mi
,(select sum(datediff(mi,x.start1,x.end1)) from tbl x where x.id=t.id and x.end1<=t.end1) totalmi
from tbl t
), tbl3 as
(
-- now final query showing starttime and endtime for 5 hours other wise null in case not completed 5(300 minutes) hours
select
t.id
,min(t.start1) starttime
,min(case when t.totalmi>300 then t.end1 else null end) endtime
from tbl2 t
group by t.id
)
-- final result
select *
from tbl3
where endtime is not null
This is one way to do it:
;WITH CTErn AS (
SELECT activity_client_id, activity_type,
activity_start_date, activity_end_date,
ROW_NUMBER() OVER (PARTITION BY activity_client_id
ORDER BY activity_start_date) AS rn
FROM activities
),
CTEdiff AS (
SELECT c1.activity_client_id, c1.activity_type,
x.activity_start_date, c1.activity_end_date,
DATEDIFF(mi, x.activity_start_date, c1.activity_end_date) AS diff,
ROW_NUMBER() OVER (PARTITION BY c1.activity_client_id
ORDER BY x.activity_start_date) AS seq
FROM CTErn AS c1
LEFT JOIN CTErn AS c2 ON c1.rn = c2.rn + 1
CROSS APPLY (SELECT CASE
WHEN c1.activity_start_date < c2.activity_end_date
THEN c2.activity_end_date
ELSE c1.activity_start_date
END) x(activity_start_date)
)
SELECT TOP 1 client_id, client_sign_up_date, activity_start_date,
hoursOfActivicty
FROM CTEdiff AS c1
INNER JOIN clients AS c2 ON c1.activity_client_id = c2.client_id
CROSS APPLY (SELECT SUM(diff) / 60.0
FROM CTEdiff AS c3
WHERE c3.seq <= c1.seq) x(hoursOfActivicty)
WHERE hoursOfActivicty >= 5
ORDER BY seq
Common Table Expressions and ROW_NUMBER() were introduced with SQL Server 2005, so the above query should work for that version.
Demo here
The first CTE, i.e. CTErn, produces the following output:
client_id activity_type start_date end_date rn
112 Interview 2015-06-01 09:00 2015-06-01 11:00 1
112 CV updating 2015-06-01 09:30 2015-06-01 11:30 2
112 Course 2015-06-02 09:00 2015-06-02 16:00 3
112 Interview 2015-06-03 09:00 2015-06-03 10:00 4
The second CTE, i.e. CTEdiff, uses the above table expression in order to calculate time difference for each record, taking into consideration any overlapps with the previous record:
client_id activity_type start_date end_date diff seq
112 Interview 2015-06-01 09:00 2015-06-01 11:00 120 1
112 CV updating 2015-06-01 11:00 2015-06-01 11:30 30 2
112 Course 2015-06-02 09:00 2015-06-02 16:00 420 3
112 Interview 2015-06-03 09:00 2015-06-03 10:00 60 4
The final query calculates the cumulative sum of time difference and selects the first record that exceeds 5 hours of activity.
The above query will work for simple interval overlaps, i.e. when just the end date of an activity overlaps the start date of the next activity.
A Geometric Approach
For another issue, I've taken a geometric approach to date
packing. Namely, I convert dates and times to a sql geometry
type and utilize geometry::UnionAggregate to merge the ranges.
I don't believe this will work in sql-server 2005. But your
problem was such an interesting puzzle that I wanted to see
whether the geometrical approach would work. So any future
users running into this problem that have access to a later
version can consider it.
Code Description
In 'numbers':
I build a table representing a sequence
Swap it out with your favorite way to make a numbers table.
For a union operation, you won't ever need more rows than in
your original table, so I just use it as the base to build it.
In 'mergeLines':
I convert the dates to floats and use those floats
to create geometrical points.
I then connect these points via STUnion and STEnvelope.
Finally, I merge all these lines via UnionAggregate. The resulting
'lines' geometry object might contain multiple lines, but if they
overlap, they turn into one line.
In 'redate':
I use the numbers CTE to extract the individual lines inside 'lines'.
I envelope the lines which here ensures that the lines are stored
only as its two endpoints.
I read the endpoint x values and convert them back to their time
representations (This is usually the end goal, but you need more).
I calculate the difference in minutes between activity start and
end dates (I do this first in seconds then divide by 60 for the
sake of a precision issue).
I calculate the cumulative sume of these minutes for each row.
In the outer query:
I align the previous cumulative minutes sum with each current row
I filter for the row where the 5hr goal was met but where the
previous minutes shows that the 5hr goal for the previous row
was not met.
I then calculate where in the current row's range the user has
met the 5 hours, to not only arrive at the date the five hour
goal was met, but the exact time.
The Code
with
numbers as (
select row_number() over (order by (select null)) i
from #activities -- where I put your data
),
mergeLines as (
select activity_client_id,
lines = geometry::UnionAggregate(line)
from #activities
cross apply (select
startP = geometry::Point(convert(float,activity_start_date), 0, 0),
stopP = geometry::Point(convert(float,activity_end_date), 0, 0)
) pointify
cross apply (select line = startP.STUnion(stopP).STEnvelope()) lineify
group by activity_client_id
),
redate as (
select client_id = activity_client_id,
activities_start_date,
activities_end_date,
minutes,
rollingMinutes = sum(minutes) over(
partition by activity_client_id
order by activities_start_date
rows between unbounded preceding and current row
)
from mergeLines ml
join numbers n on n.i between 1 and ml.lines.STNumGeometries()
cross apply (select line = ml.lines.STGeometryN(i).STEnvelope()) l
cross apply (select
activities_start_date = convert(datetime, l.line.STPointN(1).STX),
activities_end_date = convert(datetime, l.line.STPointN(3).STX)
) unprepare
cross apply (select minutes =
round(datediff(s, activities_start_date, activities_end_date) / 60.0,0)
) duration
)
select client_id,
activities_start_date,
activities_end_date,
met_5hr_goal = dateadd(minute, (60 * 5) - prevRoll, activities_start_date)
from (
select *,
prevRoll = lag(rollingMinutes) over (
partition by client_id
order by rollingMinutes
)
from redate
) ranker
where rollingMinutes >= 60 * 5
and prevRoll < 60 * 5;
I use MS SSMS 2008 R2 to extract data from our company management software, which registers our employee actions and schedules. The table has and ID field, which is unique to each entry. job is the activity the user is performing. user is the user ID. start_time and duration are exactly that. Then there is a "type" where 0 is login (the user logs into the job) and 1 is available time (while performing a job the user may be available or not). "reason" is the reason why the user has become unavailable (break, coffee, lunch, training, etc). Type 0 entries have no reason so reason is always null.
I need to extract the unavailable times by reason and all I'm being able to achieve is to do a DATEADD of duration to start_time in order to get end_time and then use Excel to manually calculate the times for each row.
The SQL table looks like this:
id job user start_time duration type reason
4436812 3 758 05-06-2015 09:00 125670 0 NULL
4436814 3 758 05-06-2015 09:00 6970 1 1004
4436944 3 758 05-06-2015 09:14 39280 1 1004
4437119 3 758 05-06-2015 10:20 0 1 1002
4437172 3 758 05-06-2015 10:35 18470 1 1004
4437312 3 758 05-06-2015 11:09 3960 1 1004
4437350 3 758 05-06-2015 11:16 0 1 1006
4437360 3 758 05-06-2015 11:19 30080 1 1004
4437638 3 758 05-06-2015 12:13 6730 1 1004
4437695 3 758 05-06-2015 12:24 0 1 1007
4438227 3 758 05-06-2015 13:43 NULL 0 NULL
4438228 3 758 05-06-2015 13:43 NULL 1 NULL
(job = 3 and user = 758)
This is the query I made:
select CONVERT(date,start_time) Data, a.job, a.user, convert(varchar(15),convert(datetime,a.start_time),108) StartTime, a.duration duracao,
convert(varchar(15),convert(datetime,DATEADD(second,a.duration/10,a.start_time)),108) EndTime, a.type, a.reason
from schedule_log a
where a.job = 3
and a.user = 758
and CONVERT(date,start_time) = '20150605'
order by a.start_time, a.type
Which translates to:
Date job user LogTime Avail NotAvail
2015-06-05 3 758 04:44:01 04:10:23 00:33:38
So, for each reason, I have to do a DATEDIFF from end time (start+duration) to either the next type 1 start_time or the previous type 0 end time, which ever happened first (the user may become unavailable and then logoff).
How do I do this?
ps: duration is in tenths of second.
Ok, here is my updated suggestion. It is broken into three steps for clarity, but the temp tables are unnecessary - they could become subqueries.
Step 1: Calculate the end time for each period of activity, excluding logins.
Step 2: Join each row to the row that occurred immediately after it, to get the unavailable time following each reason. Note: some of your timestamps do not line up properly, possibly as a result of storing duration in seconds but timestamps only to the minute.
Step 3: Total the unavailable time, and subtract from the duration of the login to get the available time.
Step 4: Total the unavailable time by reason.
SELECT *
,dateadd(s, duration / 10, start_time) AS Endtime
,row_number() OVER (
PARTITION BY job ,[user] ORDER BY start_time, [type]
) AS RN
INTO #temp2
FROM MyTable
WHERE [type] = 1
SELECT a.[user]
,a.job
,a.reason
,a.start_time
,a.type
,a.duration / 10 AS AvailableSeconds
,datediff(s, a.Endtime, b.start_time) AS UnavailableSeconds
INTO #temp3
FROM #temp2 a
LEFT JOIN #temp2 b
ON a.[user] = b.[user]
AND a.job = b.job
AND a.RN = b.RN - 1
SELECT cast(a.start_time AS DATE) AS [Date]
,a.job
,a.[user]
,b.duration / 10 AS LogTime
,b.duration / 10 - sum(UnavailableSeconds) AS Avail
,sum(UnavailableSeconds) AS NotAvail
FROM #temp3 a
LEFT JOIN MyTable b
ON a.job = b.job
AND a.[user] = b.[user]
AND b.[type] = 0
AND b.duration IS NOT NULL
GROUP BY cast(a.start_time AS DATE)
,a.job
,a.[user]
,b.duration
SELECT cast(a.start_time AS DATE) AS [Date]
,a.job
,a.[user]
,a.reason
,sum(UnavailableSeconds) AS NotAvail
FROM #temp3 a
where reason is not null
GROUP BY cast(a.start_time AS DATE)
,a.job
,a.[user]
,a.reason
I have a query that pulls out month/year totals for customers, and add the ntile ranking. If I were to be able to pull out the max subtotal for ntile 1, 2, 3, 4, and 5, I would ALMOST get what I'm after, but I do not know how to proceed.
For example, the result I want would look something like:
Month Year CustomerCode SubTotal ntile
1 2012 CCC 131.45 1
1 2012 CCC 342.95 2
1 2012 ELITE 643.92 3
1 2012 CCC 1454.05 4
1 2012 CCC 12971.78 5
2 2012 CCC 135.99 1
2 2012 CCI 370.47 2
2 2012 NOC 766.84 3
2 2012 ELITE 1428.26 4
2 2012 VBC 5073.20 5
3 2012 CCC 119.02 1
3 2012 CCC 323.78 2
3 2012 HUCC 759.66 3
3 2012 ELITE 1402.95 4
3 2012 CCC 7964.20 5
EXCEPT - I would expect ranking to be different customers like for month 2, but my base query isn't giving me that result - and I obviously don't know how to get it in T-SQL on SQL SERVER 2005 - in fact I'm not sure what I'm getting.
My next option is to pull a DataTable in C# and do some gymnastics to get there, but there has to be an easier way :)
My base query is
SELECT
i.DateOrdered
,LTRIM(STR(DATEPART(MONTH,i.DateOrdered))) AS [Month]
,LTRIM(STR(YEAR(i.Dateordered))) AS [Year]
,c.CustomerCode
,SUM(i.Jobprice) AS Subtotal
,NTILE(5) OVER(ORDER BY SUM(i.JobPrice)) AS [ntile]
FROM Invoices i
JOIN
Customers c
ON i.CustomerID = c.ID
WHERE i.DateOrdered >= '1/1/2012'
AND i.DateOrdered <= '9/30/2012'
GROUP BY YEAR(i.DateOrdered), MONTH(i.DateOrdered), i.DateOrdered, c.CustomerCode
ORDER BY LTRIM(STR(DATEPART(MONTH,i.DateOrdered))),
TRIM(STR(YEAR(i.Dateordered))),
SUM(i.JobPrice), c.CustomerCode ASC
I'd really appreciate help getting this right.
Thanks in advance
Cliff
If I read you correctly, what you are after is
For each month in the range,
Show 5 customers who have the greatest SUMs in that month
And against each customer, show the corresponding SUM.
In that case, this SQL Fiddle creates a sample table and runs the query that gives you the output described above. If you wanted to see what's in the created tables, just do simple SELECTs on the right panel.
The query is:
; WITH G as -- grouped by month and customer
(
SELECT DATEADD(D,1-DAY(i.DateOrdered),i.DateOrdered) [Month],
c.CustomerCode,
SUM(i.Jobprice) Subtotal
FROM Invoices i
JOIN Customers c ON i.CustomerID = c.ID
WHERE i.DateOrdered >= '1/1/2012' AND i.DateOrdered <= '9/30/2012'
GROUP BY DATEADD(D,1-DAY(i.DateOrdered),i.DateOrdered), c.CustomerCode
)
SELECT MONTH([Month]) [Month],
YEAR([Month]) [Year],
CustomerCode,
SubTotal,
Rnk [Rank]
FROM
(
SELECT *, RANK() OVER (partition by [Month] order by Subtotal desc) Rnk
FROM G
) X
WHERE Rnk <= 5
ORDER BY Month, Rnk
To explain, the first part (WITH block) is just a fancy way of writing a subquery, that GROUPs the data by month and Customer. The expression DATEADD(D,1-DAY(i.DateOrdered),i.DateOrdered) turns every date into the FIRST day of that month, so that the data can be easily grouped by month. The next subquery written in traditional form adds a RANK column within each month by the subtotal, which is finally SELECTed to give the top 5*.
Note that RANK allows for equal rankings, which may end up showing 6 customers for a month, if 3 of them are ranked equally at position 4. If that is not what you want, then you can change the word RANK to ROW_NUMBER which will randomly tie-break between equal Subtotals.
The query needs to be modified to only get the month and year dateparts. The issue you are having with the same customer showing multiple times in the same month is due to the inclusion of i.DateOrdered in the select and group by clauses.
The following query should give you what you need. Also, I suspect it is a typo on the next to last line of the query, but tsql doesn't have a TRIM() function only LTRIM and RTRIM.
SELECT
LTRIM(STR(DATEPART(MONTH,i.DateOrdered))) AS [Month]
,LTRIM(STR(YEAR(i.Dateordered))) AS [Year]
,c.CustomerCode
,SUM(i.Jobprice) AS Subtotal
,NTILE(5) OVER(ORDER BY SUM(i.JobPrice)) AS [ntile]
FROM Invoices i
JOIN
Customers c
ON i.CustomerID = c.ID
WHERE i.DateOrdered >= '1/1/2012'
AND i.DateOrdered <= '9/30/2012'
GROUP BY YEAR(i.DateOrdered), MONTH(i.DateOrdered), c.CustomerCode
ORDER BY LTRIM(STR(DATEPART(MONTH,i.DateOrdered))),
LTRIM(STR(YEAR(i.Dateordered))),
SUM(i.JobPrice), c.CustomerCode ASC
This gives these results
Month Year CustomerCode Subtotal ntile
1 2012 ELITE 643.92 2
1 2012 CCC 14900.23 5
2 2012 CCC 135.99 1
2 2012 CCI 370.47 1
2 2012 NOC 766.84 3
2 2012 ELITE 1428.26 4
2 2012 VBC 5073.20 4
3 2012 HUCC 759.66 2
3 2012 ELITE 1402.95 3
3 2012 CCC 8407.00 5
Try this:
declare #tab table
(
[month] int,
[year] int,
CustomerCode varchar(20),
SubTotal float
)
insert into #tab
select
1,2012,'ccc',131.45 union all
select
1,2012,'ccc',343.45 union all
select
1,2012,'ELITE',643.92 union all
select
2,2012,'ccc',131.45 union all
select
2,2012,'ccc',343.45 union all
select
2,2012,'ELITE',643.92 union all
select
3,2012,'ccc',131.45 union all
select
3,2012,'ccc',343.45 union all
select
3,2012,'ELITE',643.92
;with cte as
(
select NTILE(3) OVER(partition by [month] ORDER BY [month]) AS [ntile],* from #tab
)
select * from cte
Even in your base query you need to add partition by, so that you will get correct output.
I can't see how to solve this problem without double ranking:
You need to get the largest sums per customer & month.
You then need, for every month, to retrieve the top five of the found sums.
Here's how I would approach this:
;
WITH MaxSubtotals AS (
SELECT DISTINCT
CustomerID,
MonthDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, DateOrdered), 0),
Subtotal = MAX(SUM(JobPrice)) OVER (
PARTITION BY Customer, DATEADD(MONTH, DATEDIFF(MONTH, 0, DateOrdered), 0)
ORDER BY SUM(JobPrice)
)
FROM Invoices
GROUP BY
CustomerID,
DateOrdered
),
TotalsRanked AS (
SELECT
CustomerID,
MonthDate,
Subtotal,
Ranking = ROW_NUMBER() OVER (PARTITION BY MonthDate ORDER BY Subtotal DESC)
FROM MaxDailyTotals
)
SELECT
Month = MONTH(i.MonthDate),
Year = YEAR(i.MonthDate),
c.CustomerCode,
i.Subtotal,
i.Ranking
FROM TotalsRanked i
INNER JOIN Customers ON i.CustomerID = c.ID
WHERE i.Ranking <= 5
;
The first CTE, MaxSubtotals, determines the maximum subtotals per customer & month. Involving DISTINCT and a window aggregating function, it is essentially a "shortcut" for the following two-step query:
SELECT
CustomerID,
MonthDate,
Subtotal = MAX(Subtotal)
FROM (
SELECT
CustomerID,
MonthDate = DATEADD(MONTH, DATEDIFF(MONTH, 0, DateOrdered), 0),
Subtotal = SUM(JobPrice)
FROM Invoices
GROUP BY
CustomerID,
DateOrdered
) s
GROUP BY
CustomerID,
MonthDate
The other CTE, TotalsRanked, simply adds ranking numbers for the found susbtotals, partitioning by customer and month. As a final step, you only need to limit the rows to those that have rankings not greater than 5 (or whatever you might choose another time).
Note that using ROW_NUMBER() to rank the rows in this case guarantees that you'll get no more than 5 rows with the Ranking <= 5 filter. If there were two or more rows with the same subtotal, the would get distinct rankings, and in the end you might end up with an output like this:
Month Year CustomerCode Subtotal Ranking
----- ---- ------------ -------- -------
1 2012 CCC 1500.00 1
1 2012 ELITE 1400.00 2
1 2012 NOC 900.00 3
1 2012 VBC 700.00 4
1 2012 HUCC 700.00 5
-- 1 2012 ABC 690.00 6 -- not returned
-- 1 2012 ... ... ...
Even though there might be other customers with Subtotals of 700.00 for the same month, they wouldn't be returned, because they would be assigned rankings after 5.
You could use RANK() instead of ROW_NUMBER() to account for that. But note that you might end up with more than 5 rows per month then, with an output like this:
Month Year CustomerCode Subtotal Ranking
----- ---- ------------ -------- -------
1 2012 CCC 1500.00 1
1 2012 ELITE 1400.00 2
1 2012 NOC 900.00 3
1 2012 VBC 700.00 4
1 2012 HUCC 700.00 4
1 2012 ABC 700.00 4
-- 1 2012 DEF 690.00 7 -- not returned
-- 1 2012 ... ... ...
Customers with subtotals less than 700.00 wouldn't make it to the output because they would have rankings starting with 7, which would correspond to the ranking of the first under-700.00 sum if ranked by ROW_NUMBER().
And there's another option, DENSE_RANK(). You might want to use it if you want up to 5 distinct sums per month in your output. With DENSE_RANK() your output might contain even more rows per month than it would have with RANK(), but the number of distinct subtotals would be exactly 5 (or fewer if the original dataset can't provide you with 5). That is, your output might then look like this:
Month Year CustomerCode Subtotal Ranking
----- ---- ------------ -------- -------
1 2012 CCC 1500.00 1
1 2012 ELITE 1400.00 2
1 2012 NOC 900.00 3
1 2012 VBC 700.00 4
1 2012 HUCC 700.00 4
1 2012 ABC 700.00 4
1 2012 DEF 650.00 5
1 2012 GHI 650.00 5
1 2012 JKL 650.00 5
-- 1 2012 MNO 600.00 5 -- not returned
-- 1 2012 ... ... ...
Like RANK(), the DENSE_RANK() function assigns same rankings to identical values, but, unlike RANK(), it doesn't produce gaps in the ranking sequence.
References:
OVER Clause (Transact-SQL)
Ranking Functions (Transact-SQL)