SQL to provide characteristics mandatory for each entity - sql

I need a SQL for the following requirement.
Table A:
Product Charactersitic Value
A Age 100
A Number 100
B Age 100
Table B:
Charactersitic
Age
Number
Output Should be:
Product Charactersitic Value
A Age 100
A Number 100
B Age 100
**B Number Null**
This means each characteristic is Mandatory for each Product.

You need a cross join and a left join:
select p.product,
c.characteristic,
p2.value
from (
select distinct product
from products
) p
cross join characteristics c
left join products p2 on p2.product = p.product
and p2.characteristic = c.characteristic;
CROSS JOIN produces every single possible combination of product and characteristic and then you can LEFT JOIN the products with the result of the cross join to get the desired result.
Demo

Related

SQL - Create complete matrix with all variables even if null

please provide some assistance/guidance to solve the following:
I have 1 main table which indicates sales volumes by sales person per different product type.
Where a salesperson did not sell a particular product on a particular day, there is no record.
The intention is to create null value records for salesmen that did not sell a product on a specific day. The query must be dynamic as there are many more salesmen with sales over many days.
Thanks in advance
Just generate records for all sales persons, days, and products using cross join and then bring in the existing data:
select p.salesperson, d.salesdate, st.salestype,
coalesce(t.sales_volume, 0)
from (select distinct salesperson from t) p cross join
(select distinct salesdate from t) d cross join
(select distinct salestype from t) st left join
t
on t.salesperson = p.salesperson and
t.salesdate = d.salesdate and
t.salestype = st.salestype;
Note: You may have other tables that have lists of sales people, dates, and types -- and those can be used instead of the select distinct queries.

Get the total sum of 2 multiplied columns SQL

I have these 3 tables:
Bill:
idBill
Products:
idProduct
price
BillProducts (that connects the tables above):
idBill
idProduct
quantity
Now let's say I wish to get the total price of a certain bill identified by its ID
I would need to multiply the columns of Products.Price by BillProducts.quantity, get its result and sum all the others products in that idBill
Can you guys help me writing that query?
SELECT SUM(QUANTxPRICE) AS SUMED, IDBILL FROM (
SELECT (A.QUANTITY * B.PRICE) AS QUANTxPRICE,a.IDBILL
FROM BILLPRODUCTS AS A
JOIN PRODUCTS AS B ON
A.IDPRODUCT = B.IDPRODUCT
JOIN BILL AS C
ON A.IDBILL = C.IDBILL
) AS X
GROUP BY IDBILL
You would do:
select sum(p.price * bp.product)
from billproducts bp join
products p
on bp.idproduct = p.idproduct
where idbill = <idbill>;
You can get this for all bills using group by:
select idbill, sum(p.price * bp.product)
from billproducts bp join
products p
on bp.idproduct = p.idproduct
group by idbill;
Notes:
You do not need to join the bills table. All the information you need is in the other two tables.
You do not need a subquery.
When you define table aliases, they should be abbreviations for the table names, so the query is easier to follow.

SQL Server: query multiple tables to create a pre-formated "template" csv for export into Excel

I am trying to create the SQL necessary to accomplish the following. As an aside, I am using server side Report Builder against a hosted SQL Server database so am limited in what I can do. There are 3 tables. A salesperson table (sales), an items table (items), and a transaction table (tx).
Here is an example:
Sales Person table (sales)
Person A
Person B
Items ID table (items)
10
20
30
100
200
300
1000
2000
3000
Transaction table (tx)
100 (person A)
300 (person B)
300 (person A)
200 (person B)
Desired result in Report Builder:
Person A
Item 100: 1
Item 200: 0 (NULL)
Item 300: 1
-- NEW PAGE --
Person B:
Item 100: 0 (NULL)
Item 200: 1
Item 300: 1
My problem: here is the SQL I came up with. I need to be able to generate a consistent result set, regardless of whether an item was sold or not by a particular salesperson for easier import into Excel. In addition, I am only looking for items whose code is between 100 and 300 and within a specified date range. My SQL is ignoring the date range and item code range. I originally had these instructions in a WHERE clause but it returned only those lines that were in both tables and I lost the placeholder for any itemcode where the value was null (acting as an INNER join). Within Report Builder I will be counting how many of each items were sold by salesperson.
SELECT
tx.date, sales.salesperson, items.itemcode
FROM
tx
LEFT OUTER JOIN
itemcode ON (tx.itemcode = items.itemcode)
AND (date BETWEEN "10/1/2017" AND "12/31/2017")
AND (itemcode BETWEEN "100" AND "300")
INNER JOIN
sales ON (tx.salesID = sales.salesID)
ORDER BY
itemcode ASC
Many thanks for any and all insight into my challenge!
If you want all sales people and all items, then you can generate the rows using a cross join. You can bring in the available data using left join or exists:
select s.person, i.itemcode,
(case when exists (select 1
from tx
where tx.salesid = s.salesid and tx.itemcode = i.itemcode
)
then 1 else 0
end) as has_sold
from sales s cross join
items i
where i.itemcode between 100 and 300
order by s.saledid, i.itemcode;
If you want the count of items, use left join and group by:
select s.person, i.itemcode, count(tx.salesid) as num_sold
from sales s cross join
items i left join
tx
on tx.salesid = s.salesid and tx.itemcode = i.itemcode
where i.itemcode between 100 and 300
order by s.saledid, i.itemcode;
Here's an example that uses both cross join (to get all combinations of sales and items) and left join (to get the transactions for given dates)
SELECT
tx.date, sales.salesperson, items.itemcode
FROM
Items
CROSS JOIN
sales
LEFT OUTER JOIN
tx ON (items.itemcode = tx.itemcode)
AND date BETWEEN '10/1/2017' AND '12/31/2017'
AND (tx.salesID = sales.salesID)
WHERE
(items.itemcode BETWEEN '100' AND '300')
ORDER BY
itemcode ASC

Postgres array query

(The following is a highly simplified description of my problem. The company policy does not allow me to describe the actual scenario in any detail.)
The DB tables involved are:
PRODUCTS:
ID Name
---------
1 Ferrari
2 Lamborghini
3 Volvo
CATEGORIES:
ID Name
----------
10 Sports cars
20 Safe cars
30 Red cars
PRODUCTS_CATEGORIES
ProductID CategoryID
-----------------------
1 10
1 30
2 10
3 20
LOCATIONS:
ID Name
------------
100 Sports car store
200 Safe car store
300 Red car store
400 All cars r us
LOCATIONS_CATEGORIES:
LocationID CategoryID
------------------------
100 10
200 20
300 30
400 10
400 20
400 30
Note that the locations are not directly connected to the products, only the categories. The customer should be able to see a list of locations that can provide all the product categories that the products they want to buy belong to. So, for example:
A customer wants to buy a Ferrari. This would be available from stores in categories 10 or 30. This gives us stores 100, 300 and 400 but not 200.
However, if a customer wants to buy a Volvo and a Lamborghini this would be available from stores in categories 10 and 20. Which only gives us store 400.
Another customer wants to buy a Ferrari and a Volvo. This they could get from a store in either categories 10 + 20 (sporty and safe) or categories 30 + 20 (red and safe).
What I need is a postgres query that takes a number of products and returns the locations where all of them can be found. I got started with arrays and the <# operator but got lost quickly. Here follows some example SQL that attempts to get stores where a Ferrari and a Lamborghini can be bought. It does not work correctly since it requires the locations to satisfy all the categories that all the selected cars belong to. It returns location 400 only but should return locations 400 and 100.
SELECT l.* FROM locations l
WHERE
(SELECT array_agg(DISTINCT(categoryid)) FROM products_categories WHERE productid IN (1,2))
<#
(SELECT array_agg(categoryid) FROM locations_categories WHERE locationid = l.id);
I hope my description makes sense.
Here is the query. You should insert a list of selected cars Ids pc.ProductId in (1,3) and in the end you should correct condition to selected cars count so if you select 1 and 3 you should write HAVING COUNT(DISTINCT pc.ProductId) = 2 if you select 3 cars then there have to be 3. This condition in HAVING give you condition that ALL cars are in these Locations:
SELECT Id FROM Locations l
JOIN Locations_Categories lc on l.Id=lc.LocationId
JOIN Products_Categories pc on lc.CategoryId=pc.CategoryID
where pc.ProductId in (1,3)
GROUP BY l.id
HAVING COUNT(DISTINCT pc.ProductId) = 2
Sqlfiddle demo
For example for one car it will be:
SELECT Id FROM Locations l
JOIN Locations_Categories lc on l.Id=lc.LocationId
JOIN Products_Categories pc on lc.CategoryId=pc.CategoryID
where pc.ProductId in (1)
GROUP BY l.id
HAVING COUNT(DISTINCT pc.ProductId) = 1
Only Ferrary demo
Volvo and a Lamborghini demo
(This basically elaborates on #valex's answer, though I didn't realise that until I posted; please accept #valex's not this one).
This can be done using only joins and aggregation.
Build a join tree mapping locations to products, as normal. Then join it with the list of desired products (one-column values rows) and filter the join to only matching product names. You now have one row with the location of a product wherever that product can be found.
Now group by location and return locations where the number of products present is equal to the number we're looking for (for ALL). For ANY we omit the HAVING filter because any location row returned by the join is what we want.
So:
WITH wantedproducts(productname) AS (VALUES('Volvo'), ('Lamborghini'))
SELECT l."ID"
FROM locations l
INNER JOIN locations_categories lc ON (l."ID" = lc."LocationID")
INNER JOIN categories c ON (c."ID" = lc."CategoryID")
INNER JOIN products_categories pc ON (pc."CategoryID" = c."ID")
INNER JOIN products p ON (p."ID" = pc."ProductID")
INNER JOIN wantedproducts wp ON (wp.productname = p."Name")
GROUP BY l."ID"
HAVING count(DISTINCT p."ID") = (SELECT count(*) FROM wantedproducts);
is what you want, basically.
For "stores with any of the wanted products" queries, drop the HAVING clause.
You an also ORDER BY the aggregate if you want to show stores with any match but sort based on number of matches.
You can also add a string_agg(p."Name") to the SELECT values-list if you want to list products that can be found at that store.
If you want your input to be an array rather than a values-list, just replace the VALUES (...) with SELECT unnest($1) and pass your array as the parameter $1, or write it literally in place of $1.
ANSWER IN PROGRESS: (I will add answers as I get the required result)
For your first question:
A customer wants to buy a Ferrari. This would be available from stores
in categories 10 or 30. This gives us stores 100, 300 and 400 but not
200.
SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE p.id = 1
Second question:
However, if a customer wants to buy a Volvo and a Lamborghini this
would be available from stores in categories 10 and 20. Which only
gives us store 400.
SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE l.id in (select id
from locations loc
join locations_categories locat1
on loc.id = locat1.LocationId
join locations_categories locat2
on loc.id = locat2.LocationId
where locat1.CategoryId = 10
AND locat2.categoryId = 20)
RESULT FOR SECOND QUESTION USING INTERSECT:
intersect will cross reference all the stores where 1 product can be found each time:
SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE p.id = 2
INTERSECT
SELECT DISTINCT l.id, l.name
FROM Products p
LEFT JOIN Product_Categories p_c
ON p.id = p_c.ProductId
LEFT JOIN Categories c
ON p_c.CategoryId = c.id
LEFT JOIN Locations_Categories l_c
ON c.id = l_c.CategoryId
LEFT JOIN Locations l
ON l_c.LocationId = l.id
WHERE p.id = 3
For every new product you add a new INTERSECT statement and create a new select with the wanted product id
SQLFIDDLE: http://sqlfiddle.com/#!15/ce97d/15
Well, it's hard to totally avoid arrays here but I think I found a solution with less array functions.
Instead of selecting needed locations, I excluded non valid ones.
WITH needed_categories AS (
SELECT p."ID", array_agg(pc."CategoryID") AS at_least_one_should_match
FROM Products p
JOIN Products_Categories pc ON p."ID" = pc."ProductID"
WHERE p."ID" IN (1, 3)
GROUP BY p."ID"
),
not_valid_locations AS (
SELECT DISTINCT lc."LocationID", unnest(nc.at_least_one_should_match)
FROM Locations_Categories lc
JOIN needed_categories nc ON NOT ARRAY[lc."CategoryID"] && nc.at_least_one_should_match
EXCEPT
SELECT * FROM Locations_Categories
)
SELECT *
FROM Locations
WHERE "ID" NOT IN (
SELECT "LocationID" FROM not_valid_locations
);
Here is the SQLFiddle: http://sqlfiddle.com/#!15/e138d/78
This works but I'm still trying to avoid double seq scan of the Location_Categories.
The fact that cars can belong to multiple categories is a bit tricky, I solved this using arrays but I'm trying to get rid of these too.

Create View having trouble with JOINS

I have 5 regions that each contain 1 store and 1 warehouse. I have a table that contains the regions and corresponding stores and warehouses. I also have a table that lists the inventories of both the stores and warehouses. Finally, have a table that lists the allowed quantities for the warehouse. If I look at the inventory table right now it would say something like:
FACILITY ID ITEM_NUMBER QTY
STORE 1 15D 2
WAREHOUSE 1 15D 1
The problem is, Store 1 and Warehouse 1 belong to region 1 and I would like to see it in this format:
REGION WAREHOUSE QTY OH STORE QTY OH
1 1 2 1 1
and so on... So I wrote the following SQL:
Create or replace view REGION_BALANCES
as
with WAREHOUSES as
(
select
A.REGION_CODE,A.REG_DESC,A.WAREHOUSE_NUMBER,A.FACILITY_ID,C.ITEM_NUMBER,sum(C.IN_STOCK_QTY) as IN_STOCK_QTY, B.ALLOWED_QTY
from REG_WHS_STR_ASSOC A
join ALLOWANCES B on (A.FACILITY_ID = B.FACILITY_ID)
join INVENTORIES C on (A.FACILITY_ID = FACILITY_ID) and B.ITEM_NUMBER = C.ITEM_NUMBER)
group by A.REGION_CODE,A.REG_DESC,A.WAREHOUSE_NUMBER,C.ITEM_NUMBER,B.ALLOWED_QTY
),
STORES as
(
select
A.REGION_CODE,A.REG_DESC,A.STORE_NUMBER,A.FACILITY_ID,B.ITEM_NUMBER,sum(B.IN_STOCK_QTY) as IN_STOCK_QTY
from REG_WHS_STR_ASSOC A
join INVENTORIES B on (A.FACILITY_ID = B.FACILITY_ID)
group by A.REGION_CODE,A.REG_DESC,A.STORE_NUMBER, B.ITEM_NUMBER
)
select A.REGION_CODE,A.REG_DESC,A.WAREHOUSE_NUMBER,A.FACILITY_ID,WAREHOUSES.ITEM_NUMBER,WAREHOUSES.IN_STOCK_QTY,WAREHOUSES.ALLOWED_QTY,STORES.STORE_NUMBER,STORES.FACILITY_ID,STORES.ITEM_NUMBER,STORES.IN_STOCK_QTY
from REG_WHS_STR_ASSOC A
join WAREHOUSES on (A.REGION_CODE = WAREHOUSES.REGION_CODE)
join STORES on (A.REGION_CODE = STORES.REGION_CODE)
order by 5,1 asc;
I have changed my join around from left joins to right joins to cross joins to inner joins; however, I either get 30 records (and I need hundreds of records) or I get Cartesian products. Can anyone provide advice to what I'm doing wrong?
Thanks in advance!
You were forgetting to link warehouses and stores by item number (plus left join is the better for this case)
Create or replace view REGION_BALANCES
as
with WAREHOUSES as
(
select
A.REGION_CODE, A.REG_DESC, A.WAREHOUSE_NUMBER,
A.FACILITY_ID, C.ITEM_NUMBER,sum(C.IN_STOCK_QTY) as IN_STOCK_QTY, B.ALLOWED_QTY
from REG_WHS_STR_ASSOC A
join ALLOWANCES B on (A.FACILITY_ID = B.FACILITY_ID)
join INVENTORIES C on (A.FACILITY_ID = FACILITY_ID) and B.ITEM_NUMBER = C.ITEM_NUMBER)
group by A.REGION_CODE,A.REG_DESC,A.WAREHOUSE_NUMBER,C.ITEM_NUMBER,B.ALLOWED_QTY
),
STORES as
(
select
A.REGION_CODE,A.REG_DESC,A.STORE_NUMBER,
A.FACILITY_ID,B.ITEM_NUMBER,sum(B.IN_STOCK_QTY) as IN_STOCK_QTY
from REG_WHS_STR_ASSOC A
join INVENTORIES B on (A.FACILITY_ID = B.FACILITY_ID)
group by A.REGION_CODE,A.REG_DESC,A.STORE_NUMBER, B.ITEM_NUMBER
)
select A.REGION_CODE, A.REG_DESC,A. WAREHOUSE_NUMBER, A.FACILITY_ID,
WAREHOUSES.ITEM_NUMBER, WAREHOUSES.IN_STOCK_QTY, WAREHOUSES.ALLOWED_QTY,
STORES.STORE_NUMBER, STORES.FACILITY_ID, STORES.ITEM_NUMBER, STORES.IN_STOCK_QTY
from REG_WHS_STR_ASSOC A
left join WAREHOUSES on (A.REGION_CODE = WAREHOUSES.REGION_CODE)
left join STORES on
(A.REGION_CODE = STORES.REGION_CODE AND STORES.ITEM_NUMBER = WAREHOUSES.ITEM_NUMBER)
order by 5,1 asc;