SQL Server query : how can I SUM one SELECT to another? - sql

I have a problem with my SQL Server query, and I hope you will help me
The main select must output date and sum of the weight
How can I sum one select to another? Data is taking from one table Tbreport, and condition is - to sum weight of the concrete date and previous date (concrete date minus 1 day)
For example:
CONCRETE DATE WEIGHT
Jan 1 100
Jan 2 150
Jan 3 210
PREVIOUS DATE WEIGHT
Jan 1 100
Jan 2 250 (Jan 1 + Jan 2)
Jan 3 460 (Jan 1 + Jan 2 + Jan 3)
In real table I have format in seconds. For ex: 1358892000 seconds is 2013 Jan 23 0:00:00 and 1358978400 is 2013 Jan 23 23:59:00. And every time is own weight
Query:
SELECT CONVERT(varchar, DATEADD(s, TBreport.date, 25568), 102) AS DATE,
SUM(TBreport.weight)
+
(
SELECT SUM(TBreport.weight) AS WEIGHT
FROM TBreport INNER JOIN TBway ON TBreport.id_way = TBway.id
WHERE (SUBSTRING(TBway.name, 5, 8) LIKE 'to warehouse')
AND ... ???
GROUP BY CONVERT(varchar, DATEADD(s, TBreport.date, 25568), 102)
)
FROM TBreport INNER JOIN TBway ON TBreport.id_way = TBway.id
WHERE (SUBSTRING(TBway.name, 5, 8) LIKE 'to warehouse')
GROUP BY CONVERT(varchar, DATEADD(s, TBreport.date, 25568), 102)

I'm assuming date is unique in TBreport
SELECT this.date, SUM(upToThis.weight)
FROM TBreport this
INNER JOIN TBreport upToThis ON upToThis.date <= this.date
GROUP BY this.date

Related

Join table with dates list - include reference on NULLs

More brain freeze moments from me. I'm sure this will be an easy one.
I have two tables. One is a list of part usage by week. This is called TransactionsPerWeek and looks like this:
ItemPK xWeek xYear TotalQty
1234 2 2019 65
1234 4 2019 15
1234 5 2019 50
I also have a DateList table that has week numbers and years in it
xWeek xYear
1 2019
2 2019
3 2019
etc.
When I right join the two together on week and year I get
ItemPK xWeek xYear TotalQty
NULL 1 2019 0
1234 2 2019 65
NULL 3 2019 0
1234 4 2019 15
1234 5 2019 50
What I need is to have the ItemPK on every line, even if the TotalQty is 0. So in effect, I need:
ItemPK xWeek xYear TotalQty
1234 1 2019 0
1234 2 2019 65
1234 3 2019 0
1234 4 2019 15
1234 5 2019 50
This is my code...
SELECT itemfk,
dates.year,
dates.week,
isnull(transactionsperweek.TotalQty,0) as TotalQty
from (
SELECT iit.ItemFK,
year(iit.transactiondate) xYear,
datepart(wk,iit.transactiondate) xWeek,
abs(sum(iit.quantity)) TotalQty
from iteminventorytransaction iit
INNER JOIN ItemInventoryTransactionType iitt on ItemInventoryTransactionTypePK = iit.ItemInventoryTransactionTypeFK
where iit.itemfk = 5311
and iit.ItemInventoryTransactionTypeFK in (10,8)
and iit.TransactionDate BETWEEN
-- 1 year up to the sunday of last week
DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
AND Quantity < 0
group by iit.itemfk,
year(iit.transactiondate),
datepart(wk,iit.transactiondate)
) transactionsPerWeek
RIGHT JOIN (
select year,
week
from DatesList
where date > DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND date < DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
group by year,
week
) Dates ON dates.week = transactionsPerWeek.xWeek
AND dates.year = transactionsPerWeek.xYear
where week not in (52,53)
Hope this is clear enough. Thanks in advance.
You can use recursive cte :
with cte as (
select 1 as id, max(xWeek) as maxwk
from TransactionsPerWeek
union all
select id + 1, maxwk
from cte c
where c.id < maxwk
)
select coalesce(wk.ItemPK, wk1.ItemPK) as ItemPK, c.id as xWeek, wk.xYear, wk.TotalQty
from cte c left join
TransactionsPerWeek wk
on wk.xWeek = c.id outer apply
( select top (1) wk1.ItemPK
from TransactionsPerWeek wk1
where wk1.xWeek >= c.id and wk1.xWeek is not null
order by wk1.xWeek
) wk1;
Ok, so I did what #larnu suggested and cross joined the item with the dates, then left joined it to the transactionsperweek table and it worked. Thank you.
This is my code now;
SELECT itempk, week, year
, ISNULL(transactionsPerWeek.TotalQty,0) as TotalQty
from item
CROSS JOIN
(
select year, week from DatesList where date >
DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND date <
DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
group by year, week
) dates
LEFT JOIN
(
SELECT iit.ItemFK, year(iit.transactiondate) xYear, datepart(wk,iit.transactiondate) xWeek, abs(sum(iit.quantity)) TotalQty from iteminventorytransaction iit
INNER JOIN ItemInventoryTransactionType iitt on ItemInventoryTransactionTypePK = iit.ItemInventoryTransactionTypeFK
where iit.itemfk = 5311 and iit.ItemInventoryTransactionTypeFK in (10,8)
and iit.TransactionDate BETWEEN
-- 1 year up to the sunday of last week
DateAdd(wk,-51,DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE()))
AND
DATEADD(day,-1 - (DATEPART(weekday, GETDATE()) + ##DATEFIRST - 2) % 7,GETDATE())
AND Quantity < 0
group by iit.itemfk, year(iit.transactiondate), datepart(wk,iit.transactiondate)
) transactionsPerWeek
ON itempk = transactionsperweek.ItemFK and transactionsPerWeek.xYear = dates.year and transactionsPerWeek.xWeek = dates.week
where itempk = 5311
Use a cross join to generate the rows and a left join to bring in the results you already have.
Your question explicitly states that you have two tables. Hence, I don't know what your SQL code is doing, because it is not referencing those tables. So, based on the description:
select i.ItemPK, d.xWeek, d.xYear,
coalesce(TotalQty, 0) as TotalQty
from (select distinct itemPK from TransactionsPerWeek
) i cross join
DateList d left join
TransactionsPerWeek t
on t.itemPK = i.itemPK and
t.xWeek = d.xWeek and
t.xYear = d.xYear;
Of course if the "tables" are really subqueries, then I would recommend using CTEs and still this basic query structure.

Generate a calendar table with hours of the day using SQL Server 2012

Problem Statement:
I work for a fire department and I am working on statistical analysis of my data. One issue is to generate the number of calls for service for every hour of every day for a calendar year. I need a table that can be joined to the fire incidents that has every day of the year and every hour of each day. What I am hoping for is the following (using military time)
1 January 2017 00:00:00
1 January 2017 00:00:00
1 January 2017 01:00:00
1 January 2017 02:00:00
1 January 2017 03:00:00
1 January 2017 04:00:00
1 January 2017 05:00:00
1 January 2017 06:00:00
1 January 2017 07:00:00
1 January 2017 08:00:00
etc until the end of the year
31 December 2017 21:00:00
31 December 2017 22:00:00
31 December 2017 23:00:00
end of year
this table will allow me to join to the fire incidents table and I will be able to statistically calculate the number of incidents for each hour of the day and for each day of the year. A calculated table is necessary because the fire incidents table has gaps in it. For example; On january 1st at 0100 hours and 0200 hours and 0300 hours no emergency calls came in. Therefore I cannot make a calculation using the fire incidents table because there is no data for when no calls come in. The fire incidents table with gaps looks like:
TimeInterval, IncidentAddress
1 january 2017 00:00:00, 123 Elm Street
1 January 2017 04:00:00, 456 Oak Street
1 January 2017 05:00:00, 789 Maple Street
(Notice there are no fire calls for the hours of 0100, 0200 and 0300. Those are the gaps.)
Because there are gaps in the data where zeros should be the calculated averages necessary for a Poisson distribution are missing. The averages are incorrect.
Desired Output:
My goal is to have a calendar with an hours of day table to join to my fire incidents so my results set returns. Here is a rough draft of a query that returns every row from the calendar table and rows from the fire incidents table if there is matching value.
SELECT
TimeInterval
, COUNT(Incidents) AS [CountOfIncidents] /*this should probably be a COALESCE statement*/
FROM CalendarTable /*all rows from the calendar with hours and rows with data from FireIncidents*/
LEFT OUTER JOIN FireIncidents ON CalendarTable.timeInterval = FireIncidents.TimeInterval
GROUP BY TimeInterval
Query would return what I am hoping to achieve:
TimeInterval, CountOfIncidents
1 january 2017 00:00:00, 5
1 January 2017 01:00:00, 0
1 January 2017 02:00:00, 0
1 January 2017 03:00:00, 0
1 January 2017 04:00:00, 2
1 January 2017 05:00:00, 1
(Notice the hours of 0100, 0200 and 0300 have zero number of calls. This is what I want! Now I can create a histogram showing how many hours had zero calls. Or I can calculate an average value that takes into account zero calls for parts of the day.)
What I have tried:
I have tried the following but I cannot figure out how to create a table out of this and how to make it a finished product as you can see below in the Question paragraph.
DECLARE #DayOfYearNumber INT
DECLARE #HourNumber INT
SET #DayOfYearNumber = 1
SET #HourNumber = 0
PRINT 'Year' + ', ' + 'CalendarDayOfYear' + ', ' + 'HourOfDay'
WHILE #DayOfYearNumber < 366
BEGIN
SET #HourNumber = 0
WHILE #HourNumber < 24
BEGIN PRINT '2017' + ', ' + CONVERT(VARCHAR, #DayOfYearNumber) + ' ' + CONVERT(VARCHAR, #HourNumber)
SET #HourNumber = #HourNumber + 1
END
SET #DayOfYearNumber = #DayOfYearNumber + 1
END
Question:
How do I generate a calendar table in SQL Server 2012 that will have every day of the year and every hour of each day. My example again
1 January 2017 00:00:00
1 January 2017 01:00:00
1 January 2017 02:00:00
1 January 2017 03:00:00
1 January 2017 04:00:00
1 January 2017 05:00:00
1 January 2017 06:00:00
1 January 2017 07:00:00
1 January 2017 08:00:00
etc until the end of the year
31 December 2017 21:00:00
31 December 2017 22:00:00
31 December 2017 23:00:00
end of year
A simple method uses recursion:
with d as (
select cast('2017-01-01' as datetime) as dte
union all
select dateadd(hour, 1, dte)
from d
where dateadd(hour, 1, dte) < '2018-01-01'
)
select d.*
from d
option (maxrecursion 0);
Although recursion is surprisingly fast, if you are going to be needing this multiple times, you might want to consider have a numbers table around or storing this in a temporary or permanent table.
A alternative method to using a rCTE is a Tally Table, as it's not RBAR:
DECLARE #TopDate date = '20550101';
WITH N AS(
SELECT *
FROM (VALUES (NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL),(NULL)) V(N)),
Tally AS(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) - 1 AS I
FROM N N1
CROSS JOIN N N2
CROSS JOIN N N3
CROSS JOIN N N4
CROSS JOIN N N5
CROSS JOIN N N6)
SELECT DATEADD(HOUR, I, '20170101') AS DateValue
FROM Tally
WHERE DATEADD(HOUR, I, '20170101') < #TopDate;
You could achieve it using single query. All you need is tally(number) table:
WITH tally(n) AS (
SELECT ROW_NUMBER() OVER(ORDER BY 1/0)-1
FROM master..spt_values s1, master..spt_values s2, master..spt_values s3
)
-- INSERT INTO calendar(col_name)
SELECT DATEADD(HOUR,n,'20170101') AS d
FROM tally
WHERE DATEADD(HOUR,n,'20170101') <= '20180101'
Rextester Demo

Incremental and decremental count based on a date

Name Start_date end_date
aaa 01/02/2017 05/03/2017
bbb 03/05/2017 07/07/2017
ccc 02/01/2017 10/09/2017
I want to write a query that calculates the number of people who exist in the DB in a certain month/year.
Answer:
Jan 2017 1
Feb 2017 2
Mar 2017 3
Apr 2017 3
May 2017 2 (one person - aaa ,ended in May 2017)
Jun 2017 2
Jul 2017 1 (bbb ended in July 2017)
How do I write a PSQL query to get the desired output?
Thanks.
First, get the max and min dates in order to declare the dates range.
Second, with etc select all the month in the range.
Third, sum the number of the records in each dates.
Like:
declare #date date
declare #toDate date
select #date = min(Start_date),
#toDate = max(end_date)
from table_name
;With dt As
(
Select #date As [TheDate]
Union All
Select DateAdd(month, 1, TheDate) From dt Where [TheDate] < #toDate
)
select month(dt.TheDate),
year(dt.TheDate),
sum(case when table_name.Name is not null then 1 else 0 end)
from dt
left join table_name
on table_name.Start_date >= dt.TheDate
and table_name.end_date < dateadd(day,-1,dateAdd(month,1,dt.TheDate))

Total Number of Records per Week

I have a Postgres 9.1 database. I am trying to generate the number of records per week (for a given date range) and compare it to the previous year.
I have the following code used to generate the series:
select generate_series('2013-01-01', '2013-01-31', '7 day'::interval) as series
However, I am not sure how to join the counted records to the dates generated.
So, using the following records as an example:
Pt_ID exam_date
====== =========
1 2012-01-02
2 2012-01-02
3 2012-01-08
4 2012-01-08
1 2013-01-02
2 2013-01-02
3 2013-01-03
4 2013-01-04
1 2013-01-08
2 2013-01-10
3 2013-01-15
4 2013-01-24
I wanted to have the records return as:
series thisyr lastyr
=========== ===== =====
2013-01-01 4 2
2013-01-08 3 2
2013-01-15 1 0
2013-01-22 1 0
2013-01-29 0 0
Not sure how to reference the date range in the subsearch. Thanks for any assistance.
The simple approach would be to solve this with a CROSS JOIN like demonstrated by #jpw. However, there are some hidden problems:
The performance of an unconditional CROSS JOIN deteriorates quickly with growing number of rows. The total number of rows is multiplied by the number of weeks you are testing for, before this huge derived table can be processed in the aggregation. Indexes can't help.
Starting weeks with January 1st leads to inconsistencies. ISO weeks might be an alternative. See below.
All of the following queries make heavy use of an index on exam_date. Be sure to have one.
Only join to relevant rows
Should be much faster:
SELECT d.day, d.thisyr
, count(t.exam_date) AS lastyr
FROM (
SELECT d.day::date, (d.day - '1 year'::interval)::date AS day0 -- for 2nd join
, count(t.exam_date) AS thisyr
FROM generate_series('2013-01-01'::date
, '2013-01-31'::date -- last week overlaps with Feb.
, '7 days'::interval) d(day) -- returns timestamp
LEFT JOIN tbl t ON t.exam_date >= d.day::date
AND t.exam_date < d.day::date + 7
GROUP BY d.day
) d
LEFT JOIN tbl t ON t.exam_date >= d.day0 -- repeat with last year
AND t.exam_date < d.day0 + 7
GROUP BY d.day, d.thisyr
ORDER BY d.day;
This is with weeks starting from Jan. 1st like in your original. As commented, this produces a couple of inconsistencies: Weeks start on a different day each year and since we cut off at the end of the year, the last week of the year consists of just 1 or 2 days (leap year).
The same with ISO weeks
Depending on requirements, consider ISO weeks instead, which start on Mondays and always span 7 days. But they cross the border between years. Per documentation on EXTRACT():
week
The number of the week of the year that the day is in. By definition (ISO 8601), weeks start on Mondays and the first week of a
year contains January 4 of that year. In other words, the first
Thursday of a year is in week 1 of that year.
In the ISO definition, it is possible for early-January dates to be part of the 52nd or 53rd week of the previous year, and for
late-December dates to be part of the first week of the next year. For
example, 2005-01-01 is part of the 53rd week of year 2004, and
2006-01-01 is part of the 52nd week of year 2005, while 2012-12-31 is
part of the first week of 2013. It's recommended to use the isoyear
field together with week to get consistent results.
Above query rewritten with ISO weeks:
SELECT w AS isoweek
, day::text AS thisyr_monday, thisyr_ct
, day0::text AS lastyr_monday, count(t.exam_date) AS lastyr_ct
FROM (
SELECT w, day
, date_trunc('week', '2012-01-04'::date)::date + 7 * w AS day0
, count(t.exam_date) AS thisyr_ct
FROM (
SELECT w
, date_trunc('week', '2013-01-04'::date)::date + 7 * w AS day
FROM generate_series(0, 4) w
) d
LEFT JOIN tbl t ON t.exam_date >= d.day
AND t.exam_date < d.day + 7
GROUP BY d.w, d.day
) d
LEFT JOIN tbl t ON t.exam_date >= d.day0 -- repeat with last year
AND t.exam_date < d.day0 + 7
GROUP BY d.w, d.day, d.day0, d.thisyr_ct
ORDER BY d.w, d.day;
January 4th is always in the first ISO week of the year. So this expression gets the date of Monday of the first ISO week of the given year:
date_trunc('week', '2012-01-04'::date)::date
Simplify with EXTRACT()
Since ISO weeks coincide with the week numbers returned by EXTRACT(), we can simplify the query. First, a short and simple form:
SELECT w AS isoweek
, COALESCE(thisyr_ct, 0) AS thisyr_ct
, COALESCE(lastyr_ct, 0) AS lastyr_ct
FROM generate_series(1, 5) w
LEFT JOIN (
SELECT EXTRACT(week FROM exam_date)::int AS w, count(*) AS thisyr_ct
FROM tbl
WHERE EXTRACT(isoyear FROM exam_date)::int = 2013
GROUP BY 1
) t13 USING (w)
LEFT JOIN (
SELECT EXTRACT(week FROM exam_date)::int AS w, count(*) AS lastyr_ct
FROM tbl
WHERE EXTRACT(isoyear FROM exam_date)::int = 2012
GROUP BY 1
) t12 USING (w);
Optimized query
The same with more details and optimized for performance
WITH params AS ( -- enter parameters here, once
SELECT date_trunc('week', '2012-01-04'::date)::date AS last_start
, date_trunc('week', '2013-01-04'::date)::date AS this_start
, date_trunc('week', '2014-01-04'::date)::date AS next_start
, 1 AS week_1
, 5 AS week_n -- show weeks 1 - 5
)
SELECT w.w AS isoweek
, p.this_start + 7 * (w - 1) AS thisyr_monday
, COALESCE(t13.ct, 0) AS thisyr_ct
, p.last_start + 7 * (w - 1) AS lastyr_monday
, COALESCE(t12.ct, 0) AS lastyr_ct
FROM params p
, generate_series(p.week_1, p.week_n) w(w)
LEFT JOIN (
SELECT EXTRACT(week FROM t.exam_date)::int AS w, count(*) AS ct
FROM tbl t, params p
WHERE t.exam_date >= p.this_start -- only relevant dates
AND t.exam_date < p.this_start + 7 * (p.week_n - p.week_1 + 1)::int
-- AND t.exam_date < p.next_start -- don't cross over into next year
GROUP BY 1
) t13 USING (w)
LEFT JOIN ( -- same for last year
SELECT EXTRACT(week FROM t.exam_date)::int AS w, count(*) AS ct
FROM tbl t, params p
WHERE t.exam_date >= p.last_start
AND t.exam_date < p.last_start + 7 * (p.week_n - p.week_1 + 1)::int
-- AND t.exam_date < p.this_start
GROUP BY 1
) t12 USING (w);
This should be very fast with index support and can easily be adapted to intervals of choice.
The implicit JOIN LATERAL for generate_series() in the last query requires Postgres 9.3.
SQL Fiddle.
Using across joinshould work, I'm just going to paste the markdown output from SQL Fiddle below. It would seem that your sample output is incorrect for series 2013-01-08: the thisyr should be 2, not 3. This might not be the best way to do this though, my Postgresql knowledge leaves a lot to be desired.
SQL Fiddle
PostgreSQL 9.2.4 Schema Setup:
CREATE TABLE Table1
("Pt_ID" varchar(6), "exam_date" date);
INSERT INTO Table1
("Pt_ID", "exam_date")
VALUES
('1', '2012-01-02'),('2', '2012-01-02'),
('3', '2012-01-08'),('4', '2012-01-08'),
('1', '2013-01-02'),('2', '2013-01-02'),
('3', '2013-01-03'),('4', '2013-01-04'),
('1', '2013-01-08'),('2', '2013-01-10'),
('3', '2013-01-15'),('4', '2013-01-24');
Query 1:
select
series,
sum (
case
when exam_date
between series and series + '6 day'::interval
then 1
else 0
end
) as thisyr,
sum (
case
when exam_date + '1 year'::interval
between series and series + '6 day'::interval
then 1 else 0
end
) as lastyr
from table1
cross join generate_series('2013-01-01', '2013-01-31', '7 day'::interval) as series
group by series
order by series
Results:
| SERIES | THISYR | LASTYR |
|--------------------------------|--------|--------|
| January, 01 2013 00:00:00+0000 | 4 | 2 |
| January, 08 2013 00:00:00+0000 | 2 | 2 |
| January, 15 2013 00:00:00+0000 | 1 | 0 |
| January, 22 2013 00:00:00+0000 | 1 | 0 |
| January, 29 2013 00:00:00+0000 | 0 | 0 |

Parameter month selection: Make a query that shows previous month, 12 months ago and last 12 months average

I wanted to play around with my Total_Sales table.
This is how the data looks like (using SQL Server 2008 R2)
Name Year Month Sales
------ ---- ----- -----
Alfred 2011 1 100
Alfred 2011 2 200
Alfred 2011 3 300
Alfred 2011 4 400
Alfred 2011 5 500
Alfred 2011 6 600
Alfred 2011 7 700
Alfred 2011 8 800
Alfred 2011 9 900
Alfred 2011 10 500
Alfred 2011 11 500
Alfred 2011 12 500
The SQL query I want to create should display the data like this:
Name Year Month Sales Prev_Month Month_Last_Year_Sales Last_12_Month_AVG
------ ---- ----- ----- ---------- --------------------- -----------------
Alfred 2011 1 100 NULL (year 2010, month 1) (2010_01 to 2011_01)/(12)
Alfred 2011 2 200 100 (year 2010, month 2) (2010_02 to 2011_02)/(12)
Alfred 2011 3 300 200 (year 2010, month 3) (2010_03 to 2011_03)/(12)
Alfred 2011 4 400 300 (year 2010, month 4) (2010_04 to 2011_04)/(12)
Alfred 2011 5 500 400 (year 2010, month 5) (2010_05 to 2011_05)/(12)
Alfred 2011 6 600 500 (year 2010, month 6) (2010_06 to 2011_06)/(12)
Alfred 2011 7 700 600 (year 2010, month 7) (2010_07 to 2011_07)/(12)
Alfred 2011 8 800 700 (year 2010, month 8) (2010_08 to 2011_08)/(12)
Alfred 2011 9 900 800 (year 2010, month 9) (2010_09 to 2011_09)/(12)
Alfred 2011 10 500 900 (year 2010, month 10) (2010_10 to 2011_10)/(12)
Alfred 2011 11 500 500 (year 2010, month 11) (2010_11 to 2011_11)/(12)
Alfred 2011 12 500 500 (year 2010, month 12) (2010_12 to 2011_12)/(12)
To copy the prior month I am using this: Copy prior month value and insert into new row
SELECT
TS.name,
TS.year,
TS.month,
TS.sales,
COALESCE(TS2.sales, 0) AS prior_month_sales
FROM
TotalSales TS
LEFT OUTER JOIN TotalSales TS2 ON
TS2.name = TS.name AND
(
(TS2.year = TS.year AND TS2.month = TS.month - 1) OR
(TS.month = 1 AND TS2.month = 12 AND TS2.year = TS.year - 1)
)
The NULL in Prev_Month is to show that the start of the Total_Sales was in year 2011 month 1, so no prior data for this example.
I am planning to use a parameter, where you select a month.
Thanks for any help!
SELECT
[this_month].*,
[last_month].Sales AS [prev_month_sales],
[last_year].Sales AS [month_last_year_sales],
[yearly].AverageSales AS [last_12_month_average]
FROM
Total_Sales AS [this_month]
LEFT JOIN
Total_Sales AS [last_month]
ON [last_month].Name = [this_month].Name
AND (
([last_month].Year = [this_month].Year AND [last_month].Month = [this_month].Month - 1)
OR ([last_month].Year = [this_month].Year - 1 AND [last_month].Month = 12 AND [this_month].Month = 1)
)
LEFT JOIN
TotalSales AS [last_year]
ON [last_year].Name = [this_month].Name
AND [last_year].Year = [this_month].Year - 1
AND [last_year].Month = [this_month].Month
CROSS APPLY
(
SELECT
AVG(Sales) AS AverageSales
FROM
Total_Sales
WHERE
Name = [this_month].Name
AND (
(Year = [this_month].Year AND Month <= [this_month].Month)
OR (Year = [this_month].Year - 1 AND Month > [this_month].Month)
)
)
AS [yearly]
The Average isn't the value divided by 12, as there are not always 12 months worth of data in the preceding year. But the AVG() function takes care of that for you.
Also, I'd highly reccomend against using YEAR and MONTH fields. Instead I would recommend using a DATETIME field to represent the "Month Start" and using SQL Server's Date functions...
Last Month : MonthStart = DATEADD(MONTH, -1, ThisMonth)
A Year Ago : MonthStart = DATEADD(YEAR, -1, ThisMonth)
Last Year : MonthStart > DATEADD(YEAR, -1, ThisMonth) AND MonthStart <= ThisMonth
Another answer that I have no idea is faster or not...
WITH sales AS (
SELECT
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Year, Month) AS month_id,
*
FROM
yearly_sales
)
SELECT
Name = [this_month].Name,
Year = MAX([this_month].Year),
Month = MAX([this_month].Month),
Sales = MAX([this_month].Sales),
Last_Month = MAX(CASE WHEN [13_months].month_id = [this_month].month_id - 1 THEN [13_months].Sales END),
Last_Year = MAX(CASE WHEN [13_months].month_id = [this_month].month_id - 12 THEN [13_months].Sales END),
Yearly_AVG = AVG(CASE WHEN [13_months].month_id > [this_month].month_id - 12 THEN [13_months].Sales END)
FROM
Sales AS [this_month]
INNER JOIN
Sales AS [13_months]
ON [13_months].Name = [this_month].Name
AND [13_months].month_id <= [this_month].month_id
AND [13_months].month_id >= [this_month].month_id - 12
GROUP BY
[this_month].Name
From AceAlfred -
One problem I have run into, maybe you know a quick fix? When a employee has not booked his sales for a previous month there is no data to display for this individual. Is there a way to add a row with the missing employee, where the "sales" is set to 0 and still pull the data for the other rows? Ex. Year 2012 -- Month 1 -- Name Alfred -- Sales 0 -- Prev 500
One approach is to "fix" your data, ensuring it always has values in it. I'd recommend doing that in whatever system is populating your data. Or as a nightly batch that checks for people that didn't enter their data and sticks in 0's for you (To be updated if/when the real data arrives). But if you can't...
CREATE TABLE agent (id INT, name NVARCHAR(128), start_date DATETIME, leave_date DATETIME);
-- populate with your agents
CREATE TABLE calendar (year DATETIME, month DATETIME, day DATETIME);
-- populate with all dates you want to report on
CREATE TABLE sales (agent_id INT, month_start DATETIME, total INT);
-- populate with your data
WITH new_raw_data AS
(
SELECT
agent.id AS [agent_id],
calendar.month AS [month_start],
COALESCE(sales.total, 0) AS [total]
FROM
agent
INNER JOIN
calendar
ON calendar.month_start >= COALESCE(DATEADD(month, -1, agent.start_date), '2000 Jan 01')
AND calendar.month_start <= COALESCE(agent.leave_date, '2079 Dec 31')
LEFT JOIN
sales
ON sales.agent_id = agent.id
AND sales.month_start = calendar.month_start
WHERE
calendar.month_start = calendar.day -- Only use records for the start of each month
)
,
<your other queries, using the nicely cleaned data, go here.>