SQL query to get current and last year sales - sql

I have the following table Sales:
Date Store Sales
1/1/2015 St01 12123
1/1/2015 St02 3123
1/1/2016 St01 4213
1/1/2016 St03 2134
When I try to self join to get this year and last year sales the closed store is not showing up.
The result should be like this:
Date Store This year Sales Last Year Sales
1/1/2016 St01 4213 1212
1/1/2016 St02 0 3123
1/1/2016 St03 2134 0
My query as follows:
SELECT CY.DATE,
CY.store cy.Sales,
LY.sales
FROM sales CY,
sales LY
WHERE CY.store(+) = LY.store(+)
AND LY.DATE = CY.DATE - 365

Oracle Setup:
CREATE TABLE sales ( "DATE", Store, Sales ) AS
SELECT DATE '2015-01-01', 'St01', 12123 FROM DUAL UNION ALL
SELECT DATE '2015-01-01', 'St02', 3123 FROM DUAL UNION ALL
SELECT DATE '2016-01-01', 'St01', 4213 FROM DUAL UNION ALL
SELECT DATE '2016-01-01', 'St03', 2134 FROM DUAL;
Query:
SELECT TRUNC( SYSDATE, 'YY' ) AS "DATE",
Store,
SUM( CASE WHEN "DATE" = TRUNC( SYSDATE, 'YY' )
THEN sales END )
AS "This year sales",
SUM( CASE WHEN "DATE" = ADD_MONTHS( TRUNC( SYSDATE, 'YY' ), -12 )
THEN sales END )
AS "Last year sales"
FROM sales
GROUP BY store
ORDER BY store;
Output:
DATE STORE This year sales Last year sales
------------------- ----- --------------- ---------------
2016-01-01 00:00:00 St01 4213 12123
2016-01-01 00:00:00 St02 3123
2016-01-01 00:00:00 St03 2134

What you need is called Pivoting Table. Although Oracle has specific clauses to do it, you can use just plain and pure SQL to do so, like this:
SELECT store,
SUM(CASE WHEN Extract(year FROM DATE) = Extract(year FROM SYSDATE) THEN
sales
ELSE 0
END) AS "This year Sales",
SUM(CASE WHEN Extract(year FROM DATE) = Extract(year FROM SYSDATE) - 1 THEN
sales
ELSE 0
END) AS "Last year Sales"
FROM sales
WHERE Extract(year FROM DATE) >= Extract(year FROM SYSDATE) - 1
GROUP BY store
ORDER BY store
It would show:
Store This year Sales Last year Sales
St01 4213 12123
St02 0 3123
St03 2134 0
Note that makes no sense to have to column date as the first column. You couldn't group by it to show the output you want.
See the equivalent of this query here on fiddle: http://sqlfiddle.com/#!15/7662d8/6

Since I want the query to return day by day sales I used MT0 answer and added the dates, this way I can get the data for all year days.
WITH AllYear AS
(select to_date('2016-01-01', 'yyyy-mm-dd') + level - 1 AS dobs
from dual
connect by level <= 366)
SELECT dobs AS "DATE",
Store,
nvl(SUM(CASE
WHEN t.Date = dobs THEN
t.sales
END),
0) AS "This Year Sales",
nvl(SUM(CASE
WHEN t.Date = dobs-365 THEN
t.sales
END),
0) AS "Last Year Sales"
FROM Sales t,AllYear
where dobs='01-Jan-2016'
GROUP BY Store
ORDER BY Store;

The general solution is a full outer join, which includes all records from both joined tables. I don't know the Oracle syntax, but in MS SQL Server it would be something like this:
SELECT ISNULL(CY.DATE, LY.DATE) as DATE,
ISNULL(CY.store, LY.store) as STORE,
isnull(cy.Sales, 0),
isnull(LY.sales, 0)
FROM sales CY FULL OUTER JOIN sales LY
ON CY.store = LY.store
AND (CY.DATE IS NULL OR
DATEPART(year, LY.DATE) = DATEPART(year, CY.DATE) - 1
ISNULL(a, b) gives a if A IS NOT NULL, else b. DATEPART extracts specified part of a date; I'm comparing a difference of exactly one year rather than 365 days, in case "last year" is a leap year/

I only work with SQL Server. If anything is different, try to apply the same logic.
Declaring a temporary table to test the query:
DECLARE #Sales TABLE (
[Date] DATE,
Store NVARCHAR(10),
Sales INT
)
INSERT INTO #Sales VALUES
('1/1/2015','St01',12123),
('1/1/2015','St02',3123),
('1/1/2016','St01',4213),
('1/1/2016','St03',2134);
SELECT * FROM #Sales;
The actual query:
SELECT
CY_Date = CASE
WHEN CY.Date IS NULL THEN DATEADD(YEAR, 1, LY.Date)
ELSE CY.Date
END,
LY_Date = CASE
WHEN LY.Date IS NULL THEN DATEADD(YEAR, -1, CY.Date)
ELSE LY.Date
END,
Store = CASE
WHEN CY.Store IS NULL THEN LY.Store
ELSE CY.Store
END,
ISNULL(CY.Sales, 0) AS CY_Sales,
ISNULL(LY.Sales, 0) AS LY_Sales
FROM #Sales CY
FULL JOIN #Sales LY ON (CY.Store = LY.Store AND LY.Date = DATEADD(YEAR, -1, CY.Date))
WHERE (CY.Date = '1/1/2016' OR CY.Date IS NULL)
AND (LY.Date = DATEADD(YEAR, -1, '1/1/2016') OR LY.Date IS NULL);
Result:
CY_Date LY_Date Store CY_Sales LY_Sales
2016-01-01 2015-01-01 St01 4213 12123
2016-01-01 2015-01-01 St03 2134 0
2016-01-01 2015-01-01 St02 0 3123
How it works:
The FULL JOIN will will combine by the Store and the lines from the current and the year before.
The WHERE clause will filter by the current date '1/1/2016'. The NULLs are allowed because sometimes you don't have lines for the current or for the last year.
On the columns, CASES are used to create the dates if they are null (If the current date is null, get the last year + 1 year, and vice versa), to create the store if they are null and to place a zero instead of a null on the sales columns.

Related

Summing sales dollars for most recent month and 2nd most recent month

For each of the 12 months, I'm looking to create a field that sums the sales dollars at the account level for the most recent month and the 2nd most recent month based on the current date.
For example, given that today's date is 10/6/22, 'MostRecentNovember' would sum up sales from November 2021. '2ndMostRecentNovember' would sum up sales from November 2020. Once the current date moves into November 2022, this query would adjust to pull MostRecentNovember sales from 2022 and 2ndMostRecentNovember sales from 2021.
Conversely, given that today's date is 10/6/22 'MostRecentJune' would sum up sales from June 2022 and '2ndMostRecentJune' would sum up sales from June 2021.
Below is my attempt at this code, I think this gets partially there, but not sure it's exactly what I want
SELECT NovemberMostRecent_Value =
sum(case when datepart(year,tran_date) = datepart(year, getdate())
AND DATEPART(month, tran_date) = 11 then value else 0 end)
NovemberSecondMostRecent_Value =
sum(case when datepart(year,tran_date) = datepart(year, getdate())-1
AND DATEPART(month, tran_date) = 11 then value else 0 end)
Here's a snippet of the source data table
account_no
tran_date
value
123
11/22/21
500
123
11/1/21
500
123
11/20/20
1500
123
6/3/22
5000
123
6/4/21
2000
456
11/3/20
525
456
11/4/21
125
Per Request in Comments. A table of desired Results
account_no
NovemberMostRecent
November2ndMostRecent
June MostRecent
June2ndMostRecent
123
1000
1500
5000
2000
456
125
525
0
0
Why don't you just sum up the sales then group by month and year for the last two years? Wouldn't that solve the problem?
Or you can show a table that depicts what you are trying to achieve.
This should work fine.
Note: I only assume the account_no is the same for all the rows, if they are different, then you will need to pass it as a condition in the subquery.
WITH CTE AS
(SELECT (SELECT SUM(value) FROM tablename WHERE datepart(year, tran_date) = YEAR(getdate()) AND datepart(month, tran_date) = 11)
AS first_value,
(SELECT SUM(value) FROM tablename WHERE datepart(year, tran_date) = YEAR(getdate())-1 AND datepart(month, tran_date) = 11)
AS second_value,
(SELECT SUM(value) FROM tablename WHERE datepart(year, tran_date) = YEAR(getdate())-2 AND datepart(month, tran_date) = 11)
AS third_value)
SELECT IIF (first_value>0, first_value, second_value) AS NovemberMostRecent_Value,
IIF (first_value>0, second_value, third_value) AS NovemberSecondMostRecent_Value FROM CTE;

SUM values from given month + previous month

Using SQL oracle.
I tried searching, but couldn't find any solution to this particular problem.
I need to sum the amounts from "this month" + "the previous month" over longer periods.
With this query:
SELECT
to_char(sales_date, 'YYYYMM') date,
count(*) amount
FROM sales
WHERE sales_date > sysdate-90
GROUP BY to_char(sales_date, 'YYYYMM')
ORDER BY to_char(sales_date, 'YYYYMM') desc
It gives me a result like this:
DATE AMOUNT
202104 55
202103 12
202102 46
202101 31
I am looking for this result:
DATE AMOUNT
202104 67 (Sales from april + march)
202103 58 (Sales from march + february)
202102 77 (Sales from february + january)
202101 31 (plus whatever the sales were in december 2020)
And so on
I am drawing a complete blank on how I can accomplish this, any ideas?
You can use lag():
SELECT date, amount + LAG(amount, 1, 0) OVER (ORDER BY date)
FROM (SELECT to_char(s.sales_date, 'YYYYMM') as date,
count(*) as amount
FROM sales s
GROUP BY to_char(s.sales_date, 'YYYYMM')
) s
WHERE date > sysdate-90
ORDER BY date desc;
I would also recommend add_months() instead of 90 days, but that is your business logic.

Split single row into multiple rows based on week of the month in date

I have a table like this
Id Valid_From Valid_To
9744 24/06/2019 07/07/2019
9745 12/08/2019 31/12/9999
I would like to split this table into multiple rows based on the week like this by joining to the date table
Id Valid_from Valid_To Month Week
9744 24/06/2019 07/07/2019 June 4
9744 24/06/2019 07/07/2019 July 1
9744 24/06/2019 07/07/2019 July 2
9745 12/08/2019 31/12/9999 August 2
9745 12/08/2019 31/12/9999 August 3
9745 12/08/2019 31/12/9999 August 4
In this case there will be 3 rows as the valid from and valid two falls between these 3 weeks for ID - 9744
For ID - 9745 the Valid_to date is infinity so we need to just take all the week in the current month from the valid_from date
I then just need to append the output with Month and the Week number
Can someone help me to write a query to have this output?
Thanks
You mention a "date" table. If you have one then you can use a join like this:
select distinct t.id, valid_from, t.valid_to, d.month, d.week
from yourtable t join
date d
on d.date >= t.valid_from and
d.date <= t.valid_to;
If I understand your question right, you need to list all month names and week numbers of these months' existing between valid_from and valid_to dates. I did it by following query:
SELECT
Q.ID,
Q.VALID_FROM,
Q.VALID_TO,
Q.MONTH_NAME,
WEEK_NUMBER
FROM
(
SELECT
CEIL((Q.DATES_BETWEEN_INTERVAL - FIRST_DAY_OF_MONTH + 1) / 7) WEEK_NUMBER,
TO_CHAR(Q.DATES_BETWEEN_INTERVAL, 'MONTH', 'NLS_DATE_LANGUAGE = American') MONTH_NAME,
Q.*
FROM
(
SELECT
LEVEL + S.VALID_FROM DATES_BETWEEN_INTERVAL,
TRUNC(LEVEL + S.VALID_FROM, 'MONTH') FIRST_DAY_OF_MONTH,
S.* FROM
(
SELECT T.*,
(CASE WHEN EXTRACT(YEAR FROM T.VALID_TO) = 9999 THEN LAST_DAY(T.VALID_FROM) ELSE T.VALID_TO END) - T.VALID_FROM DAYS_COUNT
FROM AAA_TABLE T
) S
CONNECT BY LEVEL <= S.DAYS_COUNT
) Q
) Q
GROUP BY
Q.ID,
Q.VALID_FROM,
Q.VALID_TO,
Q.MONTH_NAME,
WEEK_NUMBER
ORDER BY
Q.ID,
Q.VALID_FROM,
Q.VALID_TO,
Q.MONTH_NAME,
WEEK_NUMBER;
But there must be 5th week if the date greater than 28th day of month. Hope this will help you.

How to extract random dates targets/sales data from monthly targets

I have monthly targets defined for the different category of items for the complete year.
Example:
January Target for A Category - 15,000
January Target for R Category - 10,000
January Target for O Category - 5,000
Actual Sales for A Category January - 18,400
Actual Sales for R Category January - 8,500
Actual Sales for O Category January - 3,821
The SQL query to compare actual sales with target will be simple as follows:
SELECT TO_CHAR (Sales_Date, 'MM') Sales_Month,
Sales_Category,
SUM (Sales_Value) Sales_Val_Monthly,
Target_Month,
Target_Category,
Target_Value
FROM Sales_Data, Target_Data
WHERE TO_CHAR (Sales_Date, 'MM') = Target_Month
AND Sales_Category = Target_Category
GROUP BY TO_CHAR (Sales_Date, 'MM'),
Target_Month,
Target_Category,
Sales_Category,
Target_Value;
Now I have a requirement that user will input FROM_DATE and TILL_DATE in the report parameter and the starting/ending date can be random, it will not represent a complete month or week, the start date can be 12/01/2018 and end date can be 15/01/2018, i.e., data for 4 days. The result should calculate the actual data for 4 days, calculate the target for 4 days considering the fact that there will be 6 working days (Sunday is a holiday) and if the date range includes Sunday, it should not be considered.
Also, the number of days in a month should be considered and the date parameters may contain some days from one month and some days from another month or maybe more than one month.
Target_Table (Target_Data)
Target_Year Target_Month Target_Category Target_Value
2018 01 A 15000
2018 02 A 8500
2018 03 A 9500
2018 01 R 15000
2018 02 R 8500
2018 03 R 9500
2018 01 O 15000
2018 02 O 8500
2018 03 O 9500
Sales Table (Sales_Data)
Inv_Txn Inv_No Sales_Date Item_Code Sales_Category Qty Rate Sales_Value Inv_Locn Inv_SM_ID
A21 2018000001 02/01/2018 XXXX A 2 5.5 11 O001 XXXX
R32 2018000001 27/02/2018 XXXX R 3 9.5 28.5 O305 XXXX
O98 2018000001 12/03/2018 XXXX O 12 12.5 150 O901 XXXX
U76 2018000001 18/01/2018 XXXX A 98 5.5 539 O801 XXXX
B87 2018000001 19/02/2018 XXXX R 2 9.5 19 O005 XXXX
A21 2018000002 13/03/2018 XXXX R 45 9.5 427.5 O001 XXXX
B87 2018000002 14/03/2018 XXXX O 12 12.5 150 O005 XXXX
Desired Output (From Date: 27/02/2018 Till Date: 06/03/2018)
Target_Category Target_Value Sales_Value
A 87.52 21.88
A 96.25 24.06
A 74.25 18.56
R 100.25 25.06
R 800.2 200.05
R 25.1 6.28
O 75.5 18.88
O 98.1 24.53
O 25.5 6.38
The first step might be to see whether we can get the number of Sundays in a given month. As it turns out, we can - and we don't have to use any SQL tricks or PL/SQL:
SELECT EXTRACT( DAY FROM LAST_DAY(SYSDATE) ) AS month_day_cnt
, CEIL( ( LAST_DAY(TRUNC(SYSDATE, 'MONTH')) - NEXT_DAY(TRUNC(SYSDATE, 'MONTH')-1, 'SUN') + 1 ) / 7 ) AS sunday_cnt
FROM dual;
This will give us the number of days in a given month as well as the number of Sundays. All we need to do is subtract the latter number from the former to get the number of working days. We can work that into your initial query (by the way, I suggest using TRUNC() instead of TO_CHAR() since your users might want a date range that spans more than one calendar year):
SELECT TRUNC(s.Sales_Date, 'MONTH') AS Sales_Month
, EXTRACT( DAY FROM LAST_DAY( TRUNC(s.Sales_Date, 'MONTH') ) ) - CEIL( ( LAST_DAY(TRUNC(s.Sales_Date, 'MONTH')) - NEXT_DAY(TRUNC(s.Sales_Date, 'MONTH')-1, 'SUN') + 1 ) / 7 ) AS working_day_cnt
, s.Sales_Category, SUM(s.Sales_Value) AS Sales_Val_Monthly
, t.Target_Value -- Target_Month and Target_Category are superfluous
FROM Sales_Data s INNER JOIN Target_Data t
ON TO_CHAR(s.Sales_Date, 'MM') = t.Target_Month
AND TO_CHAR(s.Sales_Date, 'YYYY') = t.Target_Year
AND s.Sales_Category = t.Target_Category
GROUP BY TRUNC(s.Sales_Date, 'MONTH'), Sales_Category, Target_Value;
Now given a start date and an end date, we can generate the number of working days for all the months in between those dates as follows:
SELECT TRUNC(range_dt, 'MONTH'), COUNT(*) FROM (
SELECT start_dt + LEVEL - 1 AS range_dt
FROM dual
CONNECT BY start_dt + LEVEL - 1 < end_dt
) WHERE TO_CHAR(range_dt, 'DY') != 'SUN'
GROUP BY TRUNC(range_dt, 'MONTH');
where start_dt and end_dt are parameters supplied by the user. Putting this all together, we'll have something like the following:
WITH rd ( range_month, range_day_cnt ) AS (
SELECT TRUNC(range_dt, 'MONTH'), COUNT(*) FROM (
SELECT start_dt + LEVEL - 1 AS range_dt
FROM dual
CONNECT BY start_dt + LEVEL - 1 < end_dt
) WHERE TO_CHAR(range_dt, 'DY') != 'SUN'
GROUP BY TRUNC(range_dt, 'MONTH')
)
SELECT range_month, Sales_Category, Sales_Val_Monthly
, range_day_cnt, working_day_cnt, Target_Value
, Target_Value*range_day_cnt/working_day_cnt AS prorated_target_value
FROM (
SELECT r.range_month, r.range_day_cnt
, EXTRACT( DAY FROM LAST_DAY( TRUNC(s.Sales_Date, 'MONTH') ) ) - CEIL( ( LAST_DAY(TRUNC(s.Sales_Date, 'MONTH')) - NEXT_DAY(TRUNC(s.Sales_Date, 'MONTH')-1, 'SUN') + 1 ) / 7 ) AS working_day_cnt
, s.Sales_Category, SUM(s.Sales_Value) AS Sales_Val_Monthly
, t.Target_Value -- Target_Month and Target_Category are superfluous
FROM rd INNER JOIN Sales_Data s
ON rd.range_month = TRUNC(s.Sales_Date, 'MONTH')
INNER JOIN Target_Data t
ON TO_CHAR(s.Sales_Date, 'MM') = t.Target_Month
AND TO_CHAR(s.Sales_Date, 'YYYY') = t.Target_Year
AND s.Sales_Category = t.Target_Category
WHERE s.Sales_Date >= TRUNC(start_dt)
AND s.Sales_Date < TRUNC(end_dt+1)
GROUP BY r.range_month, r.range_day_cnt, s.Sales_Category, t.Target_Value
) ORDER BY range_month;
If you have a table of public holidays, then those will have to be factored in somewhere as well - both in the rd common table expression and from the calculation of working days. If the above doesn't give you a start on that then I can take a look again in a bit and see how the other holidays might be worked in.
You can calculate the number of working days between two dates using below query. I added a nonworking date via a table named: holiday_dates and created a series of dates from 12/01/2018 to 15/01. I remove those dates that are either Sunday or holiday. Please let me know if it works for you. Thanks.
create table holiday_dates(holiday_dte date, holiday_desc varchar(100));
insert into holiday_dates values(TO_DATE('13/01/2018','DD-MM-YYYY'), 'Not a Working Day');
With tmp as (
select count(*) as num_of_working_days
from ( select rownum as rn
from all_objects
where rownum <= to_date('15/01/2018','DD-MM-YYYY') - to_date('12/01/2018','DD-MM-YYYY')+1 )
where to_char( to_date('12/01/2018','DD-MM-YYYY')+rn-1, 'DY' ) not in ( 'SUN' )
and not exists ( select null from holiday_dates where holiday_dte = trunc(to_date('12/01/2018','DD-MM-YYYY') + rn - 1)))
SELECT TO_CHAR (Sales_Date, 'MM') Sales_Month,
Sales_Category,
SUM (Sales_Value) Sales_Val_Monthly,
Target_Month,
Target_Category,
Target_Value,
tmp.num_of_working_days
FROM Sales_Data, Target_Data, tmp
WHERE Sales_Date between to_date('12/01/2018','DD-MM-YYYY') and to_date('15/01/2018','DD-MM-YYYY')
AND Sales_Category = Target_Category
GROUP BY TO_CHAR (Sales_Date, 'MM'),
Target_Month,
Target_Category,
Sales_Category,
Target_Value;

Teradata - Split date range into month columns with day count

I need to split different date ranges over a quarter period into month columns with only the days actually used in that month. Each record (range) would be different.
Example:
Table
Record_ID Start_Date End_Date
1 10/27 11/30
2 11/30 12/14
3 12/14 12/31
Range 1 = 10/5 to 12/14
Range 2 = 11/20 to 12/31
Range 3 = 10/28 to 12/2
Output:
Range 1
Oct Nov Dec
27 30 14
Similar to #ULick's answer using sys_calendar.calendar, but a little more succinct:
CREATE VOLATILE MULTISET TABLE datetest (record_id int, start_date date, end_date date) ON COMMIT PRESERVE ROWS;
INSERT INTO datetest VALUES (1, '2017-10-05', '2017-12-14');
INSERT INTO datetest VALUES (2, '2017-11-20','2017-12-31');
SELECT record_id,
SUM(CASE WHEN month_of_year = 10 THEN 1 ELSE 0 END) as October,
SUM(CASE WHEN month_of_year = 11 THEN 1 ELSE 0 END) as November,
SUM(CASE WHEN month_of_year = 12 THEN 1 ELSE 0 END) as December
FROM datetest
INNER JOIN sys_calendar.calendar cal
ON cal.calendar_date BETWEEN start_date and end_date
GROUP BY record_id;
DROP TABLE datetest;
Because Quarter was mentioned in the question (I'm not sure how it relates here) there is also quarter_of_year and month_of_quarter available in the sys_calendar to slice and dice this even further.
Also, if you are on 16.00+ There is PIVOT functionality which may help get rid of the CASE statements here.
First join with the calendar to get all the dates within the range and get the number of days per each month (incl. full month, not mentioned in Start_Date and End_Date).
Then sum up each month in a column per Range.
create table SplitDateRange ( Range bigint, Start_Date date, End_Date date );
insert into SplitDateRange values ( 1, '2018-10-05', '2018-12-14' );
insert into SplitDateRange values ( 2, '2018-11-20', '2018-12-31' );
insert into SplitDateRange values ( 3, '2018-10-28', '2018-12-02' );
select
Range
, sum(case when mon = 10 then days else 0 end) as "Oct"
, sum(case when mon = 11 then days else 0 end) as "Nov"
, sum(case when mon = 12 then days else 0 end) as "Dec"
from (
select
Range
, extract(MONTH from C.calendar_date) as mon
, max(C.calendar_date) - min(calendar_date) +1 as days
from Sys_Calendar.CALENDAR as C
inner join SplitDateRange as DR
on C.calendar_date between DR.Start_Date and DR.End_Date
group by 1,2
) A
group by Range
order by Range
;
Different approach, avoids the cross join to the calendar by applying Teradata Expand On feature for creating time series. More text, but should be more efficient for larger tables/ranges:
SELECT record_id,
Sum(CASE WHEN mth = 10 THEN days_in_month ELSE 0 END) AS October,
Sum(CASE WHEN mth = 11 THEN days_in_month ELSE 0 END) AS November,
Sum(CASE WHEN mth = 12 THEN days_in_month ELSE 0 END) AS December
FROM
( -- this Derived Table simply avoids repeating then EXTRACT/INTERVAL calculations (can't be done directly in the nested Select)
SELECT record_id,
Extract(MONTH From Begin(expanded_pd)) AS mth,
Cast((INTERVAL( base_pd P_INTERSECT expanded_pd) DAY) AS INT) AS days_in_month
FROM
(
SELECT record_id,
PERIOD(start_date, end_date+1) AS base_pd,
expanded_pd
FROM datetest
-- creates one row per month
EXPAND ON base_pd AS expanded_pd BY ANCHOR PERIOD Month_Begin
) AS dt
) AS dt
GROUP BY 1