SQL Server aggregate over range of dates - sql

I am using SQL Server 2014. I need to aggregate totals (sum total) over a range of dates that are partitioned or grouped by customer and location. The key is to get all the adjustment amounts and sum them up as they apply to a billing transaction date.
So all adjustments after the last bill date, but less than the next bill date need to get summed up and presented nicely along with the bill amount.
See example:
+------------------+------------+------------+------------------+--------------------+
| TRANSACTION_TYPE | CUSTOMERID | LOCATIONID | TRANSACTION DATE | TRANSACTION AMOUNT |
+------------------+------------+------------+------------------+--------------------+
| bill | 215 | 102 | 7/7/2016 | $100.00 |
| bill | 215 | 102 | 6/6/2016 | $121.00 |
| adj | 215 | 102 | 6/1/2016 | $22.00 |
| adj | 215 | 102 | 5/8/2016 | $0.35 |
| adj | 215 | 102 | 5/7/2016 | $5.00 |
| bill | 215 | 102 | 5/6/2016 | $115.00 |
| bill | 215 | 102 | 4/7/2016 | $200.00 |
| adj | 215 | 102 | 4/2/2016 | $4.35 |
| adj | 215 | 102 | 4/1/2016 | $(0.50) |
| adj | 215 | 102 | 3/28/2016 | $33.00 |
| bill | 215 | 102 | 3/28/2016 | $75.00 |
| adj | 215 | 102 | 3/5/2016 | $0.33 |
| bill | 215 | 102 | 3/3/2016 | $99.00 |
+------------------+------------+------------+------------------+--------------------+
What I would like to see is the following:
+------------------+------------+------------+------------------+-------------+-------------------+
| TRANSACTION_TYPE | CUSTOMERID | LOCATIONID | TRANSACTION DATE | BILL AMOUNT | ADJUSTMENT AMOUNT |
+------------------+------------+------------+------------------+-------------+-------------------+
| bill | 215 | 102 | 7/7/2016 | $100.00 | $- |
| bill | 215 | 102 | 6/6/2016 | $121.00 | $27.35 |
| bill | 215 | 102 | 5/6/2016 | $115.00 | $- |
| bill | 215 | 102 | 4/7/2016 | $200.00 | $36.85 |
| bill | 215 | 102 | 3/28/2016 | $75.00 | $0.33 |
| bill | 215 | 102 | 3/3/2016 | $99.00 | $- |
+------------------+------------+------------+------------------+-------------+-------------------+

You need to:
first conceive the table as two (virtual) sub-tables, on the TransactionType;
then use the LEAD function to get the date range of adjustments to be applied; and
finally perform a eft join.
Untested SQL below:
with
BillData as (
select
TransactionType,
CustomerID,
LocationID,
TransactionDate,
TransactionAmount,
lead(TransactionDate, 1) over (partition by CustomerID
order by TransactionDate) as NextDate
from #data bill
where TransactionType = 'bill'
),
AdjData as (
select
CustomerID,
TransactionDate,
sum(TransactionAmount) as AdjAmount
from #data adj
where TransactionType = 'adj'
)
select
bill.TransactionType,
bill.CustomerID,
bill.LocationID,
bill.TransactionDate,
sum(TransactionAmount) as BillAmount,
sum(AdjAmount) as AdjAmount
from BillData bill
left join AdjData adj
on adj.CustomerID = bill.CustomerID
and bill.TransactionDate <= adj.TransactionDate
and adj.TransactionDate < bill.NextDate
group by
bill.TransactionType,
bill.CustomerID,
bill.LocationID,
bill.TransactionDate
;

This is what I ended up doing:
select
bill.TransactionType,
bill.CustomerID,
bill.LocationID,
bill.TransactionDate,
TransactionAmount as BillAmount,
sum(AdjAmount) as AdjAmount
from
(
select
TransactionType,
CustomerID,
LocationID,
TransactionDate,
TransactionAmount,
lag(TransactionDate, 1) over (partition by CustomerID, LocationID
order by TransactionDate) as PreviousDate --NextDate
from test1
where TransactionType = 'bill'
) as bill
left join
(
select
CustomerID,
LocationID,
TransactionDate,
TransactionAmount as AdjAmount
from test1
where TransactionType = 'adj'
) as adj
ON
adj.CustomerID = bill.CustomerID
and adj.LocationID = bill.LocationID
and adj.TransactionDate >= bill.PreviousDate
and adj.TransactionDate < bill.TransactionDate
group by
bill.TransactionType,
bill.CustomerID,
bill.LocationID,
bill.TransactionDate,
bill.TransactionAmount
order by 4 desc

Related

How to get Max date and sum of its rows SQL

I have following table,
+------+-------------+----------+---------+
| id | date | amount | amount2 |
+------+-------------+----------+---------+
| | | | 500 |
| 1 | 1/1/2020 | 1000 | |
+------+-------------+----------+---------+
| | | | 100 |
| 1 | 1/3/2020 | 1558 | |
+------+-------------+----------+---------+
| | | | 200 |
| 1 | 1/3/2020 | 126 | |
+------+-------------+----------+---------+
| | | | 500 |
| 2 | 2/5/2020 | 4921 | |
+------+-------------+----------+---------+
| | | | 100 |
| 2 | 2/5/2020 | 15 | |
+------+-------------+----------+---------+
| | | | 140 |
| 2 | 1/1/2020 | 5951 | |
+------+-------------+----------+---------+
| | | | 10 |
| 2 | 1/2/2020 | 1588 | |
+------+-------------+----------+---------+
| | | | 56 |
| 2 | 1/3/2020 | 1568 | |
+------+-------------+----------+---------+
| | | | 45 |
| 2 | 1/4/2020 | 12558 | |
+------+-------------+----------+---------+
I need to get each Id's max date and its amount and amount2 summations, how can I do this. according to above data, I need following output.
+------+-------------+----------+---------+
| | | | 300 |
| 1 | 1/3/2020 | 1684 | |
+------+-------------+----------+---------+
| | | | 600 |
| 2 | 2/5/2020 | 4936 | |
+------+-------------+----------+---------+
How can I do this.
Aggregate and use MAX OVER to get the IDs' maximum dates:
select id, [date], sum_amount, sum_amount2
from
(
select
id, [date], sum(amount) as sum_amount, sum(amount2) as sum_amount2,
max([date]) over (partition by id) as max_date_for_id
from mytable group by id, [date]
) aggregated
where [date] = max_date_for_id
order by id;
first is to use dense_rank() to find the row with latest date
dense_rank () over (partition by id order by [date] desc)
after that, just simply group by with sum() on the amount
select id, [date], sum(amount), sum(amount2)
from
(
select *,
dr = dense_rank () over (partition by id order by [date] desc)
from your_table
) t
where dr = 1
group by id, [date]

Query to reorganize dates

I need to do a transformation of a Postgres database table and I don't know where to start.
This is the table:
| Customer Code | Activity | Start Date |
|:---------------:|:--------:|:----------:|
| 100 | A | 01/05/2017 |
| 100 | A | 19/07/2017 |
| 100 | B | 18/09/2017 |
| 100 | C | 07/12/2017 |
| 101 | A | 11/02/2018 |
| 101 | B | 02/04/2018 |
| 101 | B | 14/06/2018 |
| 100 | A | 13/07/2018 |
| 100 | B | 14/08/2018 |
Customers can perform activities A, B and C, always in that order.
To carry out activity B he/she has to carry out activity A. To carry out C, he/she has to carry out activity A, then to B.
An activity or cycle can be performed more than once by the same customer.
I need to reorganize the table in this way, placing the beginning and end of each step:
| Customer Code | Activity | Start Date | End Date |
|:---------------:|:--------:|:----------:|:----------:|
| 100 | A | 01/05/2017 | 18/09/2017 |
| 100 | B | 18/09/2017 | 07/12/2017 |
| 100 | C | 07/12/2017 | 13/07/2018 |
| 101 | A | 11/02/2018 | 02/04/2018 |
| 101 | B | 02/04/2018 | |
| 100 | A | 13/07/2018 | 14/08/2018 |
| 100 | B | 14/08/2018 | |
Here is approach at this gaps-and-islands problem:
select
customer_code,
activity,
start_date,
case when (activity, lead(activity) over(partition by customer_code order by start_date))
in (('A', 'B'), ('B', 'C'), ('C', 'A'))
then lead(start_date) over(partition by customer_code order by start_date)
end end_date
from (
select
t.*,
lead(activity) over(partition by customer_code order by start_date) lead_activity
from mytable t
) t
where activity is distinct from lead_activity
The query starts by removing consecutive rows that have the same customer_code and activity. Then, we use conditional logic to bring in the start_date of the next row when the activty is in sequence.
Demo on DB Fiddle:
customer_code | activity | start_date | end_date
------------: | :------- | :--------- | :---------
100 | A | 2017-07-19 | 2017-09-18
100 | B | 2017-09-18 | 2017-12-07
100 | C | 2017-12-07 | 2018-07-13
100 | A | 2018-07-13 | 2018-08-14
100 | B | 2018-08-14 | null
101 | A | 2018-02-11 | 2018-06-14
101 | B | 2018-06-14 | null

Create rows for iteration in SQL

I have to create a report of how long a ticket is open every first of the month, and another that shows how long it took to close a ticket. What is the best way to do this with SQL without creating an interval for each month? I am using SQL Server 2008 R2
My current data:
| Ticket | Start Date | End Date |
|--------|------------|------------|
| ABC | 5/8/2018 | 9/28/2018 |
| XYZ | 6/22/2018 | 10/15/2018 |
Expected result:
| Ticket | Start Date | End Date | Report Date | Ticket Age | Ticket Interval |
|--------|------------|------------|-------------|------------|-----------------|
| ABC | 5/8/2018 | 9/28/2018 | 6/1/2018 | 24 | |
| ABC | 5/8/2018 | 9/28/2018 | 7/1/2018 | 54 | |
| ABC | 5/8/2018 | 9/28/2018 | 8/1/2018 | 85 | |
| ABC | 5/8/2018 | 9/28/2018 | 9/1/2018 | 116 | |
| ABC | 5/8/2018 | 9/28/2018 | 10/1/2018 | | 143 |
| XYZ | 6/22/2018 | 10/15/2018 | 7/1/2018 | 9 | |
| XYZ | 6/22/2018 | 10/15/2018 | 8/1/2018 | 40 | |
| XYZ | 6/22/2018 | 10/15/2018 | 9/1/2018 | 71 | |
| XYZ | 6/22/2018 | 10/15/2018 | 10/1/2018 | 101 | |
| XYZ | 6/22/2018 | 10/15/2018 | 11/1/2018 | | 115 |
You can use recursive CTEs:
with cte as (
select ticket, sdate, edate, dateadd(month, 1, dateadd(day, 1 - day(sdate), sdate)) as reportdate
from t
union all
select ticket, sdate, edate, dateadd(month, 1, reportdate)
from cte
where reportdate <= edate
)
select cte.*, datediff(day, sdate, reportdate) as ticketage,
(case when datediff(month, edate, reportdate) = 1 then datediff(day, sdate, edate) end) as interval
from cte
order by ticket, reportdate;
I included the ticket age on the last month for the ticket. You can use a similar case expression if you really don't want it.
Here is a db<>fiddle.

SQL percent of total and weighted average

I have the following postgreSql table stock, there the structure is the following:
| column | pk |
+--------+-----+
| date | yes |
| id | yes |
| type | yes |
| qty | |
| fee | |
The table looks like this:
| date | id | type | qty | cost |
+------------+-----+------+------+------+
| 2015-01-01 | 001 | CB04 | 500 | 2 |
| 2015-01-01 | 002 | CB04 | 1500 | 3 |
| 2015-01-01 | 003 | CB04 | 500 | 1 |
| 2015-01-01 | 004 | CB04 | 100 | 5 |
| 2015-01-01 | 001 | CB02 | 800 | 6 |
| 2015-01-02 | 002 | CB03 | 3100 | 1 |
I want to create a view or query, so that the result looks like this.
The table will show the t_qty, % of total Qty, and weighted fee for each day and each type:
% of total Qty = qty / t_qty
weighted fee = fee * % of total Qty
| date | id | type | qty | cost | t_qty | % of total Qty | weighted fee |
+------------+-----+------+------+------+-------+----------------+--------------+
| 2015-01-01 | 001 | CB04 | 500 | 2 | 2600 | 0.19 | 0.38 |
| 2015-01-01 | 002 | CB04 | 1500 | 3 | 2600 | 0.58 | 1.73 |
| 2015-01-01 | 003 | CB04 | 500 | 1 | 2600 | 0.19 | 0.192 |
| 2015-01-01 | 004 | CB04 | 100 | 5 | 2600 | 0.04 | 0.192 |
| | | | | | | | |
I could do this in Excel, but the dataset is too big to process.
You can use SUM with windows function and some Calculation to make it.
SELECT *,
SUM(qty) OVER (PARTITION BY date ORDER BY date) t_qty,
qty::numeric/SUM(qty) OVER (PARTITION BY date ORDER BY date) ,
fee * (qty::numeric/SUM(qty) OVER (PARTITION BY date ORDER BY date))
FROM T
If you want to Rounding you can use ROUND function.
SELECT *,
SUM(qty) OVER (PARTITION BY date ORDER BY date) t_qty,
ROUND(qty::numeric/SUM(qty) OVER (PARTITION BY date ORDER BY date),3) "% of total Qty",
ROUND(fee * (qty::numeric/SUM(qty) OVER (PARTITION BY date ORDER BY date)),3) "weighted fee"
FROM T
sqlfiddle
[Results]:
| date | id | type | qty | fee | t_qty | % of total Qty | weighted fee |
|------------|-----|------|------|-----|-------|----------------|--------------|
| 2015-01-01 | 001 | CB04 | 500 | 2 | 2600 | 0.192 | 0.385 |
| 2015-01-01 | 002 | CB04 | 1500 | 3 | 2600 | 0.577 | 1.731 |
| 2015-01-01 | 003 | CB04 | 500 | 1 | 2600 | 0.192 | 0.192 |
| 2015-01-01 | 004 | CB04 | 100 | 5 | 2600 | 0.038 | 0.192 |
| 2015-01-02 | 002 | CB03 | 3100 | 1 | 3100 | 1 | 1 |

how to adjust the sum() of a group using the sum() of a subgroup()

I am trying to get the Output shown in the third table below using the tables "Assets" and "Transactions".
I am trying to group by Cmpy, Acct and AssetID and get the Sum(cost). But each cost has to be adjusted from the Transactions table before being summed. Not sure how to do it.
Table: Assets
+----------+------+---------+--------+
| Cpny | Acct | AssetID | Cost |
+----------+------+---------+--------+
| 50 | 120 | 109 | 100.00 |
| 50 | 120 | 109 | 200.00 |
| 50 | 120 | 110 | 300.00 |
| 50 | 120 | 110 | 20.00 |
| 50 | 121 | 107 | 150.00 |
| 50 | 121 | 201 | 200.00 |
+----------+------+---------+--------+
Table: Transactions
+------+---------+--------+
| Cpny | AssetID | Amt |
+------+---------+--------+
| 50 | 109 | -50.00 |
| 50 | 110 | 50.00 |
| 50 | 110 | -20.00 |
| 50 | 201 | -50.00 |
+------+---------+--------+
OUTPUT
+------+------+--------+
| Cpny | Acct | Total |
+------+------+--------+
| 50 | 120 | 600.00 |
| 50 | 121 | 300.00 |
+------+------+--------+
This one should give you an accurate answer:
SELECT a.Cpny,
a.Acct,
SUM(a.Cost + ISNULL(t.Adjustment, 0)) AS Total
FROM Assets a
LEFT JOIN (SELECT Cpny,
AssetID,
SUM(Amt) AS Adjustment
FROM Transactions
GROUP BY Cpny, AssetID) t
ON t.Cpny = a.Cpny AND t.AssetID = a.AssetID
GROUP BY a.Cpny, a.Acct
Associated SQLFiddle here.
Essentially, SUM the adjustment amounts in the transactions table, then join this to the main results list, summing the cost plus the adjustment for each asset in each account.
If the "relationship" between Acct and AssetID values are 1 to many then you could use this query (which is not so efficient):
SELECT x.Cpny,x.Acct, SUM( ISNULL(x.Total,0) + ISNULL(y.Total,0) ) AS Total
FROM
(
SELECT a.Cpny,a.Acct,a.AssetID, SUM(a.Cost) AS Total
FROM dbo.Assets a
GROUP BY a.Cpny,a.Acct,a.AssetID
) x
LEFT JOIN
(
SELECT t.Cpny,t.AssetID, SUM(t.Cost) AS Total
FROM dbo.Transactions t
GROUP BY t.Cpny,t.AssetID
) y ON x.Cpny=y.Cpny AND x.AssetID=y.AssetID
GROUP BY x.Cpny,x.Acct;