Return stock in a warehouse even if there is no row for that given stock - sql

I have 5 different tables:
Toasters: product name (foreign key to products and primary key), slots, serial
Microwaves: product name (same as toaster), wattage
Products: product name (primary key)
Stock: product (fk to product), warehouse (fk to warehouse), amount
Warehouse: name (primary key)
toasters and microwaves are child tables of products (although its not using postgres inheritance, since there are issues with it). They represent different models of toasters (simplified to just slots and wattage here). Every toaster and microwave has exactly 1 entry in the products table.
Now the goal is to create a query that essentially gives me an amount of all products across all warehouses for a given list of product names. The problem is, that some warehouses may not have a stock entry for a certain product. They also have either one stock per product or none.
I have managed to make it work for a single warehouse:
--join together all 3 product tables and select all desired products
WITH selIProducts AS(
SELECT
--Get the products category by checking if the table is part of the query
(CASE
WHEN toasters IS NOT NULL THEN 'toasters'
WHEN microwaves IS NOT NULL THEN 'microwaves'
ELSE 'ERROR'
END) as category,
products.name as productName,
*
FROM products
--I need a full join to include everything
FULL JOIN toasters ON toasters.name=products.name
FULL JOIN microwaves ON microwaves.name=products.name
WHERE
products.name IN (
'TOASTMASTER 3000',
'TOASTMASTER 3000Rev01',
'A3452 Ultra Microwave Oven',
)
),
warehouseStock AS
(
--only works with one inventory
SELECT * FROM STOCK
WHERE stock.warehouse='WH-1'
)
-- left join to ensure all item categories are included
SELECT COALESCE(warehouseStock.amount,0) as amount,* FROM selProducts
LEFT JOIN warehouseStock ON selIProducts.itemId=warehouseStock.item
It tried replacing WHERE stock.warehouse='WH-1' with WHERE stock.warehouse IN ('WH-1','WH-2') but that doesn't work since the desired product types are only joined once, instead of once per warehouse.
The final result should look like this:
Warehouse productName amount wattage slots category
WH-1 TOASTMASTER 3000 0 null 2 toasters
WH-1 TOASTMASTER 3000Rev01 1 null 3 toasters
WH-1 A3452 Ultra Microwave Oven 1 3000 null microwave
WH-2 TOASTMASTER 3000 2 null 2 toasters
WH-2 TOASTMASTER 3000Rev01 0 null 3 toasters
WH-2 A3452 Ultra Microwave Oven 0 3000 null microwave
I don't know how I am I should get postgres to return a null when there isn't a stock in a given warehouse.
Does anybody have any ideas?

You seem to want all products and all warehouses. That suggests a cross join to generate the rows:
SELECT v.warehouse, p.productname,
COALESCE(s.amount, 0) as amount
FROM selProducts p CROSS JOIN
(VALUES ('WH-1'), ('WH-2')) v(warehouse) LEFT JOIN
stock s
ON p.itemId = s.item AND v.warehouse = s.warehouse;
You might have another source for the warehouses, if you don't want to list them explicitly.

Add a table of warehouses wanted.
WITH selIProducts AS(
SELECT
--Get the products category by checking if the table is part of the query
(CASE
WHEN toasters IS NOT NULL THEN 'toasters'
WHEN microwaves IS NOT NULL THEN 'microwaves'
ELSE 'ERROR'
END) as category,
products.name as productName,
*
FROM products
--I need a full join to include everything
FULL JOIN toasters ON toasters.name=products.name
FULL JOIN microwaves ON microwaves.name=products.name
WHERE
products.name IN (
'TOASTMASTER 3000',
'TOASTMASTER 3000Rev01',
'A3452 Ultra Microwave Oven',
)
),
warehousesWanted AS
(
SELECT *
FROM Warehouse
WHERE name in ('WH-1', 'WH-2')
)
-- left join to ensure all item categories are included
SELECT COALESCE(warehouseStock.amount,0) as amount, *
FROM selIProducts sp
CROSS JOIN warehousesWanted ww
LEFT JOIN Stock ON Stock.itemId = sp.itemId
and ww.Name = Stock.Warehouse;
You may need to correct ON clause as I'm not sure what are proper column names of your real tables.

Related

LEFT OUTER JOIN with 'field IS NULL' in WHERE works as INNER JOIN

Today I've faced some unexplainable (for me) behavior in PostgreSQL — LEFT OUTER JOIN does not return records for main table (with nulls for joined one fields) in case the joined table fields are used in WHERE expression.
To make it easier to grasp the case details, I'll provide an example. So, let's say we have 2 tables: item with some goods, and price, referring item, with prices for the goods in different years:
CREATE TABLE item(
id INTEGER PRIMARY KEY,
name VARCHAR(50)
);
CREATE TABLE price(
id INTEGER PRIMARY KEY,
item_id INTEGER NOT NULL,
year INTEGER NOT NULL,
value INTEGER NOT NULL,
CONSTRAINT goods_fk FOREIGN KEY (item_id) REFERENCES item(id)
);
The table item has 2 records (TV set and VCR items), and the table price has 3 records, a price for TV set in years 2000 and 2010, and a price for VCR for year 2000 only:
INSERT INTO item(id, name)
VALUES
(1, 'TV set'),
(2, 'VCR');
INSERT INTO price(id, item_id, year, value)
VALUES
(1, 1, 2000, 290),
(2, 1, 2010, 270),
(3, 2, 2000, 770);
-- no price of VCR for 2010
Now let's make a LEFT OUTER JOIN query, to get prices for all items for year 2010:
SELECT
i.*,
p.year,
p.value
FROM item i
LEFT OUTER JOIN price p ON i.id = p.item_id
WHERE p.year = 2010 OR p.year IS NULL;
For some reason, this query will return a results only for TV set, which has a price for this year. Record for VCR is absent in results:
id | name | year | value
----+--------+------+-------
1 | TV set | 2010 | 270
(1 row)
After some experimenting, I've found a way to make the query to return results I need (all records for item table, with nulls in the fields of joined table in case there are no mathing records for the year. It was achieved by moving year filtering into a JOIN condition:
SELECT
i.*,
p.year,
p.value
FROM item i
LEFT OUTER JOIN (
SELECT * FROM price
WHERE year = 2010 -- <= here I filter a year
) p ON i.id = p.item_id;
And now the result is:
id | name | year | value
----+--------+------+-------
1 | TV set | 2010 | 270
2 | VCR | |
(2 rows)
My main question is — why the first query (with year filtering in WHERE) does not work as expected, and turns instead into something like INNER JOIN?
I'm severely blocked by this issue on my current project, so I'll be thankful about tips/hints on the next related questions too:
Are there any other options to achieve the proper results?
... especially — easily translatable to Django's ORM queryset?
Upd: #astentx suggested to move filtering condition directly into JOIN (and it works too):
SELECT
i.*,
p.year,
p.value
FROM item i
LEFT OUTER JOIN price p
ON
i.id = p.item_id
AND p.year = 2010;
Though, the same as my first solution, I don't see how to express it in terms of Django ORM querysets. Are there any other suggestions?
The first query does not work as expected because expectation is wrong. It does not work as INNER JOIN as well. The query returns a record for VCR only if there is no price for VCR at all.
SELECT
i.*,
y.year,
p.value
FROM item i
CROSS JOIN (SELECT 2010 AS year) y -- here could be a table
LEFT OUTER JOIN price p
ON (p.item_id = i.id
AND p.year = y.year);

Create row if column does not consist ID from another column

I have database which contain three tables product, product_size (variations) and handbook. Product contains all data about the product, handbook knows which size I have and product_size contain information about products which have size(s) from handbook and also contain size_value.
So I need to insert values in column product_size where size_price will contain price of product, handbook_id = 666 and product_id is ID of products which doesn't have product size. Is there any way to do that?
Product
id
price
name
Product_Size
id
product_id
handbook_id
size_price
Handbook
id
name
All products would mean rows from product table. So size may not be in handbook or may not be in product_size table.
Select p.name
From product p left join
Product_Size ps
On p.id= ps.product_id
Left join Handbook h
On h.id=ps.handbook_id
Where handbook_id is null
You can phrase the insert like this:
insert into product_size (product_id, handbook_id, size_price)
select p.product_id, 666, p.price
from product p
where not exists (select 1
from product_size ps
where ps.product_id = p.product_id and ps.handbook_id = 666
);
Note: This can produce unexpected results if you attempt to run two of these inserts at the same time. To protect your data from duplicates, you should have a unique constraint/index on product_size(product_id, handbook_id).

How to get record not joined result PostgreSQL

I have
table customer {ID(primary key-autoincrement),CUSTOMERNAME}
table item {ID (primary Key-autoincrement),PRODUCT,PRICE}
table price {ID (primary key-autoincrement),CUSTOMERID (foreign key),ITEMID (foreign-key),PRICE}
Now I am filling data like this:
table customer : {1,ABC},{2,DEF}
table item :{1,EGG,50},{2,BRUSH,100},{3,SHOES,290},{4,SOCKS,120},{5,PILLOW,200}
table price :{1,1,3,320}
If I get the price for customer ABC (customer ID :1), then will be displayed result like this (5 records):
{1,1,EGG,50},{2,1,BRUSH,100},{3,1,SHOES,320},{4,1,SOCKS,120},{5,1,PILLOW,200}
But if I want to display price for customer DEF, then only displayed 4 records:
{1,1,EGG,50},{2,1,BRUSH,100},{4,1,SOCKS,120},{5,1,PILLOW,200}
How can I display the price for item with item ID (3) with default price 290 for customer DEF?
I use left outer join for this case then takes record with joined, then for second case they only returned 4 records.
Here my query (returned 4 records):
SELECT TMP."ID",TMP."IDCUSTOMER",TMI."NAME",
CASE WHEN TMP."PRICE" IS NULL THEN TMI."PRICE" ELSE TMP."PRICE" END AS "CUSTOMPRICE"
FROM mitem TMI
LEFT OUTER JOIN mprice TMP ON TMP."IDITEM"=TMI."ID"
LEFT OUTER JOIN mcustomer TMC ON TMC."ID"=TMP."IDCUSTOMER"
WHERE TMP."IDCUSTOMER"='2' OR TMP."ID" IS NULL
Any idea beside without using UNION to join result?
Use CROSS JOIN.
Like this:
SELECT TMP."ID",TMP."IDCUSTOMER",TMI."NAME",
CASE WHEN TMP."PRICE" IS NULL THEN TMI."PRICE" ELSE TMP."PRICE" END AS "CUSTOMPRICE"
FROM mitem TMI
CROSS JOIN mcustomer TMC
LEFT OUTER JOIN mprice TMP ON TMP."IDITEM"=TMI."ID"
AND TMP."IDCUSTOMER"= TMC."ID" AND TMP."IDCUSTOMER"= '2'
WHERE TMC."ID" = '2'

Complex SQL Query - Joining 5 tables with complex conditions

I have the following tables: Reservations, Order-Lines, Order-Header, Product, Customer. Just a little explanation on each of these tables:
Reservations Contains "reservations" for a billing customer/product combination.
Order-Lines Contains line item detail for orders, including the product they ordered and the qty.
Order-Header Contains header info for orders including the date, customer and billing customer
Product Contains product detail information
Customer Contains Customer detail information.
Below are the tables with their associated fields and sample data:
Reservation
bill-cust-key prod-key qty-reserved reserve-date
10000 20000 10 05/30/2014
10003 20000 5 06/20/2014
10003 20001 15 06/20/2014
10003 20001 5 06/25/2014
10002 20001 5 06/21/2014
10002 20002 20 06/21/2014
Order-Item
order-num cust-key prod-key qty-ordered
30000 10000 20000 10
30000 10000 20001 5
30001 10001 20001 10
30002 10001 20001 5
30003 10002 20003 20
Order-Header
order-num cust-key bill-cust-key order-date
30000 10000 10000 07/01/2014
30001 10001 10003 07/03/2014
30002 10001 10003 07/15/2014
30003 10002 10002 07/20/2014
Customer
cust-key cust-name
10000 Customer A
10001 Customer B
10002 Customer C
10003 Customer D
Product
prod-key prod-name
20000 Prod A
20001 Prod B
20002 Prod C
20003 Prod D
I am attempting to write a query that will show me customer/product combinations that exist in both the reservation and order-item tables. A little snafu is that we have a customer and a billing customer. The reservation and order-header tables contain both the customers, but the order-item table only contains the customer. The results should display the billing customer. Additionally, there can be several reservations and order-items for the same customer/product combination, so I would like to show a total sum of the qty-reserved and the qty-ordered.
Below is an example of my desired output:
bill-cust-key cust-name prod-key prod-name qty-ordered qty-reserved
10000 Customer A 20000 Prod A 10 10
10003 Customer D 20001 Prod B 15 20
This is the query that I have tried and doesn't seem to be working for me.
SELECT customer.cust-key, customer.cust-name, product.prod-key, prod.prod-name,
SUM(order-item.qty-ordered), SUM(reservation.qty-reserved)
FROM ((reservation INNER JOIN order-item on reservation.prod-key = order-item.product-key)
INNER JOIN order-header on reservation.bill-cust-key = order-header.bill-cust-key and
order-item.order-num = order-header.order-num), customer, product
WHERE customer.cust-key = reservation.bill-cust-key
AND product.prod-key = reservation.prod-key
GROUP BY customer.cust-key, customer.cust-name, product.prod-key, product.prod-name
I'm sorry for such a long post! I just wanted to make sure that I had my bases covered!
You want to join your tables like this:
from reservation res join order-header oh on res.bill-cust-key = oh.bill-cust-key
join order-item oi on oi.order-num = oh.order-num
and oi.prod-key = res.prod-key
/* join customer c on c.cust-key = oi.cust-key old one */
join customer c on c.cust-key = oh.bill-cust-key
join product p on p.prod-key = oi.prod-key
I find that it can be very helpful to separate out your output rows from your aggregate rows by using CROSS APPLY (or OUTER APPLY) or simply an aliased inner query if you don't have access to those.
For example,
SELECT
customer.cust-key,
customer.cust-name,
tDetails.prod-key,
tDetails.prod-name,
tDetails.qty-ordered,
tDetails.qty-reserved
FROM customer
--note that this could be an inner-select table in which you join if not cross-join
CROSS APPLY (
SELECT
product.prod-key,
prod.prod-name,
SUM(order-item.qty-ordered) as qty-ordered,
SUM(reservation.qty-reserved) as qty-reserved
FROM reservation
INNER JOIN order-item ON reservation.prod-key = order-item.product-key
INNER JOIN product ON reservation.prod-key = product.prod-key
WHERE
reservation.bill-cust-key = customer.cut-key
GROUP BY product.prod-key, prod.prod-name
) tDetails
There are many ways to slice this, but you started out the right way saying "what recordset do I want returned". I like the above because it helps me visualize what each 'query' is doing. The inner query marked by the CROSS apply is simply grouping by prod orders and reservations but is filtering by the current customer in the outer top-most query.
Also, I would keep joins out of the 'WHERE' clause. Use the 'WHERE' clause for non-primary key filtering (e.g. cust-name = 'Bob'). I find it helps to say that one is a table join, the 'WHERE' clause is a property filter.
TAKE 2 - using inline queries
This approach still tries to get a list of customers with distinct products, and then uses that data to form the outer query from which you can get aggregates.
SELECT
customer.cust-key,
customer.cust-name,
products.prod-key,
products.prod-name,
--aggregate for orders
( SELECT SUM(order-item.qty-ordered)
FROM order-item
WHERE
order-item.cust-key = customer.cust-key AND
order-item.prod-key = products.prod-key) AS qty-ordered,
--aggregate for reservations
( SELECT SUM(reservation.qty-reserved)
FROM reservations
--join up billingcustomers if they are different from customers here
WHERE
reservations.bill-cust-key = customer.cust-key AND
reservations.prod-key = products.prod-key) AS qty-reserved
FROM customer
--get a table of distinct products across orders and reservations
--join products table for name
CROSS JOIN (
SELECT DISTINCT order-item.prod-key FROM order-item
UNION
SELECT DISTINCT reservation.prod-key FROM reservations
) tDistinctProducts
INNER JOIN products ON products.prod-key = tDistinctProducts.prod-key
TAKE 3 - Derived Tables
According to some quick googling, Progress DB does support derived tables. This approach has largely been replaced with CROSS APPLY (or OUTER APPLY) because you don't need to do the grouping. However, if your db only supports this way then so be it.
SELECT
customer.cust-key,
customer.cust-name,
products.prod-key,
products.prod-name,
tOrderItems.SumQtyOrdered,
tReservations.SumQtyReserved
FROM customer
--get a table of distinct products across orders and reservations
--join products table for name
CROSS JOIN (
SELECT DISTINCT order-item.prod-key FROM order-item
UNION
SELECT DISTINCT reservation.prod-key FROM reservations
) tDistinctProducts
INNER JOIN products ON products.prod-key = tDistinctProducts.prod-key
--derived table for order-items
LEFT OUTER JOIN ( SELECT
order-item.cust-key,
order-item.prod-key,
SUM(order-item.qty-ordered) AS SumQtyOrdered
FROM order-item
GROUP BY
order-item.cust-key,
order-item.prod-key) tOrderItems ON
tOrderItems.cust-key = customer.cust-key AND
tOrderItems.prod-key = products.prod-key
--derived table for reservations
LEFT OUTER JOIN ( SELECT
reservations.bill-cust-key,
reservations.prod-key,
SUM(reservations.qty-reserved) AS SumQtyReserved
FROM reservations
--join up billingcustomers if they are different from customers here
WHERE
reservations.bill-cust-key = customer.cust-key AND
reservations.prod-key = products.prod-key) tReservations ON
tReservations.bill-cust-key = customer.cust-key AND
tReservations.prod-key = products.prod-key
Based on your original code and request, here's the starting point of a Progress solution -
DEFINE VARIABLE iQtyOrd AS INTEGER NO-UNDO.
DEFINE VARIABLE iQtyReserved AS INTEGER NO-UNDO.
FOR EACH order-item
NO-LOCK,
EACH order-header
WHERE order-header.order-num = order-item.order-num
NO-LOCK,
EACH reservation
WHERE reservation.prod-key = order-item.prod-key AND
reservation.bill-cust-key = order-header.bill-cust-key
NO-LOCK,
EACH product
WHERE product.prod-key = reservation.prod-key
NO-LOCK,
EACH customer
WHERE customer.cust-key = reservation.bill-cust-key
NO-LOCK
BREAK BY customer.cust-key
BY product.prod-key
BY product.prod-name
:
IF FIRST-OF(customer.cust-key) OR FIRST-OF(product.prod-key) THEN
ASSIGN
iQtyOrd = 0
iQtyReserved = 0
.
ASSIGN
iQtyOrd = iQtyOrd + reservation.qty-ordered
iQtyReserved = iQtyReserved + reservation.qty-reserved
.
IF LAST-OF(customer.cust-key) OR LAST-OF(product.prod-key) THEN
DISPLAY
customer.cust-key
customer.cust-name
product.prod-key
prod.prod-name
iQtyOrd
iQtyReserved
WITH FRAME f-qty
DOWN
.
END.

How to ensure outer join with filter still returns all desired rows?

Imagine I have two tables in a DB like so:
products:
product_id name
----------------
1 Hat
2 Gloves
3 Shoes
sales:
product_id store_id sales
----------------------------
1 1 20
2 2 10
Now I want to do a query to list ALL products, and their sales, for store_id = 1. My first crack at it would be to use a left join, and filter to the store_id I want, or a null store_id, in case the product didn't get any sales at store_id = 1, since I want all the products listed:
SELECT name, coalesce(sales, 0)
FROM products p
LEFT JOIN sales s ON p.product_id = s.product_id
WHERE store_id = 1 or store_id is null;
Of course, this doesn't work as intended, instead I get:
name sales
---------------
Hat 20
Shoes 0
No Gloves! This is because Gloves did get sales, just not at store_id = 1, so the WHERE clause has filtered them out.
How then can I get a list of ALL products and their sales for a specific store?
Here are some queries to create the test tables:
create temp table test_products as
select 1 as product_id, 'Hat' as name;
insert into test_products values (2, 'Gloves');
insert into test_products values (3, 'Shoes');
create temp table test_sales as
select 1 as product_id, 1 as store_id, 20 as sales;
insert into test_sales values (2, 2, 10);
UPDATE: I should note that I am aware of this solution:
SELECT name, case when store_id = 1 then sales else 0 end as sales
FROM test_products p
LEFT JOIN test_sales s ON p.product_id = s.product_id;
however, it is not ideal... in reality I need to create this query for a BI tool in such a way that the tool can simply add a where clause to the query and get the desired results. Inserting the required store_id into the correct place in this query is not supported by this tool. So I'm looking for other options, if there are any.
Add the WHERE condition to the LEFT JOIN clause to prevent that rows go missing.
SELECT p.name, coalesce(s.sales, 0)
FROM products p
LEFT JOIN sales s ON p.product_id = s.product_id
AND s.store_id = 1;
Edit for additional request:
I assume you can manipulate the SELECT items? Then this should do the job:
SELECT p.name
,CASE WHEN s.store_id = 1 THEN coalesce(s.sales, 0) ELSE NULL END AS sales
FROM products p
LEFT JOIN sales s USING (product_id)
Also simplified the join syntax in this case.
I'm not near SQL, but give this a shot:
SELECT name, coalesce(sales, 0)
FROM products p
LEFT JOIN sales s ON p.product_id = s.product_id AND store_id = 1
You don't want a where on the whole query, just on your join