Postgres FIFO query calculate profit margin - sql

I am working on my inventory system query to calculate profit based on FIFO (First-In-First-Out) in PostgreSQL (9.3+). Most of the replies are targeted for MS SQL Server so I am not sure how to go about it for PostgreSQL. I have tried using the Windows functions but am getting stuck at calculating the profit (I'm not sure if we need/can use cursors as I have not used them before)
Sales (negative quantity) are around (20*4 + 30*1) = 110
Cost of Goods sold based on FIFO are (5*2 + 10*2 + 10*1) = 40
Profit should be 110 - 40 = 70
I have till now managed to calculate running totals. Could someone help with this?
http://sqlfiddle.com/#!15/50b12/6
product_id product_name product_price purchase_date product_quantity
1 Notebook 5 2017-05-05 00:00:00 2
1 Notebook 10 2017-05-06 00:00:00 4
1 Notebook 15 2017-05-07 00:00:00 6
1 Notebook 20 2017-05-08 00:00:00 -4 (this is sale)
1 Notebook 30 2017-05-09 00:00:00 -1 (this is sale)
Desired results should display the Sales and profit margin. As long as I can get the profit margin it would fix my issue.

select *,
sum(price_sold - price_purchased) over(order by rn) as profit
from
(
select
row_number() over(order by purchase_date, product_id) as rn,
product_id, product_price as price_purchased
from inv_test, generate_series(1, abs(product_quantity))
where product_quantity > 0
) p
full join
(
select
row_number() over(order by purchase_date, product_id) as rn,
product_id, product_price as price_sold
from inv_test, generate_series(1, abs(product_quantity))
where product_quantity < 0
) s using (rn, product_id)
;
rn | product_id | price_purchased | price_sold | profit
----+------------+-----------------+------------+--------
1 | 1 | 5 | 20 | 15
2 | 1 | 5 | 20 | 30
3 | 1 | 10 | 20 | 40
4 | 1 | 10 | 20 | 50
5 | 1 | 10 | 30 | 70
6 | 1 | 10 | | 70
7 | 1 | 15 | | 70
8 | 1 | 15 | | 70
9 | 1 | 15 | | 70
10 | 1 | 15 | | 70
11 | 1 | 15 | | 70
12 | 1 | 15 | | 70

Related

count total items, sold items (in another table reference by id) and grouped by serial number

I have a table of items in the shop, an item may have different entries with same serial number (sn) (but different ids) if the same item was bought again later on with different price (price here is how much did a single item cost the shop)
id | sn | amount | price
----+------+--------+-------
1 | AP01 | 100 | 7
2 | AP01 | 50 | 8
3 | X2P0 | 200 | 12
4 | X2P0 | 30 | 18
5 | STT0 | 20 | 20
6 | PLX1 | 200 | 10
and a table of transactions
id | item_id | price
----+---------+-------
1 | 1 | 10
2 | 1 | 9
3 | 1 | 10
4 | 2 | 11
5 | 3 | 15
6 | 3 | 15
7 | 3 | 15
8 | 4 | 18
9 | 5 | 22
10 | 5 | 22
11 | 5 | 22
12 | 5 | 22
and transaction.item_id references items(id)
I want to group items by serial number (sn), get their sum(amount) and avg(price), and join it with a sold column that counts number of transactions with referenced id
I did the first with
select i.sn, sum(i.amount), avg(i.price) from items i group by i.sn;
sn | sum | avg
------+-----+---------------------
STT0 | 20 | 20.0000000000000000
PLX1 | 200 | 10.0000000000000000
AP01 | 150 | 7.5000000000000000
X2P0 | 230 | 15.0000000000000000
Then when I tried to join it with transactions I got strange results
select i.sn, sum(i.amount), avg(i.price) avg_cost, count(t.item_id) sold, sum(t.price) profit from items i left join transactions t on (i.id=t.item_id) group by i.sn;
sn | sum | avg_cost | sold | profit
------+-----+---------------------+------+--------
STT0 | 80 | 20.0000000000000000 | 4 | 88
PLX1 | 200 | 10.0000000000000000 | 0 | (null)
AP01 | 350 | 7.2500000000000000 | 4 | 40
X2P0 | 630 | 13.5000000000000000 | 4 | 63
As you can see, only the sold and profit columns show correct results, the sum and avg show different results than the expected
I can't separate the statements because I am not sure how can I add the count to the sn group which has the item_id as its id?
select
j.sn,
j.sum,
j.avg,
count(item_id)
from (
select
i.sn,
sum(i.amount),
avg(i.price)
from items i
group by i.sn
) j
left join transactions t
on (j.id???=t.item_id);
There are multiple matches in both tables, so the join multiplies the rows (and eventually produces wron results). I would recommend pre-joining, then aggregating:
select
sn,
sum(amount) total_amount,
avg(price) avg_price,
sum(no_transactions) no_transactions
from (
select
i.*,
(
select count(*)
from transactions t
where t.item_id = i.id
) no_transactions
from items i
) t
group by sn

Aggregate columns based on different conditions?

I have a Teradata query that generates:
customer | order | amount | days_ago
123 | 1 | 50 | 2
123 | 1 | 50 | 7
123 | 2 | 10 | 19
123 | 3 | 100 | 35
234 | 4 | 20 | 20
234 | 5 | 10 | 10
With performance in mind, what’s the most efficient way to produce an output per customer where orders is the number of distinct orders a customer had within the last 30 days and total is the sum of the amount of the distinct orders regardless of how many days ago the order was placed?
Desired output:
customer | orders | total
123 | 2 | 160
234 | 2 | 30
Given your rules, maybe it takes two steps - de-duplicate first then aggregate:
SELECT customer,
SUM(CASE WHEN days_ago <=30 THEN 1 ELSE 0 END) AS orders,
SUM(amount) AS total
FROM
(SELECT customer, order, MAX-or-MIN(amount) AS amount, MIN-or-MAX(days_ago) AS days_ago
FROM your_relation
GROUP BY 1, 2) AS DistinctCustOrder
GROUP BY 1;

Query to get the count of data for particular customer with all other data from table

My table structure is as follows:
group_id | cust_id | ticket_num
------------------------------
60 | 12 | 1
60 | 12 | 2
60 | 12 | 3
60 | 12 | 4
60 | 30 | 5
60 | 30 | 6
60 | 31 | 7
60 | 31 | 8
65 | 02 | 1
I want to fetch all the data for group_id=60 and find the count of ticket_num for each customer in that group. My output should be like this:
cust_id | ticket_count | ticket_num
------------------------------
12 | 4 | 1
12 | | 2
12 | | 3
12 | | 4
30 | 2 | 5
30 | | 6
31 | 2 | 7
31 | | 8
I tried this query:
SELECT gd.cust_id, Count(gd.cust_id),gd.ticket_num
FROM Group_details gd
WHERE gd.group_id = 65
GROUP BY gd.cust_id;
But this query is not working.
You appear to want the ANSI/ISO standard row_number() functions and count() as a window function:
select gd.cust_id, count(*) over (partition by gd.cust_id) as num_tickets,
row_number() over (order by gd.cust_id) as ticket_seqnum
from group_details gd
where gd.group_id = 60;
use aggregate and subquery
select t2.*,t1.ticket_num from Group_details t1
inner join
(
SELECT gd.cust_id, Count(gd.ticket_num) as ticket_count
FROM Group_details gd where gd.group_id = 60
GROUP BY gd.cust_id
) t2 on t1.cust_id=t2.cust_id
http://sqlfiddle.com/#!9/dd718b/1

oracle query alternating records

I have a table that looks like this:
SEQ TICKER INDUSTRY
1 AAPL 10
1 FB 10
1 IBM 10
1 CSCO 10
1 FEYE 20
1 F 20
2 JNJ 10
2 CMPQ 10
2 CYBR 10
2 PFPT 10
2 K 20
2 PANW 20
What I need is record with the same industry code, to alternate between the 1 & 2 records like this:
1 AAPL 10
2 IBM 10
1 FB 10
2 CSCO 10
1 FEYE 20
2 PANW 20
So basically, grouped by the same industry code, alternate between the 1 & 2 records.
Can't figure out how.
Use an analytic function to create a row number that starts over for each group (industry and sequence), then sort by that row number.
select seq, ticker, industry
,row_number() over (partition by industry, seq order by ticker)custom_order
from stocks
order by industry, custom_order, seq;
See this SQL Fiddle for a full example. (It doesn't perfectly match your example results but either your example results are incorrect or there's something else to this question I don't understand.)
Don't see how you arrived at the example result in your question, but this result:
| SEQ | TICKER | INDUSTRY |
|-----|--------|----------|
| 1 | AAPL | 10 |
| 2 | CMPQ | 10 |
| 1 | CSCO | 10 |
| 2 | CYBR | 10 |
| 1 | FB | 10 |
| 2 | IBM | 10 |
| 1 | JNJ | 10 |
| 2 | PFPT | 10 |
| 1 | F | 20 |
| 2 | FEYE | 20 |
| 1 | K | 20 |
| 2 | PANW | 20 |
Was produced using this query, where (I assume) you want the SEQ column calculated for you:
select
1 + mod(rn,2) Seq
, ticker
, industry
from (
select
ticker
, industry
, 1+ row_number() over (partition by industry
order by ticker) rn
from stocks
)
order by industry, rn
Please note this is a derivative of the earlier answer by Jon Heller, this derivative can be found online at http://sqlfiddle.com/#!4/088271/1

Return all records if more than 2/3 satisfy a value

I have a table representing multiple transactions by customers in any given day. I need to return all transactions per customer if two thirds or more of the transactions per customer were cash instead of credit card.
In the example below I want to return all of customers' 1, 4 transactions as they were the only customers to have 2 thirds or more of their transactions as cash:
+----------------+-------------+-----------------+------------------+
| Transaction ID | CustomerNum | TransactionType | TransactionValue |
+----------------+-------------+-----------------+------------------+
| 1 | 1 | Cash | 11 |
| 2 | 1 | Card | 12 |
| 3 | 1 | Cash | 13 |
| 4 | 2 | Cash | 14 |
| 5 | 2 | Card | 15 |
| 6 | 3 | Cash | 15 |
| 7 | 3 | Card | 11 |
| 8 | 3 | Cash | 12 |
| 9 | 3 | Card | 13 |
| 10 | 4 | Cash | 14 |
| 11 | 4 | Cash | 15 |
| 12 | 4 | Cash | 15 |
+----------------+-------------+-----------------+------------------+
This seems to work with the sample data:
declare #t table (TranID int not null,CustomerNum int not null,
TranType varchar(17) not null,TranValue decimal(18,0) not null)
insert into #t(TranID,CustomerNum,TranType,TranValue) values
( 1,1,'Cash',11), ( 2,1,'Card',12), ( 3,1,'Cash',13),
( 4,2,'Cash',14), ( 5,2,'Card',15),
( 6,3,'Cash',15), ( 7,3,'Card',11), ( 8,3,'Cash',12), ( 9,3,'Card',13),
(10,4,'Cash',14), (11,4,'Cash',15), (12,4,'Cash',15)
;With Counted as (
select *,
COUNT(*) OVER (PARTITION BY CustomerNum) as cnt,
SUM(CASE WHEN TranType='Cash' THEN 1 ELSE 0 END)
OVER (PARTITION BY CustomerNum) as cashcnt
from #t
)
select * from Counted
where cashcnt * 3 >= cnt * 2
I've gone with simple multiplication at the end to keep all of the maths as integers and avoid having to think about float/decimal and the representation of 2/3.
Result:
TranID CustomerNum TranType TranValue cnt cashcnt
----------- ----------- ----------------- ----------- ----------- -----------
1 1 Cash 11 3 2
2 1 Card 12 3 2
3 1 Cash 13 3 2
10 4 Cash 14 3 3
11 4 Cash 15 3 3
12 4 Cash 15 3 3
Try this:
select t.*
from (select customernum
from transactions
group by customernum
having sum(case when TransactionType = 'Cash' then 1.0 else 0.0 end) / sum(1.0) > 0.6666) c
join transactions t on t.customernum = c.customernum