Divide a number into multiple bands - sql

I've been dealing with a problem of splitting a metric into several bands. To give you some context, let's take this example where we have certain number of orders per customer. Now, a customer may order n number of products. Let's give the customer certain discount based on the number of orders. The discounts are offered based a tiered model. I'm leaving out multiple product categories to keep it simple. Here are some examples of the tables.
Orders table
Customer | order_no
----------------------------
Customer1 | 400
Customer2 | 1200
Customer3 | 40
Customer4 | 2000
Customer5 | 700
Tiered pricing table
Tier | lower_th | higer_th | price |
--------------------------------------
Tier1 | 0 | 250 | 50 |
TIer2 | 251 | 500 | 45 |
Tier3 | 501 | 1000 | 40 |
TIer4 | 1001 | 10000 | 30 |
Example1: I want to be able to charge Customer1 $50 for 250 order and $45 for the rest of 150 products out of a total of 400.
Example2: I want to be able to charge Customer5 $50 for 250 order and $45 for another 250 and $40 for the rest 200 products out of a total of 700.
How do I achieve this in PostgreSQL? My output needs to be the following for Customer1. What's the best way to split the total number of orders and join it to the pricing tiers to get the corresponding amount?
Customer | order_no | charges |
--------------------------------
Customer1 | 250 | 50 |
Customer1 | 150 | 45 |

You can think of your tiers as intervals.
Two intervals [a1, b1] and [a2, b2] intersect when
a1 <= b2 AND b1 >= a2
The number of orders is another interval that always starts at 1.
Your two intervals are: Tiers [lower_th, higer_th] and Orders [1, order_no].
The query is a simple join using this intersection expression:
SELECT *
,CASE WHEN O.order_no > T.higer_th
THEN T.higer_th - T.lower_th + 1 -- full tier
ELSE O.order_no - T.lower_th + 1
END AS SplitOrderNumbers
FROM
Orders AS O
INNER JOIN Tiers AS T
-- ON 1 <= T.higer_th AND O.order_no >= T.lower_th
ON O.order_no >= T.lower_th
ORDER BY
O.Customer
,T.lower_th
;
You don't really need the 1 <= T.higer_th part, because it is always true, so the expression becomes simple O.order_no >= T.lower_th.
Also, usually it is better to store intervals as [closed; open). It usually simplifies arithmetic, similar to why most programming languages have array indexes starting at 0, not 1. Your intervals seem to be [closed; closed]. In this case you need to set lower_th to 1, not 0 and have +1 in the calculations.
With this adjustment of the sample data this query produces the following result:
+-----------+----------+-------+----------+----------+-------+-------------------+
| Customer | order_no | Tier | lower_th | higer_th | price | SplitOrderNumbers |
+-----------+----------+-------+----------+----------+-------+-------------------+
| Customer1 | 400 | Tier1 | 1 | 250 | 50.00 | 250 |
| Customer1 | 400 | Tier2 | 251 | 500 | 45.00 | 150 |
| Customer2 | 1200 | Tier1 | 1 | 250 | 50.00 | 250 |
| Customer2 | 1200 | Tier2 | 251 | 500 | 45.00 | 250 |
| Customer2 | 1200 | Tier3 | 501 | 1000 | 40.00 | 500 |
| Customer2 | 1200 | Tier4 | 1001 | 10000 | 30.00 | 200 |
| Customer3 | 40 | Tier1 | 1 | 250 | 50.00 | 40 |
| Customer4 | 2000 | Tier1 | 1 | 250 | 50.00 | 250 |
| Customer4 | 2000 | Tier2 | 251 | 500 | 45.00 | 250 |
| Customer4 | 2000 | Tier3 | 501 | 1000 | 40.00 | 500 |
| Customer4 | 2000 | Tier4 | 1001 | 10000 | 30.00 | 1000 |
| Customer5 | 700 | Tier1 | 1 | 250 | 50.00 | 250 |
| Customer5 | 700 | Tier2 | 251 | 500 | 45.00 | 250 |
| Customer5 | 700 | Tier3 | 501 | 1000 | 40.00 | 200 |
+-----------+----------+-------+----------+----------+-------+-------------------+

For pricing data I would use a table like this to make data maintenance easier
create table pricing_data
(
high_limit int,
price numeric
);
A view will give you the intervals you need for this using a window function:
create view pricing as
select coalesce(lag(high_limit) over (order by high_limit), 0) as last_limit,
high_limit, price
from pricing_data;
This simplifies the breakout into pricing tiers:
select o.customer,
least(o.order_no - p.last_limit, p.high_limit - p.last_limit) as order_no,
p.price as charges
from orders o
join pricing p on p.last_limit < o.order_no
order by o.customer, p.price desc
;
Result:
customer order_no charges
Customer1 250 50
Customer1 150 45
Customer2 250 50
Customer2 250 45
Customer2 500 40
Customer2 200 30
Customer3 40 50
Customer4 250 50
Customer4 250 45
Customer4 500 40
Customer4 1000 30
Customer5 250 50
Customer5 250 45
Customer5 200 40
14 rows
Fiddle here.

Related

SUB TOTAL WITH MULTIPLE TABLE

I want to show the subtotals of each product name and maybe the commands I made are useless,
is there any suggestions for me
I have 3 tables
select
ORDERS.ORDER_NUM,
PRODUCT.PRODUCT_ID,
PRODUCT.PRODUCT_NAME,
ORDER_DETAILS.QUANTITY,
ORDER_DETAILS.UNIT_PRICE,
sum(ORDER_DETAILS.UNIT_PRICE) as SUBTOTAL
from
ORDER_DETAILS
inner join ORDERS on ORDER_DETAILS.ORDER_NUM = ORDERS.ORDER_NUM
inner join PRODUCT on ORDER_DETAILS.PRODUCT_ID = PRODUCT.PRODUCT_ID
group by
ORDER_DETAILS.ITEM_NUM
order by
PRODUCT.PRODUCT_NAME;
+-----------+------------+--------------+----------+------------+----------+
| ORDER_NUM | PRODUCT_ID | PRODUCT_NAME | QUANTITY | UNIT_PRICE | SUBTOTAL |
+-----------+------------+--------------+----------+------------+----------+
| 3004 | 2001 | BEER | 10 | 160,000 | 160 |
| 3012 | 2001 | BEER | 5 | 160,000 | 160 |
| 3002 | 2005 | CAKE | 5 | 50,000 | 50 |
| 3001 | 2004 | CIGARETTE | 2 | 25,000 | 25 |
| 3011 | 2004 | CIGARETTE | 5 | 25,000 | 25 |
| 3005 | 2007 | ICE CREAM | 50 | 10,000 | 10 |
| 3007 | 2010 | MILK | 3 | 45,000 | 45 |
| 3010 | 2010 | MILK | 7 | 12,000 | 12 |
| 3008 | 2008 | NOODLES | 1 | 5,000 | 5 |
| 3013 | 2006 | SODA | 50 | 12,000 | 12 |
| 3009 | 2002 | WINE | 1 | 200,000 | 200 |
| 3003 | 2002 | WINE | 2 | 200,000 | 200 |
| 3006 | 2002 | WINE | 1 | 200,000 | 200 |
+-----------+------------+--------------+----------+------------+----------+
You don't specify the database. However, you need to fix your query:
Details from the order and order line are not appropriate, if you are summarizing by product.
The SELECT and GROUP BY lists should be consistent.
The total you want is probably the price times the quantity.
So, the query should be more like this:
select p.PRODUCT_ID, p.PRODUCT_NAME,
sum(od.UNIT_PRICE * od.QUANTITY) as SUBTOTAL
from ORDER_DETAILS od join
ORDERS o
on od.ORDER_NUM = o.ORDER_NUM join
PRODUCT p
on od.PRODUCT_ID = p.PRODUCT_ID
group by p.PRODUCT_ID, p.PRODUCT_NAME
order by p.PRODUCT_NAME;
Also note the use of table aliases, so the query is easier to write and to read.

How to write an SQL report using distinct and sum

I'm trying to write an SQL report that groups rows, removes duplicates, and sums up values in virtual columns.
I have this table
make | model | warranty | price
-------+--------+----------+-------
Honda | Accord | 2 | 700
Honda | Civic | 3 | 500
Lexus | ES 350 | 1 | 900
Lexus | ES 350 | 1 | 900
Lexus | ES 350 | 2 | 1300
Lexus | ES 350 | 3 | 1800
(6 rows)
I'm trying to create a report that adds two virtual columns, qty and total. Total is the sum of qty * price. The table should like the one below.
qty | make | model | warranty | price | total
-------+--------+--------+----------+-------------
1 | Honda | Accord | 2 | 700 | 700
1 | Honda | Civic | 3 | 500 | 500
2 | Lexus | ES 350 | 1 | 900 | 1800
1 | Lexus | ES 350 | 2 | 1300 | 1300
1 | Lexus | ES 350 | 3 | 1800 | 1800
(5 rows)
I think this is simple aggregation:
select count(*) as qty, make, model, warranty,
avg(price) as price, sum(price) as total
from t
group by make, model, warranty;

DB2 SQL, Calculate total on orders ready to ship

I'm trying to calculate the total value or all orders where we have all items in stock required to fill the order. In the example below, I want to select only the total value of order 100 only, since there is not enough inventory to fill order 200.
+-------+------+-------------+--------------+-------+
| Order | Item | Qty Ordered | Qty In Stock | Price |
+-------+------+-------------+--------------+-------+
| 100 | A | 10 | 25 | 1.00 |
+-------+------+-------------+--------------+-------+
| 100 | B | 15 | 50 | 2.00 |
+-------+------+-------------+--------------+-------+
| 100 | C | 30 | 75 | 3.00 |
+-------+------+-------------+--------------+-------+
| 200 | A | 5 | 25 | 1.00 |
+-------+------+-------------+--------------+-------+
| 200 | B | 100 | 50 | 2.00 | * Not enough stock to fill
+-------+------+-------------+--------------+-------+
| 200 | C | 35 | 75 | 3.00 |
+-------+------+-------------+--------------+-------+
How about:
select o.id, sum(o.qty_ordered * o.price) as total_value
from orders o
where o.id not in (
select id from orders where qty_ordered > qty_in_stock
)
group by o.id

query to match a value in multiple ranges

I have a table as below:
promo_code | minimum_order | discount
------------+-------------------+-------------
100 | 10 | 10%
100 | 20 | 15%
100 | 30 | 20%
101 | 13 | 7%
102 | 8 | 10%
102 | 12 | 14%
In another table I have the sales in quantity and the promotions they are eligible
sales | promo_eligibility | record_id
--------+-----------------------+-------------
14 | 100 | 1000
7 | 101 | 1001
25 | 102 | 1002
I need to get the discount that correspond to the sales volume and promotion...
as in the above examples:
record_id | discount | Comments
------------+---------------+--------------
1000 | 15% | (bigger than 10 and lower than 20)
1001 | 0% | (did not reached the minimum)
1002 | 14% |
The number of thresholds could vary from 0 to 3
Any ideas?
Thanks!!!
Something like this:
select
r.record_id,
isnull(d.discount, '0%')
from records r
outer apply (
select top 1 discount
from discounts d
where d.promo_code = r.promo_eligibility
and d.minimum_order <= r.sales)

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;