SQL Deduct value from multiple rows - sql

I would like to apply total $10.00 discount for each customers.The discount should be applied to multiple transactions until all $10.00 used.
Example:
CustomerID Transaction Amount Discount TransactionID
1 $8.00 $8.00 1
1 $6.00 $2.00 2
1 $5.00 $0.00 3
1 $1.00 $0.00 4
2 $5.00 $5.00 5
2 $2.00 $2.00 6
2 $2.00 $2.00 7
3 $45.00 $10.00 8
3 $6.00 $0.00 9

The query below keeps track of the running sum and calculates the discount depending on whether the running sum is greater than or less than the discount amount.
select
customerid, transaction_amount, transactionid,
(case when 10 > (sum_amount - transaction_amount)
then (case when transaction_amount >= 10 - (sum_amount - transaction_amount)
then 10 - (sum_amount - transaction_amount)
else transaction_amount end)
else 0 end) discount
from (
select customerid, transaction_amount, transactionid,
sum(transaction_amount) over (partition by customerid order by transactionid) sum_amount
from Table1
) t1 order by customerid, transactionid
http://sqlfiddle.com/#!6/552c2/7
same query with a self join which should work on most db's including mssql 2008
select
customerid, transaction_amount, transactionid,
(case when 10 > (sum_amount - transaction_amount)
then (case when transaction_amount >= 10 - (sum_amount - transaction_amount)
then 10 - (sum_amount - transaction_amount)
else transaction_amount end)
else 0 end) discount
from (
select t1.customerid, t1.transaction_amount, t1.transactionid,
sum(t2.transaction_amount) sum_amount
from Table1 t1
join Table1 t2 on t1.customerid = t2.customerid
and t1.transactionid >= t2.transactionid
group by t1.customerid, t1.transaction_amount, t1.transactionid
) t1 order by customerid, transactionid
http://sqlfiddle.com/#!3/552c2/2

You can do this with recursive common table expressions, although it isn't particularly pretty. SQL Server stuggles to optimize these types of query. See Sum of minutes between multiple date ranges for some discussion.
If you wanted to go further with this approach, you'd probably need to make a temporary table of x, so you can index it on (customerid, rn)
;with x as (
select
tx.*,
row_number() over (
partition by customerid
order by transaction_amount desc, transactionid
) rn
from
tx
), y as (
select
x.transactionid,
x.customerid,
x.transaction_amount,
case
when 10 >= x.transaction_amount then x.transaction_amount
else 10
end as discount,
case
when 10 >= x.transaction_amount then 10 - x.transaction_amount
else 0
end as remainder,
x.rn as rn
from
x
where
rn = 1
union all
select
x.transactionid,
x.customerid,
x.transaction_amount,
case
when y.remainder >= x.transaction_amount then x.transaction_amount
else y.remainder
end,
case
when y.remainder >= x.transaction_amount then y.remainder - x.transaction_amount
else 0
end,
x.rn
from
y
inner join
x
on y.rn = x.rn - 1 and y.customerid = x.customerid
where
y.remainder > 0
)
update
tx
set
discount = y.discount
from
tx
inner join
y
on tx.transactionid = y.transactionid;
Example SQLFiddle

I usually like to setup a test environment for such questions. I will use a local temporary table. Please note, I made the data un-ordered since it is not guaranteed in a real life.
-- play table
if exists (select 1 from tempdb.sys.tables where name like '%transactions%')
drop table #transactions
go
-- play table
create table #transactions
(
trans_id int identity(1,1) primary key,
customer_id int,
trans_amt smallmoney
)
go
-- add data
insert into #transactions
values
(1,$8.00),
(2,$5.00),
(3,$45.00),
(1,$6.00),
(2,$2.00),
(1,$5.00),
(2,$2.00),
(1,$1.00),
(3,$6.00);
go
I am going to give you two answers.
First, in 2014 there are new windows functions for rows preceding. This allows us to get a running total (rt) and a rt adjusted by one entry. Give these two values, we can determine if the maximum discount has been exceeded or not.
-- Two running totals for 2014
;
with cte_running_total
as
(
select
*,
SUM(trans_amt)
OVER (PARTITION BY customer_id
ORDER BY trans_id
ROWS BETWEEN UNBOUNDED PRECEDING AND
0 PRECEDING) as running_tot_p0,
SUM(trans_amt)
OVER (PARTITION BY customer_id
ORDER BY trans_id
ROWS BETWEEN UNBOUNDED PRECEDING AND
1 PRECEDING) as running_tot_p1
from
#transactions
)
select
*
,
case
when coalesce(running_tot_p1, 0) <= 10 and running_tot_p0 <= 10 then
trans_amt
when coalesce(running_tot_p1, 0) <= 10 and running_tot_p0 > 10 then
10 - coalesce(running_tot_p1, 0)
else 0
end as discount_amt
from cte_running_total;
Again, the above version is using a common table expression and advanced windowing to get the totals.
Do not fret! The same can be done all the way down to SQL 2000.
Second solution, I am just going to use the order by, sub-queries, and a temporary table to store the information that is normally in the CTE. You can switch the temporary table for a CTE in SQL 2008 if you want.
-- w/o any fancy functions - save to temp table
select *,
(
select count(*) from #transactions i
where i.customer_id = o.customer_id
and i.trans_id <= o.trans_id
) as sys_rn,
(
select sum(trans_amt) from #transactions i
where i.customer_id = o.customer_id
and i.trans_id <= o.trans_id
) as sys_tot_p0,
(
select sum(trans_amt) from #transactions i
where i.customer_id = o.customer_id
and i.trans_id < o.trans_id
) as sys_tot_p1
into #results
from #transactions o
order by customer_id, trans_id
go
-- report off temp table
select
trans_id,
customer_id,
trans_amt,
case
when coalesce(sys_tot_p1, 0) <= 10 and sys_tot_p0 <= 10 then
trans_amt
when coalesce(sys_tot_p1, 0) <= 10 and sys_tot_p0 > 10 then
10 - coalesce(sys_tot_p1, 0)
else 0
end as discount_amt
from #results
order by customer_id, trans_id
go
In short, your answer is show in the following screen shot. Cut and paste the code into SSMS and have some fun.

Related

Is it possible to substract multiple rows based on other table without looping?

Let's say I have this Table Rewards
CustomerID DateReceived Point
1 2020-01-01 15
1 2020-01-02 20
2 2020-01-08 20
2 2020-01-10 10
3 2020-01-10 50
Then I have this table MaxRewards
CustomerID MaxPoint
1 20
2 10
3 30
So, the idea is Customer's Point combined (summed) should never surpassed MaxPoint in MaxRewards table, if it does, then it will substract starting on latest date, so in the case above, the expected result should be
CustomerID DateReceived Point
1 2020-01-01 15
1 2020-01-02 5
2 2020-01-08 10
2 2020-01-10 0
3 2020-01-10 30
How do I execute a query to update the table without a loop? I've been thinking of using CROSS APPLY but I can't seem to get it right.
The following approach seems to be working:
WITH cte AS (
SELECT
r.CustomerID,
r.DateReceived,
r.Point,
mr.MaxPoint,
COALESCE(SUM(r.Point) OVER (PARTITION BY r.CustomerID ORDER BY r.DateReceived
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING), 0) AS cumPoints
FROM Rewards r
INNER JOIN MaxRewards mr ON r.CustomerID = mr.CustomerID
)
SELECT
CustomerID,
DateReceived,
CASE WHEN cumPoints + Point < MaxPoint
THEN Point
ELSE CASE WHEN MaxPoint - cumPoints > 0
THEN MaxPoint - cumPoints ELSE 0 END END AS Point
FROM cte
ORDER BY
CustomerID,
DateReceived;
Demo
The logic used here is to compute, in the CTE, the cumulative sum of points for each customer, before the current record in the date sequence. Then, we report one of two things as the Point in the output. For cases where the cumulative sum be already greater than the maximum number of points, we report 0. Otherwise, we report the difference between the maximum number of points and the cumulative sum, which gives the number of points from the current record which we want to add.
Make use of Lag and Case statement:
Demo
SELECT CASE WHEN RUNNING_SUM > MAXPOINT AND PREV_VALUE IS NULL AND POINT > MAXPOINT THEN MAXPOINT
WHEN RUNNING_SUM > MAXPOINT AND PREV_VALUE IS NULL THEN POINT - MAXPOINT
WHEN RUNNING_SUM > MAXPOINT AND PREV_VALUE IS NOT NULL AND PREV_VALUE < POINT THEN POINT - PREV_VALUE
WHEN RUNNING_SUM > MAXPOINT AND PREV_VALUE IS NOT NULL THEN POINT - MAXPOINT
WHEN RUNNING_SUM > MAXPOINT THEN MAXPOINT - PREV_VALUE
ELSE POINT END AS POINT,
CUSTOMERID, DATERECEIVED FROM (
SELECT C.CUSTOMERID,C.DATERECEIVED, C.POINT, M.MAXPOINT, LAG(POINT) OVER(PARTITION BY C.CUSTOMERID ORDER BY DateReceived) PREV_VALUE,
SUM(POINT) OVER(PARTITION BY C.CUSTOMERID ORDER BY DateReceived) RUNNING_SUM
FROM CUSTOMER C INNER JOIN MaxRewards M
ON(C.CUSTOMERID = M.CUSTOMERID) ) X;
You can achieve the result from your requirements using lag, derived table, and case
select
CustomerID,
DateReceived,
case
when R.LastPoint > R.MaxPoint then 0
when R.Point + R.LastPoint > R.MaxPoint
then R.MaxPoint - R.LastPoint
else Point
end as Point
from (
select
R.CustomerID,
R.DateReceived,
R.Point,
MR.MaxPoint,
isnull(LAG(R.Point,1) OVER (
PARTITION BY R.CustomerID
ORDER BY R.DateReceived
),0) LastPoint
from dbo.Rewards R
join dbo.MaxRewards MR
on R.CustomerID = MR.CustomerID
) R
http://sqlfiddle.com/#!18/1a368
Please find the below query, I used an accumulative sum to calculate how many points left for each customer, and used IIF to understand whether a customer surpassed its threshold :
SELECT CustomerID,DateReceived,IIF(temp_point>0,temp_point,0) as Point
FROM
(
select CustomerID,DateReceived,
IIF(PointsLeft>0,Point,Point-ABS(PointsLeft)) as temp_point
from
(
select Rewards.* ,
sum(Rewards.Point) over (partition by MaxRewards.CustomerID order by DateReceived asc) s,
MaxRewards.Point as MaxPoint,
MaxRewards.Point - sum(Rewards.Point) over (partition by MaxRewards.CustomerID order by DateReceived asc) PointsLeft
From Rewards, MaxRewards
where Rewards.CustomerID = MaxRewards.CustomerID
) a
) b

Oracle SQL Hierarchy Summation

I have a table TRANS that contains the following records:
TRANS_ID TRANS_DT QTY
1 01-Aug-2020 5
1 01-Aug-2020 1
1 03-Aug-2020 2
2 02-Aug-2020 1
The expected output:
TRANS_ID TRANS_DT BEGBAL TOTAL END_BAL
1 01-Aug-2020 0 6 6
1 02-Aug-2020 6 0 6
1 03-Aug-2020 6 2 8
2 01-Aug-2020 0 0 0
2 02-Aug-2020 0 1 1
2 03-Aug-2020 1 0 1
Each trans_id starts with a beginning balance of 0 (01-Aug-2020). For succeeding days, the beginning balance is the ending balance of the previous day and so on.
I can create PL/SQL block to create the output. Is it possible to get the output in 1 SQL statement?
Thanks.
Try this following script using CTE-
Demo Here
WITH CTE
AS
(
SELECT DISTINCT A.TRANS_ID,B.TRANS_DT
FROM your_table A
CROSS JOIN (SELECT DISTINCT TRANS_DT FROM your_table) B
),
CTE2
AS
(
SELECT C.TRANS_ID,C.TRANS_DT,SUM(D.QTY) QTY
FROM CTE C
LEFT JOIN your_table D
ON C.TRANS_ID = D.TRANS_ID
AND C.TRANS_DT = D.TRANS_DT
GROUP BY C.TRANS_ID,C.TRANS_DT
ORDER BY C.TRANS_ID,C.TRANS_DT
)
SELECT F.TRANS_ID,F.TRANS_DT,
(
SELECT COALESCE (SUM(QTY), 0) FROM CTE2 E
WHERE E.TRANS_ID = F.TRANS_ID AND E.TRANS_DT < F.TRANS_DT
) BEGBAL,
(
SELECT COALESCE (SUM(QTY), 0) FROM CTE2 E
WHERE E.TRANS_ID = F.TRANS_ID AND E.TRANS_DT = F.TRANS_DT
) TOTAL ,
(
SELECT COALESCE (SUM(QTY), 0) FROM CTE2 E
WHERE E.TRANS_ID = F.TRANS_ID AND E.TRANS_DT <= F.TRANS_DT
) END_BAL
FROM CTE2 F
You can as well do like this (I would assume it's a bit faster): Demo
with
dt_between as (
select mindt + level - 1 as trans_dt
from (select min(trans_dt) as mindt, max(trans_dt) as maxdt from t)
connect by level <= maxdt - mindt + 1
),
dt_for_trans_id as (
select *
from dt_between, (select distinct trans_id from t)
),
qty_change as (
select distinct trans_id, trans_dt,
sum(qty) over (partition by trans_id, trans_dt) as total,
sum(qty) over (partition by trans_id order by trans_dt) as end_bal
from t
right outer join dt_for_trans_id using (trans_id, trans_dt)
)
select
trans_id,
to_char(trans_dt, 'DD-Mon-YYYY') as trans_dt,
nvl(lag(end_bal) over (partition by trans_id order by trans_dt), 0) as beg_bal,
nvl(total, 0) as total,
nvl(end_bal, 0) as end_bal
from qty_change q
order by trans_id, trans_dt
dt_between returns all the days between min(trans_dt) and max(trans_dt) in your data.
dt_for_trans_id returns all these days for each trans_id in your data.
qty_change finds difference for each day (which is TOTAL in your example) and cumulative sum over all the days (which is END_BAL in your example).
The main select takes END_BAL from previous day and calls it BEG_BAL, it also does some formatting of final output.
First of all, you need to generate dates, then you need to aggregate your values by TRANS_DT, and then left join your aggregated data to dates. The easiest way to get required sums is to use analitic window functions:
with dates(dt) as ( -- generating dates between min(TRANS_DT) and max(TRANS_DT) from TRANS
select min(trans_dt) from trans
union all
select dt+1 from dates
where dt+1<=(select max(trans_dt) from trans)
)
,trans_agg as ( -- aggregating QTY in TRANS
select TRANS_ID,TRANS_DT,sum(QTY) as QTY
from trans
group by TRANS_ID,TRANS_DT
)
select -- using left join partition by to get data on daily basis for each trans_id:
dt,
trans_id,
nvl(sum(qty) over(partition by trans_id order by dates.dt range between unbounded preceding and 1 preceding),0) as BEGBAL,
nvl(qty,0) as TOTAL,
nvl(sum(qty) over(partition by trans_id order by dates.dt),0) as END_BAL
from dates
left join trans_agg tr
partition by (trans_id)
on tr.trans_dt=dates.dt;
Full example with sample data:
alter session set nls_date_format='dd-mon-yyyy';
with trans(TRANS_ID,TRANS_DT,QTY) as (
select 1,to_date('01-Aug-2020'), 5 from dual union all
select 1,to_date('01-Aug-2020'), 1 from dual union all
select 1,to_date('03-Aug-2020'), 2 from dual union all
select 2,to_date('02-Aug-2020'), 1 from dual
)
,dates(dt) as ( -- generating dates between min(TRANS_DT) and max(TRANS_DT) from TRANS
select min(trans_dt) from trans
union all
select dt+1 from dates
where dt+1<=(select max(trans_dt) from trans)
)
,trans_agg as ( -- aggregating QTY in TRANS
select TRANS_ID,TRANS_DT,sum(QTY) as QTY
from trans
group by TRANS_ID,TRANS_DT
)
select
dt,
trans_id,
nvl(sum(qty) over(partition by trans_id order by dates.dt range between unbounded preceding and 1 preceding),0) as BEGBAL,
nvl(qty,0) as TOTAL,
nvl(sum(qty) over(partition by trans_id order by dates.dt),0) as END_BAL
from dates
left join trans_agg tr
partition by (trans_id)
on tr.trans_dt=dates.dt;
You can use a recursive query to generate the overall date range, cross join it with the list of distinct tran_id, then bring the table with a left join. The last step is aggregation and window functions:
with all_dates (trans_dt, max_dt) as (
select min(trans_dt), max(trans_dt) from trans group by trans_id
union all
select trans_dt + interval '1' day, max_dt from all_dates where trans_dt < max_dt
)
select
i.trans_id,
d.trans_dt,
coalesce(sum(sum(t.qty)) over(partition by i.trans_id order by d.trans_dt), 0) - coalesce(sum(t.qty), 0) begbal,
coalesce(sum(t.qty), 0) total,
coalesce(sum(sum(t.qty)) over(partition by i.trans_id order by d.trans_dt), 0) endbal
from all_dates d
cross join (select distinct trans_id from trans) i
left join trans t on t.trans_id = i.trans_id and t.trans_dt = d.trans_dt
group by i.trans_id, d.trans_dt
order by i.trans_id, d.trans_dt

Find the true start end dates for customers that have multiple accounts in SQL Server 2014

I have a checking account table that contains columns Cust_id (customer id), Open_Date (start date), and Closed_Date (end date). There is one row for each account. A customer can open multiple accounts at any given point. I would like to know how long the person has been a customer.
eg 1:
CREATE TABLE [Cust]
(
[Cust_id] [varchar](10) NULL,
[Open_Date] [date] NULL,
[Closed_Date] [date] NULL
)
insert into [Cust] values ('a123', '10/01/2019', '10/15/2019')
insert into [Cust] values ('a123', '10/12/2019', '11/01/2019')
Ideally I would like to insert this into a table with just one row, that says this person has been a customer from 10/01/2019 to 11/01/2019. (as he opened his second account before he closed his previous one.
Similarly eg 2:
insert into [Cust] values ('b245', '07/01/2019', '09/15/2019')
insert into [Cust] values ('b245', '10/12/2019', '12/01/2019')
I would like to see 2 rows in this case- one that shows he was a customer from 07/01 to 09/15 and then again from 10/12 to 12/01.
Can you point me to the best way to get this?
I would approach this as a gaps and islands problem. You want to group together groups of adjacents rows whose periods overlap.
Here is one way to solve it using lag() and a cumulative sum(). Everytime the open date is greater than the closed date of the previous record, a new group starts.
select
cust_id,
min(open_date) open_date,
max(closed_date) closed_date
from (
select
t.*,
sum(case when not open_date <= lag_closed_date then 1 else 0 end)
over(partition by cust_id order by open_date) grp
from (
select
t.*,
lag(closed_date) over (partition by cust_id order by open_date) lag_closed_date
from cust t
) t
) t
group by cust_id, grp
In this db fiddle with your sample data, the query produces:
cust_id | open_date | closed_date
:------ | :--------- | :----------
a123 | 2019-10-01 | 2019-11-01
b245 | 2019-07-01 | 2019-09-15
b245 | 2019-10-12 | 2019-12-01
I would solve this with recursion. While this is certainly very heavy, it should accommodate even the most complex account timings (assuming your data has such). However, if the sample data provided is as complex as you need to solve for, I highly recommend sticking with the solution provided above. It is much more concise and clear.
WITH x (cust_id, open_date, closed_date, lvl, grp) AS (
SELECT cust_id, open_date, closed_date, 1, 1
FROM (
SELECT cust_id
, open_date
, closed_date
, row_number()
OVER (PARTITION BY cust_id ORDER BY closed_date DESC, open_date) AS rn
FROM cust
) AS t
WHERE rn = 1
UNION ALL
SELECT cust_id, open_date, closed_date, lvl, grp
FROM (
SELECT c.cust_id
, c.open_date
, c.closed_date
, x.lvl + 1 AS lvl
, x.grp + CASE WHEN c.closed_date < x.open_date THEN 1 ELSE 0 END AS grp
, row_number() OVER (PARTITION BY c.cust_id ORDER BY c.closed_date DESC) AS rn
FROM cust c
JOIN x
ON x.cust_id = c.cust_id
AND c.open_date < x.open_date
) AS t
WHERE t.rn = 1
)
SELECT cust_id, min(open_date) AS first_open_date, max(closed_date) AS last_closed_date
FROM x
GROUP BY cust_id, grp
ORDER BY cust_id, grp
I would also add the caveat that I don't run on SQL Server, so there could be syntax differences that I didn't account for. Hopefully they are minor, if present.
you can try something like that:
select distinct
cust_id,
(select min(Open_Date)
from Cust as b
where b.cust_id = a.cust_id and
a.Open_Date <= b.Closed_Date and
a.Closed_Date >= b.Open_Date
),
(select max(Closed_Date)
from Cust as b
where b.cust_id = a.cust_id and
a.Open_Date <= b.Closed_Date and
a.Closed_Date >= b.Open_Date
)
from Cust as a
so, for every row - you're selecting minimal and maximal dates from all overlapping ranges, later distinct filters out duplicates

Histogram: Counting orders with variable bins in SQL

I have a table containing orders, items, and prices. I am trying to generate histograms for each item based on the prices.
Create Table #Customer_Pricing
(
customer_id int,
item_id VARCHAR(10),
qty DECIMAL(5,2),
price DECIMAL(5,2),
)
;
GO
-- Insert Statements
Insert into #Customer_Pricing values(128456, 'SOM 555', 8, 2.50)
Insert into #Customer_Pricing values(123856, 'SOM 554', 1, 2.50)
Insert into #Customer_Pricing values(123456, 'SOM 554', 55, 2.00)
Insert into #Customer_Pricing values(123556, 'SOM 555', 2, 2.20)
Insert into #Customer_Pricing values(123456, 'SOM 553', 12, 2.13)
;
For each item, I wanted 3 bins so I determined the bin sizes by dividing the difference of the MAX-MIN by 3, then adding that value to the MIN.
WITH Stats_Table_CTE (item_id2,max_p, min_p, int_p, r1_upper, r2_lower, r2_upper, r3_lower)
AS
( SELECT item_id
,max(price)
,min(price)
,(max(price) - min(price))/3
,min(price)+(max(price) - min(price))/3-0.01
,min(price)+(max(price) - min(price))/3
,min(price)+((max(price) - min(price))/3)*2-0.01
,min(price)+((max(price) - min(price))/3)*2
FROM #Customer_Pricing
GROUP BY item_id)
Now, I need to count the frequencies for each range and each item. I have attempted to do so by using SUM(CASE...) but was unsuccessful.
SELECT item_id
,SUM(CASE WHEN price <= r1_upper, THEN 1 ELSE 0 END) AS r1_count
,SUM(CASE WHEN price >= r2_lower AND <= r2_upper, THEN 1 ELSE 0 END) AS r2_count
,SUM(CASE WHEN price >= r3_lower, THEN 1 ELSE 0 END) AS r3_count
FROM Stats_Table_CTE
GROUP BY item_id
I also attempted to use COUNT in the form
SELECT item_id, price
count(price <= r1_upper) AS r1_count.... but I got stuck
In one attempt, INNER JOINed the #Customer_Pricing table and Stats_Table_CTE but didn't know where to go from there.
Ideally, I would like the output table to appear as follows: *This is not the actual data, but I included it to show the desired format of the output.
Item ID min_p r1_upper (r2 bins) r3_lower max_p r1_count r2_ct
SOM 553 2.00 2.16 saving space 2.33 2.50 2 1
SOM 554 2.13 2.48 2.88 3.25 1 0
SOM 555 2.31 2.51 2.72 2.92 3 2
*The format of the output table is off, but I have item ID, the bins, and the counts across the top grouped by item
Here is my recommendation:
WITH Stats_Table_CTE AS (
SELECT item_id, max(price) as maxprice, min(price) as minprice,
(max(price) - min(price))/3 as binsize
FROM #Customer_Pricing
GROUP BY item_id
)
SELECT cp.item_id,
SUM(CASE WHEN price < minprice + binsize THEN 1 ELSE 0
END) AS r1_count
SUM(CASE WHEN price >= minprice + binsize AND price < minprice+ 2*binsize
THEN 1 ELSE 0
END) AS r2_count
SUM(CASE WHEN price >= minprice + 2*binsize
THEN 1 ELSE 0
END) AS r3_count
FROM #Customer_Pricing cp JOIN
Stats_Table_CTE st
ON st.item_id = cp.item_id
GROUP BY cp.item_id
The important part is the join back to #Customer_Pricing. Also important is the simplification of the logic -- you can define the bounds for the bins and use <, rather than having a lower and upper bound for each one. Also, your query had some syntax errors in it.
Note that in many databases, the CTE would not be necessary because you could just use window functions. Your question is not tagged with the database (although I could guess what it is), so that change seems unwarranted.

Oracle : sum this and this and this in a single query

I have on order table containing orders for last week and the ID of the driver who delivered them. It looks a little like this:
ORDERDATE, ORDERNO, DRIVER
23/01/2013, 901398503, 1
23/01/2013, 901332159, 1
23/01/2013, 901334158, 2
24/01/2013, 901338455, 1
25/01/2013, 902907513, 1
25/01/2013, 902338553, 2
25/01/2013, 903936533, 2
27/01/2013, 903944523, 1
27/01/2013, 903981522, 2
27/01/2013, 911334951, 1
28/01/2013, 911338851, 1
28/01/2013, 911339259, 1
28/01/2013, 912332555, 2
28/01/2013, 912336650, 2
29/01/2013, 912337655, 1
29/01/2013, 913969582, 1
29/01/2013, 913973583, 1
29/01/2013, 913982552, 1
29/01/2013, 916379158, 1
I'd like to select ORDERDATE, ORDERCOUNT, DRIVER_1_COUNT, DRIVER_2_COUNT.
so, date | total orders | total orders for driver 1 | total orders for driver 2
Also, I need zeros if ORDERDATE, ORDERCOUNT, DRIVER_1_COUNT or DRIVER_2_COUNT are 0 (or null).
(In oracle) I can select dates for each day last week, and a zero order count (placeholder) for each day like this:
select
TRUNC(NEXT_DAY(sysdate,'SUNDAY')-7 +i) ORDERDATE,
0 as ORDERCOUNT
from
(select rownum i from all_objects where rownum < 8)
I should be able to use this output to make sure there are no days missing in the final results (no orders on 26th in this example)
ORDERDATE,ORDERCOUNT
23/01/2013,0
24/01/2013,0
25/01/2013,0
26/01/2013,0
27/01/2013,0
28/01/2013,0
29/01/2013,0
I need this output:
ORDERDATE,ORDERCOUNT,DRIVER_1_COUNT,DRIVER_2_COUNT
23/01/2013,3,2,1
24/01/2013,1,1,0
25/01/2013,3,1,2
26/01/2013,0,0,0
27/01/2013,3,2,1
28/01/2013,4,2,2
29/01/2013,5,5,0
I can get ORDERDATE & ORDERCOUNT(simple sum) and union with the other query to avoid missing days, but I can't work out how to sum for each driver too.
Thanks in advance for you help.
Ed
In Oracle 11g, you can do this:-
SELECT *
FROM orders
PIVOT (
COUNT( ORDERNO )
FOR DRIVER IN (1,2,3)
)
For further explanation see pivot and unpivot queries in 11g
Firstly you need to summarise and crosstab results:
SELECT ORDERDATE, SUM(ORDERCOUNT) ORDERCOUNT,
SUM(DECODE(DRIVER,1,ORDERCOUNT,0)) DRIVER_1_COUNT,
SUM(DECODE(DRIVER,2,ORDERCOUNT,0)) DRIVER_2_COUNT
FROM (
SELECT ORDERDATE, DRIVER, COUNT(*) ORDERCOUNT
FROM YourTable
GROUP BY ORDERDATE, DRIVER
) S
GROUP BY ORDERDATE
There might be smarter ways to do this in Oracle
Then you need to fill in the blanks using by outer joining this to your dates:
(note the query above is aliased as 'T' in this query:)
SELECT D.ORDERDATE,
NVL(T.ORDERCOUNT,0) ORDERCOUNT,
NVL(T.DRIVER_1_COUNT,0) DRIVER_1_COUNT,
NVL(T.DRIVER_1_COUNT,0) DRIVER_2_COUNT
FROM
(
SELECT ORDERDATE, SUM(ORDERCOUNT) ORDERCOUNT,
SUM(DECODE(DRIVER,1,ORDERCOUNT,0)) DRIVER_1_COUNT,
SUM(DECODE(DRIVER,2,ORDERCOUNT,0)) DRIVER_2_COUNT
FROM
(
SELECT ORDERDATE, DRIVER, COUNT(*) ORDERCOUNT
FROM YourTable
GROUP BY ORDERDATE, DRIVER
) S
GROUP BY ORDERDATE
) T
RIGHT OUTER JOIN
(
SELECT
TRUNC(NEXT_DAY(sysdate,'SUNDAY')-7 +i) ORDERDATE
FROM (select rownum i from all_objects where rownum < 8)
) D
ON D.ORDERDATE = T.ORDERDATE
You'll have to select from a subquery. Something like this should work.
select orderdate, ordercount, sum(driver1) driver1count, sum(driver2) driver2count
from (
select orderdate
, case when driver = 1 then 1 else 0 end driver1
, case when driver = 2 then 1 else 0 end driver2
, count(*) ordercount
from yourtable
where whatever
group by orderdate
, case when driver = 1 then 1 else 0 end driver1
, case when driver = 2 then 1 else 0 end driver2
) you_need_an_alias_here
group by orderdate, ordercount
order by orderdate