SQL Calculations for budgeting - sql

I have a database that contains the following columns:
Vendor, Amount, StartDate, Months
I would like to be able to calculate the average monthly amount based on the Months that are entered. I would also like to see it calculate out from the start date to the end date based on the StartDate + Months calculation. The resulting table would look something like this:
Vendor1 has 2 months of 1112 starting Jan 1 while Vendor2 has 3 months of 2040 staring Feb 1
| | ANNUAL | JAN | FEB | MAR | APR |
Vendor1 | 2,224 | 1,112 | 1,112 | | |
Vendor2 | 6,120 | | 2,040 | 2,040 | 2,040 |
Any assistance or direction would be greatly appreciated.

That's a strange DB design. However, here's what you've got to try:
SELECT (Amount * Months) AS Annual, (Case #(StartDate < DATE("01.02.year")) WHEN 1 THEN Amount ELSE NULL) AS Jan FROM Table --etc for all months
Will think of modifications though, because this way is a little too straightforward.

You would use conditional aggregation. Assuming the start dates are all in the same year, the code might look like this:
select vendorid, (amount * months) as total,
(case when month(startdate) <= 1 and month(startdate) + months >= 1
then amount
end) as jan,
(case when month(startdate) <= 2 and month(startdate) + months >= 2
then amount
end) as feb,
(case when month(startdate) <= 3 and month(startdate) + months >= 3
then amount
end) as mar,
(case when month(startdate) <= 4 and month(startdate) + months >= 4
then amount
end) as apr,
from t;

Related

SQL query to get running total for records matching condition over last 12 months

I am trying to query a table and get a running total for each of the last 12 months. A record could fall in more than one month if the range of two date fields falls on multiple months. The fields are DueDate and DeferralDate.
So for example, lets say I have the following 4 records:
Id | Date1 | Date2
1 01/20/2020 05/29/2020
2 02/01/2020 08/14/2020
3 04/01/2020 04/30/2020
4 07/08/2020 12/31/2020
My result would look like this:
Nov 19 | Dec 19 | Jan 20 | Feb 20 | Mar 20 | Apr 20 | May 20 | Jun 20 | Jul 20 | Aug 20 | Sept 20 | Oct 20
0 0 1 2 2 3 2 1 2 2 1 1
I have no idea how to go about this other than 12 separate queries but there's probably a better way to do it I'm unaware of. Hopefully someone can point me in the right direction.
Thanks in advance.
If you want this in columns, then it is conditional aggregation. Assuming you want any overlap in the month:
select sum(case when date1 < '2019-12-01' and date2 >= '20190-11-01' then 1 else 0 end) as cnt_201911,
sum(case when date1 < '2020-01-01' and date2 >= '20190-12-01' then 1 else 0 end) as cnt_201912,
sum(case when date1 < '2020-02-01' and date2 >= '2020-01-01' then 1 else 0 end) as cnt_202001,
sum(case when date1 < '2020-03-01' and date2 >= '2020-02-01' then 1 else 0 end) as cnt_202002,
. . .
from t
Select sum(count(date1) , count(date2)) ,
Format(date1,'MMMyy')
from tablename
Where month (date1) = month (date2)
Then you have to use Pivot to horizontalize the select result

DB2/SQL aggregates with preceeding weekdays

I have a query that currently gets daily records against a weekly number from a prepopulated table:
SELECT Employee,
sum(case when category = 'Shirts' then daily_total else 0 end) as Shirts_DAILY,
sum(case when category = 'Shirts' then weekly_quota else 0 end) as Shirts_QUOTA, -- this is a static column, this number is the same for every record
sum(case when category = 'Shoes' then daily_total else 0 end) as Shoes_DAILY,
sum(case when category = 'Shoes' then weekly_quota else 0 end) as Shoes_QUOTA, -- this is a static column, this number is the same for every record
CURRENT_DATE as DATE_OF_REPORT
from SalesNumbers
where date_of_report >= current_date
group by Employee;
This runs in a script nightly and returns records like this:
Employee | shirts_DAILY | shirts_QUOTA | Shoes_DAILY | Shoes_QUOTA | DATE_OF_REPORT
--------------------------------------------------------------------------------------------------------
123 15 75 14 85 2019-08-30
That's the record from last Friday Night's report. I'm trying to figure out a way to add a column for each category that would take the sum of daily totals (shirts_DAILY, shoes_DAILY) for each category on preceding weekdays (running sunday through saturday as a week) and divide by that category's quota (shirts_QUOTA, shoes_QUOTA).
For example, here are records from sunday through thursday
Employee | shirts_DAILY | shirts_QUOTA | Shoes_DAILY | Shoes_QUOTA | DATE_OF_REPORT
--------------------------------------------------------------------------------------------------------
123 15 75 16 85 2019-08-25
123 4 75 2 85 2019-08-26
123 8 75 6 85 2019-08-27
123 2 75 8 85 2019-08-28
123 15 75 14 85 2019-08-29
With my new change, I would want Friday night's record to take the sum of sunday through thursday's daily records and divide by the quota (including friday's daily in the sum)
Friday night's record with new column:
Employee | shirts_DAILY | shirts_QUOTA | shirtsPercent | Shoes_DAILY | Shoes_QUOTA | shoesPercent | DATE_OF_REPORT
-----------------------------------------------------------------------------------------------------------------------------------------------
123 2 75 61.3 7 85 62.4 2019-08-30
So friday's run added 15,4,8,2,15,2 for the shirts for 46/75 and 7,14,8,6,2,16 for shoes for 53/85. So the daily sum of each for the preceding week, including present day daily totals, if that makes sense.
What is the best way for me to achieve this?
SELECT Employee,
sum(case when category = 'Shirts' and date_of_report >= current date then
daily_total else 0 end) as Shirts_DAILY,
sum(case when category = 'Shirts' and date_of_report >= current date then
weekly_quota else 0 end) as Shirts_QUOTA,
( sum(case when category = 'Shirts' then
daily_total else 0 end) * 100 ) /
( sum(case when category = 'Shirts' and date_of_report >= current date then
weekly_quota else 0 end) ) as Shirts_PERCENT,
CURRENT_DATE as DATE_OF_REPORT
from SalesNumbers
where date_of_report >= ( current date - ( dayofweek(current date) - 1 ) days )
group by Employee

SQL Select Where date in (Jan and March) but not in Feb

I have a table like this in SQL called Balance
+----+-----------+-------+------+
| id | accountId | Date | Type |
+----+-----------+-------+------+
| PK | FK | Date | Int |
+----+-----------+-------+------+
I need to find the accountIds that has balance entries in January and March, but not in Febuary.
Only in 2018 and Type should be 2.
How would I go about writing my sql select statement?
Thanks
Edit:
What's I've done so far:
Selecting rows that either in Jan OR March is not a problem for me.
SELECT AccountId, Date FROM Balance
WHERE Month(Date) in (1,3) AND YEAR(Date) = 2018 AND Type =2
ORDER BY AccountId, Date
But if an AccountId has a single entry, say in January, then this will be included. And that's not what I want.
Only if an Account has entries in both Jan and March, and not in Feb is it interesting.
I suspect Group BY and HAVING are keys here, but I'm unsure how to proceed
I would do this using aggregation:
select b.accountid
from balance b
where date >= '2018-01-01' and date < '2019-01-01'
group by b.accountid
having sum(case when month(date) = 1 then 1 else 0 end) > 0 and -- has january
sum(case when month(date) = 3 then 1 else 0 end) > 0 and -- has march
sum(case when month(date) = 2 then 1 else 0 end) = 0 -- does not have february

GROUP BY next months over N years

I need to aggregate amounts grouped by "horizon" 12 next months over 5 year:
assuming we are 2015-08-15
SUM amount from 0 to 12 next months (from 2015-08-16 to 2016-08-15)
SUM amount from 12 to 24 next months (from 2016-08-16 to 2017-08-15)
SUM amount from 24 to 36 next months ...
SUM amount from 36 to 48 next months
SUM amount from 48 to 60 next months
Here is a fiddled dataset example:
+----+------------+--------+
| id | date | amount |
+----+------------+--------+
| 1 | 2015-09-01 | 10 |
| 2 | 2015-10-01 | 10 |
| 3 | 2016-10-01 | 10 |
| 4 | 2017-06-01 | 10 |
| 5 | 2018-06-01 | 10 |
| 6 | 2019-05-01 | 10 |
| 7 | 2019-04-01 | 10 |
| 8 | 2020-04-01 | 10 |
+----+------------+--------+
Here is the expected result:
+---------+--------+
| horizon | amount |
+---------+--------+
| 1 | 20 |
| 2 | 20 |
| 3 | 10 |
| 4 | 20 |
| 5 | 10 |
+---------+--------+
How can I get these 12 next months grouped "horizons" ?
I tagged PostgreSQL but I'm actually using an ORM so it's just to find the idea. (by the way I don't have access to the date formatting functions)
I would split by 12 months time frame and group by this:
SELECT
FLOOR(
(EXTRACT(EPOCH FROM date) - EXTRACT(EPOCH FROM now()))
/ EXTRACT(EPOCH FROM INTERVAL '12 month')
) + 1 AS "horizon",
SUM(amount) AS "amount"
FROM dataset
GROUP BY horizon
ORDER BY horizon;
SQL Fiddle
Inspired by: Postgresql SQL GROUP BY time interval with arbitrary accuracy (down to milli seconds)
Assuming you need intervals from current date to this day next year and so on, I would query this like this:
SELECT 1 AS horizon, SUM(amount) FROM dataset
WHERE date > now()
AND date < (now() + '12 months'::INTERVAL)
UNION
SELECT 2 AS horizon, SUM(amount) FROM dataset
WHERE date > (now() + '12 months'::INTERVAL)
AND date < (now() + '24 months'::INTERVAL)
UNION
SELECT 3 AS horizon, SUM(amount) FROM dataset
WHERE date > (now() + '24 months'::INTERVAL)
AND date < (now() + '36 months'::INTERVAL)
UNION
SELECT 4 AS horizon, SUM(amount) FROM dataset
WHERE date > (now() + '36 months'::INTERVAL)
AND date < (now() + '48 months'::INTERVAL)
UNION
SELECT 5 AS horizon, SUM(amount) FROM dataset
WHERE date > (now() + '48 months'::INTERVAL)
AND date < (now() + '60 months'::INTERVAL)
ORDER BY horizon;
You can generalize it and make something like this using additional variable:
SELECT number AS horizon, SUM(amount) FROM dataset
WHERE date > (now() + ((number - 1) * '12 months'::INTERVAL))
AND date < (now() + (number * '12 months'::INTERVAL));
Where number is an integer from range [1,5]
Here is what I get from the Fiddle:
| horizon | sum |
|---------|-----|
| 1 | 20 |
| 2 | 20 |
| 3 | 10 |
| 4 | 20 |
| 5 | 10 |
Perhaps CTE?
WITH RECURSIVE grps AS
(
SELECT 1 AS Horizon, (date '2015-08-15') + interval '1' day AS FromDate, (date '2015-08-15') + interval '1' year AS ToDate
UNION ALL
SELECT Horizon + 1, ToDate + interval '1' day AS FromDate, ToDate + interval '1' year
FROM grps WHERE Horizon < 5
)
SELECT
Horizon,
(SELECT SUM(amount) FROM dataset WHERE date BETWEEN g.FromDate AND g.ToDate) AS SumOfAmount
FROM
grps g
SQL fiddle
Rather simply:
SELECT horizon, sum(amount) AS amount
FROM generate_series(1, 5) AS s(horizon)
JOIN dataset ON "date" >= current_date + (horizon - 1) * interval '1 year'
AND "date" < current_date + horizon * interval '1 year'
GROUP BY horizon
ORDER BY horizon;
You need a union and an aggregate function:
select 1 as horizon,
sum(amount) amount
from the_table
where date >= current_date
and date < current_date + interval '12' month
union all
select 2 as horizon,
sum(amount) amount
where date >= current_date + interval '12' month
and date < current_date + interval '24' month
union all
select 3 as horizon,
sum(amount) amount
where date >= current_date + interval '24' month
and date < current_date + interval '36' month
... and so on ...
But I don't know, how to do that with an obfuscation layer (aka ORM) but I'm sure it supports (or it should) aggregation and unions.
This could easily be wrapped up into a PL/PgSQL function where you pass the "horizon" and the SQL is built dynamically so that all you need to call is something like: select * from sum_horizon(5) where 5 indicates the number of years.
Btw: date is a horrible name for a column. For one because it's a reserved word, but more importantly because it doesn't document the meaning of the column. Is it a "release date"? A "due date"? An "order date"?
Try this
select
id,
sum(case when date>=current_date and date<current_date+interval 1 year then amount else 0 end) as year1,
sum(case when date>=current_date+interval 1 year and date<current_date+interval 2 year then amount else 0 end) as year2,
sum(case when date>=current_date+interval 2 year and date<current_date+interval 3 year then amount else 0 end) as year3,
sum(case when date>=current_date+interval 3 year and date<current_date+interval 4 year then amount else 0 end) as year4,
sum(case when date>=current_date+interval 4 year and date<current_date+interval 5 year then amount else 0 end) as year5
from table
group by id

SQL split number over date range

I have a table with, for example this data
ID |start_date |end_date |amount
---|------------|-----------|-------
a1 |2013-12-01 |2014-03-31 |100
Iwant to have a query that split the dates so I have the amount splitted out over the year like this :
ID |org_start_date |org_end_date |new_start_date |new_end_date |amount
---|----------------|---------------|----------------|----------------|-------
a1 |2013-12-01 |2014-03-31 |2013-12-01 |2013-12-31 |25
a1 |2013-12-01 |2014-03-31 |2014-01-01 |2014-03-31 |75
The 25 in 2013 is because 2013 has one month and 75 in 2014 because this has 3 months
Is there a way to do this in T-SQL?
Thx in advance!
Use spt_values table to create a calendar table, then join to your table to split date range into any part you want.
If split by year and divide amount by months you could:
with dates as
(
select number,DATEADD(day,number,'20130101') as dt
from master..spt_values
where number between 0 and 1000 AND TYPE='P'
)
select
m.start_date as org_start_date,
m.end_date as org_end_date,
min(d.dt) as new_start_date,
max(d.dt) as new_end_date,
m.amount*count(distinct month(d.dt))/(datediff(month,m.start_date,m.end_date)+1) as amount
from
MonthSplit m
join
dates d
on
d.dt between m.start_date and m.end_date
group by
m.start_date, m.end_date, year(d.dt),m.amount
Here is the SQL FIDDLE DEMO.
Here is a solution using a numbers table:
SQL Fiddle Example
DECLARE #STARTYR INT = (SELECT MIN(YEAR([Start Date])) FROM Table1)
DECLARE #ENDYR INT = (SELECT MAX(YEAR([End Date])) FROM Table1)
SELECT [Id]
, #STARTYR + Number AS [Year]
, CASE WHEN YEAR([Start Date]) < #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number,0)
ELSE [Start Date] END AS [Start]
, CASE WHEN YEAR([End Date]) > #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number + 1,0)
ELSE [End Date] END AS [End]
, DATEDIFF(MONTH,CASE WHEN YEAR([Start Date]) < #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number,0)
ELSE [Start Date] END
,CASE WHEN YEAR([End Date]) > #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number + 1,0)
ELSE DATEADD(MONTH,DATEDIFF(MONTH,0,DATEADD(MONTH,1,[End Date])),0) END) AS [Months]
, DATEDIFF(MONTH,[Start Date],[End Date]) + 1 [Total Months]
, ([Amount] / (DATEDIFF(MONTH,[Start Date],[End Date]) + 1))
*
DATEDIFF(MONTH,CASE WHEN YEAR([Start Date]) < #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number,0)
ELSE [Start Date] END
,CASE WHEN YEAR([End Date]) > #STARTYR + Number
THEN DATEADD(YEAR, #STARTYR - 1900 + Number + 1,0)
ELSE DATEADD(MONTH,DATEDIFF(MONTH,0,DATEADD(MONTH,1,[End Date])),0) END) AS [Proportion]
FROM Numbers
LEFT JOIN Table1 ON YEAR([Start Date]) <= #STARTYR + Number
AND YEAR([End Date]) >= #STARTYR + Number
WHERE Number <= #ENDYR - #STARTYR
I don't have a ready made SQL for you but just a thought to solve this problem. If you have some experience with SQL it won't be hard to express it in SQL.
You could do this by defining a reference table for the months that are between your begin and end date:
ID | month | year | month start | month end | count |
-----------------------------------------------------------------------------
1001 | dec-2013 | 2013 | 1-12-2013 | 31-12-2013 | 1 |
1001 | jan-2014 | 2014 | 1-1-2014 | 31-1-2014 | 1 |
1001 | feb-2014 | 2014 | 1-2-2014 | 28-2-2014 | 1 |
1001 | mar-2014 | 2014 | 1-3-2014 | 31-3-2014 | 1 |
Maybe you already have such a time ref table in your DWH.
When you join (with a between statement) your table with the record that contains the start and end date per row, with this reference table, you'll have the split from 1 row to the number of months contained in the range correct. The count column will help you to get the split ratio per year right by grouping over the year afterwards (like : 1/4 for 2013 and 3/4 for 2014). You'll have to apply the ratio to the field 'amount' that you want to split up.
It's very similar to this question:
Split date range into one row per month in sql server
Although you are doing grouping by year, so based on that answer you can modify it to do what you want by adding MIN, MAX to your date values and grouping by the YEAR():
SQL Fiddle Demo
Schema Setup:
CREATE TABLE MonthSplit
([ID] varchar(2), [start_date] datetime, [end_date] datetime, [amount] int)
;
INSERT INTO MonthSplit
([ID], [start_date], [end_date], [amount])
VALUES
('a1', '2013-12-01 00:00:00', '2014-03-31 00:00:00', 100),
('a2', '2013-10-01 00:00:00', '2015-05-01 00:00:00', 400)
;
Recursive CTE to group by Year:
WITH cte AS
(SELECT ID
, start_date
, end_date
, start_date AS from_date
, DATEADD(day, day(start_date)* -1 + 1, start_date) AS first_of_month
FROM MonthSplit
UNION ALL
SELECT ID
, start_date
, end_date
, DATEADD(month,1,first_of_month)
, DATEADD(month,1,first_of_month)
FROM cte
WHERE DATEADD(month,1,from_date) < end_date
)
SELECT ID as ID,
min(start_date) as org_start_date,
min(end_date) as org_end_date,
min(from_date) AS new_start_date,
CASE when max(end_date) < DATEADD(month,1,max(first_of_month)) THEN
max(end_date)
ELSE
DATEADD(day, -1, DATEADD(month,1,max(first_of_month)))
END AS new_end_date
FROM cte
group by year(from_date), ID
Results:
| ID | ORG_START_DATE | ORG_END_DATE | NEW_START_DATE | NEW_END_DATE |
|----|-------------------|----------------|-------------------|-------------------|
| a1 | December, 01 2013 | March, 31 2014 | December, 01 2013 | December, 31 2013 |
| a1 | December, 01 2013 | March, 31 2014 | January, 01 2014 | March, 31 2014 |
| a2 | October, 01 2013 | May, 01 2015 | October, 01 2013 | December, 31 2013 |
| a2 | October, 01 2013 | May, 01 2015 | January, 01 2014 | December, 31 2014 |
| a2 | October, 01 2013 | May, 01 2015 | January, 01 2015 | April, 30 2015 |