SQL aggregate query with one-to-many relationship with postgres - sql

I'm trying to aggregate a list of product skus with a query that relates through a line_items table. I've abstracted a simple example of my use case:
my expected result would look like this:
id name skus
1 mike bar sku1,sku2,sku3
2 bort baz sku4
given a schema and data like:
products
id sku
1 sku1
2 sku2
3 sku3
4 sku4
line_items
id order_id product_id
1 1 1
2 1 2
3 1 3
4 2 4
addresses
id name
1 'bill foo'
2 'mike bar'
3 'bort baz'
orders
id address_id total
1 2 66
2 3 99
here's a working query, but it's not correct, i'm getting ALL products for each order. my WHERE should be using orders.id
http://sqlfiddle.com/#!15/70cd7/3/0
however, i can't seem to use orders.id? i'm guessing i need to use a JOIN or LEFT JOIN or somehow change the order of things in my query...
http://sqlfiddle.com/#!15/70cd7/4

http://sqlfiddle.com/#!15/70cd7/12
SELECT orders.id,
addresses.name,
array_agg(DISTINCT products.sku )
FROM orders
LEFT JOIN addresses
ON orders.address_id = addresses.id
LEFT JOIN line_items
ON line_items.order_id = orders.id
LEFT JOIN products
ON products.id = line_items.product_id
GROUP BY orders.id,addresses.name

You can use a correlated subquery with a JOIN to get the list of skus for each order
SELECT
o.id,
a.name,
(SELECT array_to_string(array_agg(sku), ',') AS Skus
FROM products p
INNER JOIN line_items li
ON li.product_id = p.id
WHERE li.order_id = o.id
) AS Skus
FROM orders o
INNER JOIN addresses a
ON a.id = o.address_id
ONLINE DEMO

One solution could be
SELECT orders.id,
addresses.name,
(SELECT string_agg(sku,',') AS skus
FROM products
WHERE id IN
(SELECT DISTINCT line_items.product_id
FROM line_items
WHERE line_items.order_id = orders.id))
FROM orders
inner join addresses
on orders.address_id = addresses.id
;
SQLFiddle

Related

Joining to return all rows even when there are no results

I'm trying to get a list how many products we've sold, per company, by all categories.
I'd like the results to look like:
Category A Company 1 0
Category A Company 2 0
Category A Company 3 5
Category B Company 1 1
Category B Company 2 4
Category B Company 3 0
So every category is returned, every company is returned, even if there are no sales.
This is the query I'm trying and it should make the structure of the database clear. I've attacked this from a bunch of direction, but can't seem to wrap my head around how to get at what I'm looking for.
SELECT com.Company_Name, c.Category_Name, sum(p.Quantity)
FROM Category c
LEFT JOIN Item i on c.Category_ID = i.Category_ID
LEFT JOIN Products p on p.Item_ID = i.Item_ID
LEFT JOIN Invoice iv on iv.Invoice_ID = p.Invoice_ID
LEFT JOIN Company com on com.Company_Id = iv.Company_ID
group by c.Category_Name, com.Company_Name
Thanks for any help...
Generate the rows with a cross join, then left join in the rest of the information:
SELECT co.Company_Name, ca.Category_Name,
COALESCE(SUM(p.Quantity), 0) as quantity
FROM Category c CROSS JOIN
Company co LEFT JOIN
(Invoice iv JOIN
Products p
ON iv.Invoice_ID = p.Invoice_ID JOIN
Item i
ON p.Item_ID = i.Item_ID
)
ON co.Company_Id = iv.Company_ID AND
c.Category_ID = i.Category_ID
GROUP BY ca.Category_Name, co.Company_Name

SQL: How to pull particular orders based on order details?

I have four tables namely customers, orders, orderDetails and Products.
Customer table
cId cName
1 James
2 Adam
3 Ed
Order table
oId cId
1 1
2 2
3 3
OrderDetails table
oId odId pId Quantity
1 1 1 50
1 2 2 45
2 3 2 52
3 4 1 44
Products table
pId PName
1 Apple
2 Orange
I want the list of customers who have never ordered Oranges. I am able to pull records of customers whose order details don't have oranges. But in one of the case, James has ordered both apples and oranges. So, the query should not pull James. I can do this with a larger query. But I want this with a smaller query where something I'm missing.
SQL
SELECT c.cId, c.cName, p.PName, od.Quantity FROM customers c
LEFT JOIN orders o ON c.cId = o.cId
LEFT JOIN orderDetails od ON o.oId = od.oId
JOIN products p ON od.pId = p.pId
WHERE od.pId != 2
I would do this using not exists:
with has_oranges as (
select o.*
from orders o join
orderlines ol
on o.oid = ol.oid
where ol.pid = 2
)
select c.*
from customers c
where not exists (select 1
from has_oranges ho
where ho.cid = c.cid
);
If you want customer information, I don't see what oid has to do with anything.
Notes:
The CTE determines who actually has oranges.
You don't need the products table, because you are using the pid.
Use NOT EXISTS
SELECT *
FROM Customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o
JOIN orderDetails od ON o.oId = od.oId
JOIN products p ON od.pId = p.pId
WHERE p.pName = 'oranges' AND c.cId = o.cId
)
You want all customers that have never ordered oranges. So select all customer IDs that ordered oranges and only show customers that are not in this data set.
select *
from customers c
where cid not in
(
select cid
from orderdetails
where pid = (select pid from products where pname = 'Orange'
);
select * from CustomerTbl where Id in (select t1.Id from CustomerTbl t1
left join OrderTbl t2 on t1.Id = t2.CustomerId
left join OrderDetailTbl t3 on t3.OrderId = t2.Id
left join ProductTbl t4 on t4.Id = t3.ProductId
where t4.Id != 2)
This will return Customers who not ordered Oranges.
This is the sqlfiddle link : http://sqlfiddle.com/#!6/c908d/6
SQL Server 2008 introduced the EXCEPT and INTERSECT keywords for doing this sort of thing. I tend to find that the queries are clearer than when using CTEs.
Microsoft Documentation
select c.cId
from Customer c
except
select o.cId
from Orders o
join OrderDetail od on o.oId = od.oId
and od.pId = 2
cId
-----------
3
You can add the name to the result set by joining to the Customer table in the second half of the query:
select c.cId, c.cName
from Customer c
except
select o.cId, c.cName
from Orders o
join OrderDetail od on o.oId = od.oId
join Customer c on c.cId = o.cId
and od.pId = 2
cId cName
----------- --------------------
3 Ed
We have to eliminate users who took orange. So in the below query i have used sub query
Select C.Cname,OH.oid,PM.Pname,OD.Quantity from Customers C
inner join OrderHeader OH ON C.cid=OH.Cid
inner join OrderDetails OD on oh.oid=od.oid
inner join ProductMast PM on PM.pid=OD.pid where OH.oid not in (select oid
from OrderDetails where pid = 2)

left join two tables on a non-unique column in right table

I have two tables in sql server and i wanna select and join some data from these table.the first tables have some customer like:
---------------
customer id
Dave 1
Tom 2
---------------
and second table i table of purchases that includes list of last purchases with cost and which customer bought that Product:
------------------
product date customer id
PC 1-1-2000 1
phone 2-3-2000 2
laptop 3-1-2000 1
------------------
i wanna select first table (customers info) with last date of their purchases!
i tried left join but that doesn't give me last purchases becuase customer id is not unique in second table! how can i do this function with SQL server query? Regards
If you just want the max date, use aggregation. I would recommend a left join for customers who have made no purchases:
select c.customer, c.id, max(p.date)
from customers c left join
purchases p
on c.id = p.customer_id
group by c.customer, c.id;
Use the not exists clause for the win!
select c.customer, p.*
from Customer as c
inner join Purchase as p
on p.customer_id = c.id
where not exists (
select 1
from Purchase as p2
where p2.customer_id = p.customer_id
and p2.date > p.date
)
I think you can use inner join and group by
select table1.customer, table1.id, table.max(date)
from table1
inner join table2 on table1.id = table2.id
group by table1.customer, table1.id

How to get the count and display only one row

I've got 3 Tables:
tblCustomer
CustomerID CustomerName
1 Customer 1
2 Customer 2
tblOrder
OrderID CustomerID OrderTypeID LoanNumber
1 1 1 98513542
2 1 1 71283527
3 1 1 10268541
4 1 1 61258965
tblOrderType
OrderTypeID OrderTypeName
1 Purchase
2 Rental
Now, I'm looking to return CustomerID, CustomerName, OrderTypeName and OrderCount, where OrderCount is how many of each order the customer has. I'm using the following query:
SELECT tblCustomer.CustomerID, tblCustomer.customerName, tblOrderType.OrderTypeName, tblOrder.OrderID
FROM tblCustomer
INNER JOIN tblOrder
ON tblCustomer.CustomerID = tblOrder.CustomerID
INNER JOIN tblOrderType
ON tblOrderType.OrderTypeID = tblOrder.OrderTypeID
It sort of works. It gets all I'm asking for, except for the OrderCount, obviously. The result is like this:
CustomerID CustomerName OrderTypeName OrderID
1 Customer 1 Purchase 1
1 Customer 1 Purchase 2
1 Customer 1 Purchase 3
1 Customer 1 Purchase 4
But what I'm looking for is this:
CustomerID CustomerName OrderTypeName OrderCount
1 Customer 1 Purchase 4
Now, I've tried adding Count() into the query at various places (Count(tblOrder.OrderID) for one), and I get an error that tblCustomer.CustomerID is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
This isn't a homework assignment. I just don't know a whole lot about sql and database interactions, since it wasn't taught in my school, and I've got a friend who's throwing scenarios at me.
You need to use grouping and aggregation:
SELECT tblCustomer.CustomerID, tblCustomer.customerName, tblOrderType.OrderTypeName,
count (tblOrder.OrderID) asOrderCOunt
FROM tblCustomer
INNER JOIN tblOrder
ON tblCustomer.CustomerID = tblOrder.CustomerID
INNER JOIN tblOrderType
ON tblOrderType.OrderTypeID = tblOrder.OrderTypeID
GROUP BY
tblCustomer.CustomerID,
tblCustomer.customerName,
tblOrderType.OrderTypeName
You can't use aggregate functions without grouping everything that isn't aggregated.
The error message does tell you the issue. Whenever you use an aggregation operator (COUNT, SUM, AVG, etc.), you have to tell the database how these should be aggregated together. You do this with a GROUP BY statement (the link there is for SQL Server, but it should generally apply to all database engines):
SELECT
C.CustomerID
,C.customerName
,OT.OrderTypeName
,COUNT(O.OrderID)
FROM tblCustomer C
INNER JOIN tblOrder O
ON C.CustomerID = O.CustomerID
INNER JOIN tblOrderType OT
ON OT.OrderTypeID = O.OrderTypeID
GROUP BY
C.CustomerID
,C.customerName
,OT.OrderTypeName
Try this, just add group by and count the grouped rows
SELECT tblCustomer.CustomerID, tblCustomer.customerName,
tblOrderType.OrderTypeName, tblOrder.OrderID,
COUNT(tblOrder.OrderID) as OrderCount
FROM tblCustomer INNER JOIN tblOrder
ON tblCustomer.CustomerID = tblOrder.CustomerID
INNER JOIN tblOrderType
ON tblOrderType.OrderTypeID = tblOrder.OrderTypeID
GROUP BY tblCustomer.CustomerID, tblCustomer.customerName,
tblOrderType.OrderTypeName

How to join tables together based on subselects?

I'm stuck to figure out how to write a query. Basically I've three tables (Orders, Products, Orders_Products) which I want to join together and apply some filtering.
Orders table:
ORDER_ID CUSTOMER_ID
1 1
2 2
Products table:
PRODUCT_ID PRODUCT_NAME PRODUCT_TITLE
1 'P1' 'T1'
2 'P1' 'T2'
3 'P2' 'T3'
4 'P2' 'T4'
5 'P2' 'T5'
6 'P3' 'T6'
Orders_Products table:
ORDER_ID PRODUCT_ID
1 1
1 3
2 1
2 3
2 6
For example I want to get all Orders which consists (exactly) of the products P1/T1 and P2/T3. I tried something like this, but that doesn't work:
SELECT * FROM Orders
LEFT JOIN Orders_Products ON Orders_Products.ORDER_ID = Orders.ORDER_ID
LEFT JOIN Products ON Orders_Products.PRODUCT_ID = Products.PRODUCT_ID
WHERE EXISTS (SELECT * FROM Product WHERE PRODUCT_NAME = 'P1' AND PRODUCT_TITLE = 'T1')
AND EXISTS (SELECT * FROM Product WHERE PRODUCT_NAME = 'P2' AND PRODUCT_TITLE = 'T3');
EDIT: To clarify what I really have to achieve. The user should be able to search for orders matching the given products. The user enters one or more product name / product title combinations and gets all the orders which have exactly this products associated. What I get (from a web application) are only the name/title combinations and I have to use those in a query to get the ORDER_ID.
SELECT OrderID, COUNT(*) AS ProductsCount
FROM Orders_Products
WHERE (PRODUCT_ID = 1 OR PRODUCT_ID = 3)
GROUP BY OrderID
HAVING COUNT(*) = 2
EDIT: Please ignore the above statement. See if the following works.
SELECT OrderID,
SUM(CASE PRODUCT_ID WHEN 1 THEN 1 WHEN 3 THEN 1 ELSE 3 END)
AS ProductsCount
FROM Orders_Products
GROUP BY OrderID
HAVING SUM(CASE PRODUCT_ID WHEN 1 THEN 1 WHEN 3 THEN 1 ELSE 3 END) = 2
I guess this should get you Orders which has only these 2 products.
You probably cannot write simple queries in MySQL to achieve this. But ANSI SQL supports table value constructor which simplifies this type of query.
This basic query returns the full list of orders (5 rows):
SELECT * FROM Products
JOIN Orders_Products ON Orders_Products.PRODUCT_ID = Products.PRODUCT_ID
JOIN Orders ON Orders_Products.ORDER_ID = Orders.ORDER_ID
This query with table value constructor returns the orders that you need:
SELECT * FROM Products
JOIN Orders_Products ON Orders_Products.PRODUCT_ID = Products.PRODUCT_ID
JOIN Orders ON Orders_Products.ORDER_ID = Orders.ORDER_ID
LEFT JOIN (VALUES('P1', 'T1'), ('P2', 'T3')) V(P_NAME, P_TITLE) ON PRODUCT_NAME = P_NAME AND PRODUCT_TITLE=P_TITLE
This query groups the above to returns the ORDER_ID where there is no order outside the required list (eliminating the orders that have rows containing null):
SELECT ORDER_ID FROM Products
JOIN Orders_Products ON Orders_Products.PRODUCT_ID = Products.PRODUCT_ID
JOIN Orders ON Orders_Products.ORDER_ID = Orders.ORDER_ID
LEFT JOIN (VALUES('P1', 'T1'), ('P2', 'T3')) V(P_NAME, P_TITLE) ON PRODUCT_NAME = P_NAME AND PRODUCT_TITLE=P_TITLE
GROUP BY ORDER_ID HAVING COUNT(*) = 2
Among open source databases, HSQLDB is one that supports table value constructor and other user friendly features of ANSI SQL:2008