Add running or cumulative total - sql

I have below query which gives me expected results:
SELECT
total_orders,
quantity,
available_store_credits
FROM
(
SELECT
COUNT(orders.id) as total_orders,
date_trunc('year', confirmed_at) as year,
date_trunc('month', confirmed_at) as month,
SUM( quantity ) as quantity,
FROM
orders
INNER JOIN (
SELECT
orders.id,
sum(quantity) as quantity
FROM
orders
INNER JOIN line_items ON line_items.order_id = orders.id
WHERE
orders.deleted_at IS NULL
AND orders.status IN (
'paid', 'packed', 'in_transit', 'delivered'
)
GROUP BY
orders.id
) as order_quantity
ON order_quantity.id = orders.id
GROUP BY month, year) as orders_transactions
FULL OUTER JOIN
(
SELECT
date_trunc('year', created_at) as year,
date_trunc('month', created_at) as month,
SUM( ROUND( ( CASE WHEN amount_in_cents > 0 THEN amount_in_cents end) / 100, 2 )) AS store_credit_given,
SUM( ROUND( amount_in_cents / 100, 2 )) AS available_store_credits
FROM
store_credit_transactions
GROUP BY month, year
) as store_credit_results
ON orders_transactions.month = store_credit_results.month
I want to add one more column beside available_store_credits which will calculate running total of available_store_credits.
These are my trials, but none are working:
Attempt #1
SELECT
total_orders,
quantity,
available_store_credits,
cum_amt
FROM
(
SELECT
COUNT(orders.id) as total_orders,
date_trunc('year', confirmed_at) as year,
date_trunc('month', confirmed_at) as month,
SUM( quantity ) as quantity,
FROM
orders
INNER JOIN (
SELECT
orders.id,
sum(quantity) as quantity
FROM
orders
INNER JOIN line_items ON line_items.order_id = orders.id
WHERE
orders.deleted_at IS NULL
AND orders.status IN (
'paid', 'packed', 'in_transit', 'delivered'
)
GROUP BY
orders.id
) as order_quantity
ON order_quantity.id = orders.id
GROUP BY month, year) as orders_transactions
FULL OUTER JOIN
(
SELECT
date_trunc('year', created_at) as year,
date_trunc('month', created_at) as month,
SUM( ROUND( ( CASE WHEN amount_in_cents > 0 THEN amount_in_cents end) / 100, 2 )) AS store_credit_given,
SUM( ROUND( amount_in_cents / 100, 2 )) AS available_store_credits
SUM( amount_in_cents ) OVER (ORDER BY date_trunc('month', created_at), date_trunc('year', created_at)) AS cum_amt
FROM
store_credit_transactions
GROUP BY month, year
) as store_credit_results
ON orders_transactions.month = store_credit_results.month
Attempt #2
SELECT
total_orders,
quantity,
available_store_credits,
running_tot
FROM
(
SELECT
COUNT(orders.id) as total_orders,
date_trunc('year', confirmed_at) as year,
date_trunc('month', confirmed_at) as month,
FROM
orders
INNER JOIN (
SELECT
orders.id,
sum(quantity) as quantity
FROM
orders
INNER JOIN line_items ON line_items.order_id = orders.id
WHERE
orders.deleted_at IS NULL
AND orders.status IN (
'paid', 'packed', 'in_transit', 'delivered'
)
GROUP BY
orders.id
) as order_quantity
ON order_quantity.id = orders.id
GROUP BY month, year) as orders_transactions
FULL OUTER JOIN
(
SELECT
date_trunc('year', created_at) as year,
date_trunc('month', created_at) as month,
SUM( ROUND( amount_in_cents / 100, 2 )) AS available_store_credits,
SUM (available_store_creds) as running_tot
FROM
store_credit_transactions
INNER JOIN (
SELECT t0.id,
(
SELECT SUM( ROUND( amount_in_cents / 100, 2 )) as running_total
FROM store_credit_transactions as t1
WHERE date_trunc('month', t1.created_at) <= date_trunc('month', t0.created_at)
) AS available_store_creds
FROM store_credit_transactions AS t0
) as results
ON results.id = store_credit_transactions.id
GROUP BY month, year
) as store_credit_results
ON orders_transactions.month = store_credit_results.month

Making some assumptions about the undisclosed table definition and Postgres version (assuming current Postgres 14), this should do it:
SELECT total_orders, quantity, available_store_credits
, sum(available_store_credits) OVER (ORDER BY month) AS cum_amt -- HERE!!
FROM (
SELECT date_trunc('month', confirmed_at) AS month
, count(*) AS total_orders
, sum(quantity) AS quantity
FROM (
SELECT o.id, o.confirmed_at, sum(quantity) AS quantity
FROM orders o
JOIN line_items l ON l.order_id = o.id
WHERE o.deleted_at IS NULL
AND o.status IN ('paid', 'packed', 'in_transit', 'delivered')
GROUP BY 1
) o
GROUP BY 1
) orders_transactions
FULL JOIN (
SELECT date_trunc('month', created_at) AS month
, round(sum(amount_in_cents) FILTER (WHERE amount_in_cents > 0) / 100, 2) AS store_credit_given
, round(sum(amount_in_cents) / 100, 2) AS available_store_credits
FROM store_credit_transactions
GROUP BY 1
) store_credit_results USING (month)
Assuming you want the running sum to show up in every row and order of the date.
First, I simplified and removed some cruft:
date_trunc('year', confirmed_at) as year, was 100 % redundant noise in your query. I removed it.
As was another join to orders. Removed that, too. Assuming orders.id is defined as PK, we can further simplify. See:
PostgreSQL - GROUP BY clause
Use the superior aggregate FILTER. See:
Aggregate columns with additional (distinct) filters
Simplified a couple of other minor bits.

Related

How ovoid duplicate code in sql query subQuery

I'm new in SQL. Need some help to improve my query to ovoid duplicate code.
SELECT customers.name, orders.price
FROM customers
JOIN orders ON orders.id = customers.order_id
WHERE customers.order_id IN (
SELECT orders.id
FROM orders
WHERE orders.price = (
SELECT orders.price
FROM orders
WHERE orders.order_date BETWEEN
(SELECT MIN(orders.order_date) FROM orders)
AND
(SELECT DATE_ADD(MIN(orders.order_date), INTERVAL 10 year)FROM orders)
ORDER BY orders.price DESC LIMIT 1
)
AND orders.order_date BETWEEN
(SELECT MIN(orders.order_date) FROM orders)
AND
(SELECT DATE_ADD(MIN(orders.order_date), INTERVAL 10 year)FROM orders)
)
I would like ovoid duplicate code here
SELECT MIN(orders.order_date) FROM orders
and
SELECT DATE_ADD(MIN(orders.order_date), INTERVAL 10 year)FROM orders
You can use WITH to get first 10 years orders. By defitinion there exists no orders with the date < min(date), so you needn't between, just <= .
firstOrders as (
SELECT *
FROM orders
WHERE order_date <=
(SELECT DATE_ADD(MIN(o.order_date), INTERVAL 10 year)
FROM orders o)
)
SELECT customers.name, orders.price
FROM customers
JOIN FirsrOrders orders ON orders.id = customers.order_id
AND orders.price = (
select price
from firstOrders
order py price desc
limit 1
)
You want orders from the first ten years where the price was equal to the maximum price among those orders. So rank by price and grab those holding the #1 spot.
with data as (
select *,
date_add(min(order_date) over (), interval 10 year) as max_date,
rank() over (order by price desc) as price_rank
from orders
)
select *
from data
where order_date <= max_date and price_rank = 1;

window function count aggregation

I have two quite complex queries going on here. Both should return per given country (Netherlands in this case) the monthly use over the total of 12 months and the percentage that makes up that month of the total of those 12 months.
But the one where i use count as a windowing function returns one more row than the one where i don't use the count as a windowing function.
The query for the left side picture is:
;WITH MonthUsage AS
(
SELECT customer_id, country_name, [Month], [Year], SUM(ItemsPerMonth)
AS ItemsPerMonth
FROM (
SELECT cs.country_name, c.customer_id, YEAR(mol.date_watched) AS Year, MONTH(mol.date_watched) AS [Month], COUNT(*) AS ItemsPerMonth
FROM Customer c
JOIN Customer_Subscription CS ON C.customer_id = CS.customer_id
JOIN Movie_Order_Line mol ON mol.customer_id = c.customer_id
WHERE (mol.date_watched BETWEEN '2017-07-01' AND '2018-07-01') AND cs.country_name = 'Nederland'
GROUP BY c.customer_id,cs.country_name, YEAR(mol.date_watched),
MONTH(mol.date_watched)
UNION ALL
SELECT cs.country_name, c.customer_id, YEAR(sol.date_watched) AS Year,
MONTH(sol.date_watched), COUNT(*) AS ItemsPerMonth
FROM Customer c
JOIN Customer_Subscription CS ON C.customer_id = CS.customer_id
JOIN Show_Order_Line sol ON sol.customer_id = c.customer_id
WHERE sol.date_watched BETWEEN '2017-07-01' AND '2018-07-01'
GROUP BY c.customer_id, cs.country_name, YEAR(sol.date_watched),
MONTH(sol.date_watched)
) AS MonthItems
WHERE country_name = 'Nederland'
GROUP BY customer_id, country_name, [Month], [Year]
),
Months(MonthNumber) AS
(
SELECT 1
UNION ALL
SELECT MonthNumber + 1
FROM months
WHERE MonthNumber < 12
)
SELECT cmb.[Year], ISNULL(cmb.[Month], m.MonthNumber) AS [Month],
ISNULL(ItemsPerMonth, 0) AS ItemsPerMonth,
ISNULL(FORMAT(((CAST(ItemsPerMonth AS decimal) / CAST((
SELECT SUM(ItemsPerMonth)
FROM MonthUsage) AS decimal
))),'P0'), '0%') AS [PercentageOfTotal]
FROM MonthUsage cmb
JOIN Customer c ON c.customer_id = cmb.customer_id
JOIN Months m ON m.MonthNumber = cmb.[Month]
ORDER BY cmb.[Year] ASC, [Month] ASC
And the query for the right picture is:
;WITH MonthUsage AS (
SELECT *
FROM (
SELECT
YEAR(date_watched) AS [Year],
MONTH(date_watched) AS [Month],
COUNT(order_id) OVER(PARTITION BY CONCAT(YEAR(date_watched), MONTH(date_watched))) AS ItemsPerMonth
FROM Movie_Order_Line mov
JOIN Customer c ON c.customer_id = mov.customer_id
JOIN Customer_Subscription cs ON C.customer_id = cs.customer_id
WHERE date_watched BETWEEN '2017-07-01' AND '2018-07-01' AND cs.country_name = 'Nederland'
UNION ALL
SELECT YEAR(date_watched) AS [Year], MONTH(date_watched) AS [Month], COUNT(order_id) OVER(PARTITION BY CONCAT(YEAR(date_watched), MONTH(date_watched))) AS ItemsPerMonth
FROM Show_Order_Line sol
JOIN Customer c ON c.customer_id = sol.customer_id
JOIN Customer_Subscription cs ON C.customer_id = cs.customer_id
WHERE date_watched BETWEEN '2017-07-01' AND '2018-07-01' AND cs.country_name = 'Nederland'
) AS Combined
GROUP BY [YEAR], [Month], ItemsPerMonth
)
SELECT *, ISNULL(FORMAT(((CAST(ItemsPerMonth AS decimal) / CAST((SELECT
SUM(ItemsPerMonth) FROM MonthUsage) AS decimal))),'P0'), '0%') AS
[PercentageOfTotal]
FROM MonthUsage
ORDER BY [Year] ASC, [Month] ASC
I can't seem to figure out why i'm getting different results. Any help is much appreciated. Thank you in advance for your time.

Postgres get sales for top account with ranking

I have the following tables:
Account (id, name)
Solution (id, name)
Sales (solution_id, account_id, month, year, amount)
I need to calculate the monthly sales of each account in a specific period:
SELECT
to_char(make_date(sales.year, sales.month, 1), 'YYYY-MM') AS period,
acc.id AS account_id,
acc.name AS account_name,
COALESCE(SUM(sales.net_sales), 0) AS amount
FROM
(SELECT *
FROM sales
WHERE make_date(year, month, 1) >= FROM_DATE
AND make_date(year, month, 1) <= TO_DATE) sales
INNER JOIN account acc.id = sales.account_id
GROUP BY sales.year, sales.month
ORDER BY sales.year, sales.month ASC
I can now calculate the total sales, in the period in the range:
SELECT
to_char(make_date(sales.year, sales.month, 1), 'YYYY-MM') AS period,
acc.id AS account_id,
acc.name AS account_name,
COALESCE(SUM(sales.net_sales), 0) AS amount
FROM
(SELECT *, COALESCE(SUM(net_sales) OVER (PARTITION BY client_id), 0) AS total
FROM sales
WHERE make_date(year, month, 1) >= FROM_DATE
AND make_date(year, month, 1) <= TO_DATE) sales
INNER JOIN account acc.id = sales.account_id
GROUP BY sales.year, sales.month
ORDER BY sales.year, sales.month ASC
Is there a way to rank the total sales in order to get only the n top account in the selected period?
Your queries are a bit of a mess. The first is not syntactically correct. I think you can simplify and the intention is:
SELECT to_char(make_date(s.year, s.month, 1), 'YYYY-MM') AS period,
a.id AS account_id, a.name AS account_name,
COALESCE(SUM(s.net_sales), 0) AS amount,
SUM(SUM(s.net_sales)) OVER (PARTITION BY a.id) as total
FROM sales s INNER JOIN
account a
ON a.id = s.account_id
WHERE make_date(s.year, s.month, 1) >= FROM_DATE AND
make_date(s.year, s.month, 1) <= TO_DATE
GROUP BY s.year, s.month, a.id, a.name
ORDER BY s.year, s.month ASC;
If you want to rank by total sales (or monthly sales), then you can use dense_rank():
SELECT ym.*
FROM (SELECT to_char(make_date(s.year, s.month, 1), 'YYYY-MM') AS period,
a.id AS account_id, a.name AS account_name,
COALESCE(SUM(s.net_sales), 0) AS amount,
total,
DENSE_RANK() OVER (ORDER BY total DESC) as seqnum
FROM (SELECT s.*, SUM(s.net_sales) OVER (PARTITION BY client_id) as total
FROM sales s
) s INNER JOIN
account a
ON a.id = s.account_id
WHERE make_date(s.year, s.month, 1) >= FROM_DATE AND
make_date(s.year, s.month, 1) <= TO_DATE
GROUP BY s.year, s.month
) ym
WHERE seqnum <= 3
ORDER BY s.year, s.month ASC;

Join issue on SQL request

My database :
TB_DW_VAB_FLIGHT : ID_TEC_FLIGHT
TB_DW_VAB_SALES : QUANTITY, TRANSACTION_NUMBER, UNIT_SALES_PRICE
I want to have a table with 4 columns as result : CA, QTE, NB_TRANSACTION and NB_VOLS at the same month. ( N-1 )
I tried a SQL request like this :
SELECT
sum(QUANTITY*UNIT_SALES_PRICE) as CA,
sum(QUANTITY) as QTE,
count(distinct TRANSACTION_NUMBER) as NB_TRANSACTION,
count(distinct ID_TEC_FLIGHT) as NB_VOLS
FROM TB_DW_VAB_SALES, TB_DW_VAB_FLIGHT
where to_char(TB_DW_VAB_SALES.FLIGHT_DATE,'MM')=to_char(current_date,'MM')-1 and to_char(TB_DW_VAB_SALES.FLIGHT_DATE,'YYYY')=to_char(current_date,'YYYY') and SALES_TYPE='SALES'
and to_char(TB_DW_VAB_FLIGHT.FLIGHT_DATE,'MM')=to_char(current_date,'MM')-1 and to_char(TB_DW_VAB_FLIGHT.FLIGHT_DATE,'YYYY')=to_char(current_date,'YYYY');
But Oracle can't give me an answer.
Thank you a lot for any help.
Try
with CTE1 as
(
select to_char(FLIGHT_DATE, 'MM-YYYY') as PERIOD,
sum(QUANTITY*UNIT_SALES_PRICE) as CA,
sum(QUANTITY) as QTE,
count(distinct TRANSACTION_NUMBER) as NB_TRANSACTION
from TB_DW_VAB_SALES
where SALES_TYPE = 'SALES'
group by to_char(FLIGHT_DATE, 'MM-YYYY')
)
, CTE2 as
(
select count(distinct ID_TEC_FLIGHT) as NB_VOLS,
to_char(FLIGHT_DATE, 'MM-YYYY') as PERIOD
from TB_DW_VAB_FLIGHT
group by to_char(FLIGHT_DATE, 'MM-YYYY')
)
select CTE1.CA,
CTE1.QTE,
CTE1.NB_TRANSACTION,
CTE2.NB_VOLS
from CTE1
inner join CTE2 on CTE1.PERIOD = CTE2.PERIOD
where CTE1.PERIOD = to_char(add_Months(sysdate,-1),'MM-YYYY')
or if CTE's are not available in your software:
select CTE1.CA,
CTE1.QTE,
CTE1.NB_TRANSACTION,
CTE2.NB_VOLS
from
(
select to_char(FLIGHT_DATE, 'MM-YYYY') as PERIOD,
sum(QUANTITY*UNIT_SALES_PRICE) as CA,
sum(QUANTITY) as QTE,
count(distinct TRANSACTION_NUMBER) as NB_TRANSACTION
from TB_DW_VAB_SALES
where SALES_TYPE = 'SALES'
group by to_char(FLIGHT_DATE, 'MM-YYYY')
) CTE1
inner join
(
select count(distinct ID_TEC_FLIGHT) as NB_VOLS,
to_char(FLIGHT_DATE, 'MM-YYYY') as PERIOD
from TB_DW_VAB_FLIGHT
group by to_char(FLIGHT_DATE, 'MM-YYYY')
) CTE2
on CTE1.PERIOD = CTE2.PERIOD
where CTE1.PERIOD = to_char(add_Months(sysdate,-1),'MM-YYYY')

Postgresql - can you do this without a CTE?

I wanted to get the number of orders and money spent by customers in the first 7 days from their initial order. I managed to do it with a common table expression, but was curious to see if someone could point out to an obvious update to the main query's WHERE or HAVING section, or perhaps a subquery.
--This is a temp table to use in the main query
WITH first_seven AS
(
select min(o.created_at), min(o.created_at) + INTERVAL '7 day' as max_order_date, o.user_id
from orders o
where o.total_price > 0 and o.status = 30
group by o.user_id
having min(o.created_at) > '2015-09-01'
)
--This is the main query, find orders in first 7 days of purchasing
SELECT sum(o.total_price) as sales, count(distinct o.objectid) as orders, o.user_id, min(o.created_at) as first_order
from orders o, first_seven f7
where o.user_id = f7.user_id and o.created_at < f7.max_order_date and o.total_price > 0 and o.status = 30
group by o.user_id
having min(o.created_at) > '2015-09-01'
You can do this without the join by using window functions:
select sum(o.total_price) as sales, count(distinct o.objectid) as orders,
o.user_id, min(o.created_at) as first_order
from (select o.*,
min(o.created_at) over (partition by user_id) as startdate
from orders o
where o.total_price > 0 and o.status = 30
) o
where startdate > '2015-09-01' and
created_at <= startdate + INTERVAL '7 day';
A more complicated query (with the right indexes) is probably more efficient:
select sum(o.total_price) as sales, count(distinct o.objectid) as orders,
o.user_id, min(o.created_at) as first_order
from (select o.*,
min(o.created_at) over (partition by user_id) as startdate
from orders o
where o.total_price > 0 and o.status = 30 and
not exists (select 1 from orders o2 where o2.user_id = o.user_id and created_at <= '2015-09-01')
) o
where startdate > '2015-09-01' and
created_at <= startdate + INTERVAL '7 day';
This filters out older customers before the windows calculation, which should make it more efficient. Indexes that are useful are orders(user_id, created_at) and orders(status, total_price).