SQL query to compare budget to actual expenses - sql

I'm struggling to build a query comparing budget to actual expense items.
The budget table has a single record per month/category on the first day of the month whereas the expense table has many records throughout the days of the month.
My desired result:
YEAR
MONTH
category
budgetAmt
sumExpenseAmt
2021
1
daily
100
49
2021
1
monthly
42
87
2021
2
daily
101
36
2021
2
monthly
55
82
What I'm getting:
YEAR
MONTH
category
budgetAmt
sumExpenseAmt
2021
1
daily
100
85
2021
1
monthly
42
169
2021
2
daily
101
85
2021
2
monthly
55
169
The amounts in "sumExpenseAmt" are wrong AND they're repeating.
(85 is the sum of all expense-daily items (jan + feb): 40 + 9 + 32 + 4)
(169 is the sum of all expense-monthly items (jan + feb): 83 + 4 + 75 +7)
MY SQL:
SELECT YEAR( "b"."date" ) AS "Year"
, MONTH( "b"."date" ) AS "Month"
, "b"."category"
, "b"."budgetAmt"
, SUM( "e"."expenseAmt" ) AS "sumExpenseAmt"
FROM "budget" AS "b"
JOIN "expense" AS "e" ON "b"."category" = "e"."category"
GROUP BY YEAR( "b"."date" ), MONTH( "b"."date" ), "b"."category", "b"."budgetAmt"
table: budget
date
category
budgetAmt
2021-01-01
daily
100
2021-01-01
monthly
42
2021-02-01
daily
101
2021-02-01
monthly
55
table: expense
date
category
expenseAmt
2021-01-04
daily
40
2021-01-07
daily
9
2021-01-08
monthly
83
2021-01-25
monthly
4
2021-02-01
daily
32
2021-02-05
daily
4
2021-02-15
monthly
75
2021-02-20
monthly
7
I've tried aggregating the expense table with a query and feeding the result into my initial SQL query, but that gives me the same result.
query: qry_summary_expense
date
category
budgetAmt
2021-01-01
daily
49
2021-01-01
monthly
87
2021-02-01
daily
36
2021-02-01
monthly
82
SELECT YEAR( "b"."date" ) AS "Year"
, MONTH( "b"."date" ) AS "Month"
, "b"."category", "b"."budgetAmt"
, SUM( "e"."expenseAmt" ) AS "sumExpenseAmt"
FROM "budget" AS "b"
JOIN "qry_summary_expense" AS "e" ON "b"."category" = "e"."category"
GROUP BY YEAR( "b"."date" ), MONTH( "b"."date" ), "b"."category", "b"."budgetAmt"

I'd join on both month and category
select year(b.date) as year,
month(b.date) as month,
b.category,
avg(budgetAmt) as budgetAmt,
sum(expenseAmt) as expenseAmt
from expense e
join budget b
on (month(b.date) = month(e.date)
and b.category = e.category)
group by year(b.date), month(b.date), b.category

You need to join the two tables on category AND month (using eomonth (end of month) does the trick).
SELECT Year(Eomonth(e.date)) AS year,
Month(Eomonth(e.date)) AS month,
e.category,
Avg(budgetamt) AS budgetAmt,
Sum(expenseamt) AS sumExpenseAmt
FROM expense e
INNER JOIN budget b
ON Eomonth(e.date) = Eomonth(b.date)
AND e.category = b.category
GROUP BY e.category,
Eomonth(e.date);
Fiddle

Alternatively you can use left or right joins based on your need.
select b.Year,b.Month,b.category,Budget, Expenses
from(
Select year(date) [Year] ,month(date) [Month] ,category,sum(budgetAmt) Budget
from budget
group by year(date),month(date),category
) b
Join
(
Select year(date) [Year] ,month(date) [Month] ,category,sum(expenseAmt) Expenses
from expense
group by year(date),month(date),category
) e
on b.Month = e.Month and b.Year = e.Year and b.category = e.category
Another Approach Using EOMONTH Function:
select Year(a.dates) Year ,month(a.dates) Month,a.category,sum(Budget) Budget, sum(Expenses) Expenses
from(
Select EOMONTH(date) dates,category,sum(budgetAmt) Budget
from #budget
group by EOMONTH(date),category
) a
Join
(
Select EOMONTH(date) dates,category,sum(expenseAmt) Expenses
from #expense
group by EOMONTH(date),category
) b
on a.dates = b.dates and a.category = b.category
group by Year(a.dates),month(a.dates),a.category

Related

BigQuery - Year over Year Comparison with Month to Date

I am having trouble accurately doing a year over year comparison by month but at any point during the month. For example for August 2022 vs 2021, I want to compare August 1 to today, rather than full month of August 2021.
My data has a date field.
I want the final result to basically be:
Product_ID, Year, Month, PY_Sales, CY_Sales
I have daily totals. Some products do have not sales on certain days though. Here's an example:
product_id
sale_date
units
1
2021-01-01
5
2
2021-01-02
4
...
...
...
1
2021-06-05
2
2
2021-08-01
1
2
2021-08-31
6
2
2022-01-06
1
2
2022-08-15
9
The final result for August should be:
product_id
Year
Month
PY_Sales
CY_Sales
2
2022
8
1
9
Right now my code will show 7 for August for product_id = 2 because 6 sales happened on August 31st but that day hasn't happened yet in 2022.
This is the code I have, but it doesn't do MTD. Right now, PY_Sales for August 2022 is showing the entire August of 2021, but I want it to show the MTD of August 2021. I used this code because some products do not have sales on certain months.
WITH cte AS
(
SELECT
PRODUCT_ID,
EXTRACT(YEAR FROM SALE_DATE) AS Year,
EXTRACT(MONTH FROM SALE_DATE) AS Month,
CONCAT(EXTRACT(YEAR FROM SALE_DATE), '-',EXTRACT(MONTH FROM SALE_DATE)) AS Year_Month,
SUM(Units) AS Units
FROM data
WHERE Product_ID = 1
AND DATE(SALE_DATE) >= '2019-01-01'
GROUP BY 1, 2, 3
),
diff AS
(
SELECT
COALESCE(c.PRODUCT_ID, p.PRODUCT_ID) AS Product_ID,
COALESCE(c.Year, p.Year + 1) AS Year,
COALESCE(c.Month, p.Month) AS Month,
IFNULL(c.Units, 0) AS Current_Units,
IFNULL(p.Units, 0) AS Previous_Units,
NULLIF(((IFNULL(c.Units, 0) - IFNULL(p.Units,0)) / p.Units),0) * 100 AS Percent_Change
FROM CTE c
FULL OUTER JOIN CTE p ON c.PRODUCT_ID = p.PRODUCT_ID AND c.Year = p.Year + 1 AND c.Month = p.Month
WHERE c.Year <= EXTRACT(YEAR FROM CURRENT_DATE())
ORDER BY 2, c.Year, c.Month
)
SELECT *
FROM diff
--This is to avoid dividing by 0
WHERE diff.Previous_Units > 0
--AND Percent_Change <= -.5
I'm being a little repetitive but I hope this is clear! Thank you so much!
In the cte table you summarize the sold units by month and year.
Your question can be solved by adding here a column units_last_year. This contains the units, which are sold up to the day one year ago. Today is the 27th of August 2022, therefore the units on the 31th of August 2021 will be set to zero.
SUM(Units) AS Units,
SUM(IF(SALE_DATE< date_sub(current_Date(),interval 1 year), Units, 0 )) as units_last_year
Please use the safe_divide command, if there is any chance of diving by zero
Here is the full query with example data.
You given an example of fixed dates, which are compared to the current date. Therefore, the query would not show the desired effect after 30th of August 2022.
The product_id three is made up values related to the current date, thus the following query yields results after August 2022.
with data as (
select *,date(sale_date_) as sale_date
from (
Select 1 product_id, "2021-01-01" sale_date_, 5 units
union all select 2,"2021-01-02", 4
union all select 1,"2021-06-05", 2
union all select 2,"2021-08-01", 1
union all select 2,"2021-08-31", 6
union all select 2,"2022-01-06", 1
union all select 2,"2022-08-15", 9
union all select 3, current_date(), 10
union all select 3, date_sub(current_date(),interval 1 year), 9
union all select 3, date_sub( date_trunc(current_date(),month),interval 1 year), 1
)
),
cte AS
(
SELECT
PRODUCT_ID,
EXTRACT(YEAR FROM SALE_DATE) AS Year,
EXTRACT(MONTH FROM SALE_DATE) AS Month,
CONCAT(EXTRACT(YEAR FROM SALE_DATE), '-',EXTRACT(MONTH FROM SALE_DATE)) AS Year_Month,
SUM(Units) AS Units,
sum(if(SALE_DATE< date_sub(current_Date(),interval 1 year), units, 0 )) as units_last_year
FROM data
WHERE # Product_ID = 1 AND
DATE(SALE_DATE) >= '2019-01-01'
GROUP BY 1, 2, 3, 4
),
diff AS
(
SELECT
COALESCE(c.PRODUCT_ID, p.PRODUCT_ID) AS Product_ID,
COALESCE(c.Year, p.Year + 1) AS Year,
COALESCE(c.Month, p.Month) AS Month,
IFNULL(c.Units, 0) AS Current_Units,
IFNULL(p.Units, 0) AS Previous_Units,
IFNULL(p.Units_last_Year, 0) AS Previous_Units_ok,
NULLIF(((IFNULL(c.Units, 0) - IFNULL(p.Units,0)) / p.Units),0) * 100 AS Percent_Change,
NULLIF(safe_divide((IFNULL(c.Units, 0) - IFNULL(p.Units_last_Year,0)) , p.Units_last_Year),0) * 100 AS Percent_Change_ok,
FROM CTE c
FULL OUTER JOIN CTE p ON c.PRODUCT_ID = p.PRODUCT_ID AND c.Year = p.Year + 1 AND c.Month = p.Month
WHERE c.Year <= EXTRACT(YEAR FROM CURRENT_DATE())
ORDER BY 2, c.Year, c.Month
)
SELECT *
FROM diff

Select each distinct of table 1 for every value of table 2

I'm essentially working with three tables; first: a Month/Year calendar, second: Customer data (that does have a parent/child relationship with itself), and third: sales data. I would like to be able to show sales for each customer for each month in a date range, regardless of months where there may have been no sales for one or more customer.
I can get queries to show every month/year in my range, and calculate sales totals for months with sales. However, since the account numbers are joining to the calendar through a table that doesn't have values for every month, I can't figure out how to list the accounts with null sales.
The closest I've gotten so far:
with cumulative as (
select MONTH(s.docdate) [Month]
, YEAR(s.docdate) [Year]
, s.account [Account]
, sum(s.amount) [sales]
from sales s
group by MONTH(s.docdate), YEAR(s.docdate), s.account
)
select c.monthno [Month]
, c.year [Year]
, (select account from customers where account=s.account) [Account]
, s.sales
from cumulative s
right join calendar c
on datefromparts(s.year, s.month, 1) = datefromparts(c.year,c.monthno,1)
order by c.year, c.monthno
resulting with;
Month Year Account sales
1 2020 1 25
1 2020 2 90
2 2020 null null
3 2020 3 45
3 2020 4 65
4 2020 null null
5 2020 1 120
5 2020 2 45
6 2020 null null
7 2020 null null
etc.
example setup here: https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=b8ae260f2901693bf4cca75fb2451649
If I try to use a left or right join to bring in the customer table, collapses results to only months and accounts with sales values.
with cumulative as (
select MONTH(s.docdate) [Month]
, YEAR(s.docdate) [Year]
, s.account [Account]
, sum(s.amount) [sales]
from sales s
group by MONTH(s.docdate), YEAR(s.docdate), s.account
)
select c.monthno [Month]
, c.year [Year]
, c2.account [Account]
, s.sales
from cumulative s
right join calendar c
on datefromparts(s.year, s.month, 1) = datefromparts(c.year,c.monthno,1)
right join customers c2
on s.account=c2.account
order by c.year, c.monthno
gives:
Month Year Account sales
1 2020 1 25
1 2020 2 90
3 2020 3 45
3 2020 4 65
5 2020 2 45
5 2020 1 120
1 2021 2 75
Output I'd like to see:
Month Year Account sales
1 2020 1 25
1 2020 2 90
1 2020 3 null
1 2020 4 null
2 2020 1 null
2 2020 2 null
2 2020 3 null
2 2020 4 null
How can I get every account number from customer to show up for each month in calendar?
If you combine Calendar and Customers with a cross join, you get a complete set of accounts and dates. Make that a CTE, and then use your Cumulative CTE to gather those results. Now you can select from the Calendar/Customers table with a left join to the sales data:
with BaseTable
as (
select c.MonthNo as Month
, c.Year
, cust.Account
from [CALENDAR] c
cross join Customers cust
), cumulative as (
select MONTH(s.docdate) [Month]
, YEAR(s.docdate) [Year]
, s.account [Account]
, sum(s.amount) [sales]
from sales s
group by MONTH(s.docdate), YEAR(s.docdate), s.account
)
select bt.Month
, bt.Year
, bt.Account
, c.sales
from BaseTable bt
left join cumulative c
on c.month = bt.Month
and c.Year = bt.Year
and c.Account = bt.Account

check whether values lies between or not in SQL

These are my 2 tables
table 2:
date_val
yr_num
yr_wk_num
day_wk_num
yr_wk_nm
day
mo_num
20200808
2020
32
6
202032
Saturday
08
20200809
2020
32
7
202032
Sunday
08
20200810
2020
33
1
202033
Monday
08
20200811
2020
33
2
202033
Tuesday
08
20200812
2020
33
3
202033
Wednesday
08
table1:
sku_id
dateval
sales
ab124
20210603
10
ab124
20210502
20
ab123
20210606
30
Need to check sales is with in + or - 30% of 2 month avg sales
with CTE
as
(
select * from table1 where dateval >= dateadd(mm, -2, dateval)
)
select dateval, sum(sales) as [Total Sales], avg(sales) as [Average Sales] from CTE group by dateval order by 1
I tried below also...
with CTE
as
(
select * from table1 t1 left join table2 t2 on t1.dateval = t2.date_val where t2.date_val >= dateadd(mm, -2, t1.dateval)
)
select dateval,sum(sales) as [Total Sales], avg(sales) as [Average Sales] from CTE group by dateval order by 1
here am doing filtering within table1 but i need to use table 2 to get filtered for past two months and get avg sales.
Next, i need to do +30% to that result avg and -30% result avg and check whether my sales is withn avg sales( avg30% above or below) or not if yes '1' if not '0'
For Ex:
Historic 2 month avg sales 100.
(+30% of avg sales is 130)
(-30% of avg sales is 70)
if sales is 120. i need to check 120 lies between 70 to 130.
please suggest me how to achieve

Query two unbalanced tables

Sum across two tables returns unwanted Sum from one table multiplied by the number of rows in the other
I have 1 table with Actual results recorded by date and the other tables contains planned results recorded by month.
Table 1(Actual)
Date Location Amount
01/01/2019 Loc1 1000
01/02/2019 Loc1 700
01/01/2019 Loc2 7500
01/02/2019 Loc2 1000
02/01/2019 Loc1 500
Table 2(Plan)
Year Month Location Amount
2019 1 Loc1 1500
2019 1 Loc2 8000
2019 2 Loc1 800
I have tried various differed Joins using YEAR(Table1.date) and Month(table1.date) and grouping by
Month(Table1.Date) but I keep running into the same problem where the PlanAmount is multiplied by however many rows in the Actual table...
in the example of loc1 for Month 1 below I get
Year Month Location PlanAmount ActualAmount
2019 1 Loc1 3000 1700
I would like to return the below
Year Month Location PlanAmount ActualAmount
2019 1 Loc1 1500 1700
2019 1 Loc2 8000 8500
2019 2 Loc1 800 500
Thanks in advance for any help
D
You can do this with a full join or union all/group by:
select yyyy, mm, location,
sum(actual_amount) as actual_amount,
sum(plan_amount) as plan_amount
from ((select year(date) as yyyy, month(date) as mm, location,
amount as actual_amount, 0 as plan_amount
from actual
group by year(date) as yyyy, month(date) as mm, location
) union all
(select year, month, location,
0 as actual_amount, amount as plan_amount
from actual
group by year, month, location
)
) ap
group by yyyy, mm, location;
This ensures that you have rows, even when there are no matches in the other table.
To get the required results you need to group the first table on year of date, month of date and location and need to select the columns year, month, location and sum of amount from group after that you need to join that resultant r
SELECT
plans.year,
plans.month,
plans.location,
plans.plan_amount,
grouped_results.actual_amount
FROM plans
INNER JOIN (
SELECT
datepart(year, date) AS year,
datepart(month, date) AS month,
location,
SUM(amount) AS actual_amount
FROM actuals
GROUP BY datepart(year, date), datepart(month, date), location
) as grouped_results
ON
grouped_results.year = plans.year AND
grouped_results.month = plans.month AND
grouped_results.location = plans.location
I think the problem is that you are using sum(PlanTable.Amount) when grouping. Try using max(PlanTable.Amount) instead.
select
p.Year,
p.Month,
p.Location,
sum(a.Amount) as actual_amount,
max(p.Amount) as plan_amount
from
[Plan] p left join Actual a
on year(a.date) = p.year
and month(a.date) = p.Month
and a.Location = p.Location
group by
p.year,
p.month,
p.Location
SQL Fiddle
get year and month from date and use them in join , most dbms has year and month functions you can use according to your DBMS
select year(t1.date) yr,month(t1.date) as monthofyr ,t1.Location,
sum(t1.amount) as actual_amoun,
sum(t2.amount) as planamount
from table1 t1 left join table2 t2 on
month(t1.date)= t2.Month and t1.Location=t2.Location
and year(t1.date)=t2.year
group by year(t1.date) ,month(t1.date),Location

SQL Sum by week from daily table

I have a table with sales for products.
The sales are per day. like
product date sales
1 '2013-11-01' 100
1 '2013-11-02' 423
1 '2013-11-03' 700
1 '2013-11-04' 233
2 '2013-11-01' 623
2 '2013-11-02' 451
2 '2013-11-03' 9000
I want to get a query which will show me the week over week sum of sales
So something like:
product week ending sales
1 '2013-11-01' 10000
1 '2013-11-08' 15000
2 '2013-11-01' 4900
2 '2013-11-08' 30000
I'm not sure how I get this weekly groups when summing up.
I'm using teradata
If you are using Teradata 14 you can leverage the DayNumber_Of_Week() function in the database TD_SYSFNLIB:
SELECT s.Product
, s.Date + (7-DayNumber_Of_Week(s.date)) AS WeekEndingDate /* Saturday */
, SUM(s.Sales) AS Sales
FROM sales AS S
GROUP BY 1,2;
This should work in Teradata 13.10 as well.
Using Sys_Calendar:
SELECT s.Product
, s.DATE + (7-c.Day_Of_Week) AS WeekEndingDate /* Saturday */
, SUM(s.Sales) AS Sales
FROM sales AS S
INNER JOIN
Sys_Calendar.Calendar c
ON S.date = c.calendar_date
GROUP BY 1,2;
I know very little about TERADATA, but I believe you can leverage the sys_calendar.calendar table, something like:
SELECT s.Product, c.week_of_year, SUM(s.sales) AS Sales
FROM sales AS s
JOIN sys_calendar.calendar as C
ON s.date = c.date
You'd need the Year in there as well, so as to not group up week 1 of 2013 with week 1 of 2012.