Multiple SQL filter on same column - sql

How do I display the customer_id of customers who bought products A and B, but didn’t buy product C, ordered by ascending customer ID.
I tried the below code, but does not give me any result.
select customer_id, product_name from orders where customer_id = 'A' and 'product_name '= 'B'

select customer_id from orders where product_name = 'A'
intersect
select customer_id from orders where product_name = 'B'
except
select customer_id from orders where product_name = 'C'

You can use analytical function as follows:
Select * from
(select customer_id, product_name ,
Count(distinct case when product_name in ('A','B') then product_name end)
Over (partition by customer_id) as cntab ,
Count(case when product_name = 'C' then product_name end)
Over (partition by customer_id) as cntc
from orders t) t
Where cntab = 2 and cntc = 0;

One method use exists and not exists:
select o.*
from orders o
where exists (select 1
from orders o2
where o2.customer_id = o.customer_id and o2.product_name = 'A'
) and
exists (select 1
from orders o2
where o2.customer_id = o.customer_id and o2.product_name = 'B'
) and
not exists (select 1
from orders o2
where o2.customer_id = o.customer_id and o2.product_name = 'C'
)
order by customer_id;

To me, this query reads well ...
SELECT customer_id
FROM (
SELECT TableA.customer_id
FROM (
SELECT customer_id
FROM orders
WHERE product_name = 'A'
) AS TableA
INNER JOIN (
SELECT customer_id
FROM orders
WHERE product_name = 'B'
) AS TableB
ON TableA.customer_id = TableB.customer_id
) AS TableX
WHERE customer_id NOT IN (SELECT customer_id FROM orders WHERE product_name = 'C')
Explanation. Get a list of customer ids that bought products A and B with self inner join. Then, pare that list down by removing any rows where those customers bought product C.

Related

Select Customers who purchased one specific Product

We have two tables:
Customers:
Products:
The goal is to select [Id] and [CustomerName] of Customers who purchased Milk AND did not purchase Bread. In the case the correct query should return a customer with Id 2 (Ann).
The query which I thought of (and which is obviously incorrect) is:
select CustomerName from dbo.Customers
where Id in
(
select CustomerId from dbo.Products
where ProductName = 'Milk' and ProductName != 'Bread'
)
It returns two customers: 1 (John) and 2 (Ann).
How to rewrite the query so it would return only customer with Id 2?
You can try the query below
SELECT CustomerName
FROM dbo.Customers c
WHERE EXISTS (
SELECT 1
FROM dbo.Products
WHERE CustomerId = c.Id
AND ProductName = 'Milk'
) AND NOT EXISTS (
SELECT 1
FROM dbo.Products
WHERE CustomerId = c.Id
AND ProductName = 'Bread'
)
You don't need to use two exists, just use where clause with not exists :
select c.*
from customer c
where ProductName = 'Milk' and
not exists (select 1 from Products p where p.CustomerId = c.id and p.ProductName = 'Bread');
I am inclined to use aggregation for this. Here is one method:
select c.customerId
from dbo.Products p
where p.productName in ('Milk', 'Bread')
group by c.customerId
having sum(case when p.productName = 'Milk' then 1 else 0 end) > 0 and
sum(case when p.productName = 'Bread' then 1 else 0 end) = 0 ;
You can add the join in to get the customer name, if you really need that.
Basically, this counts the number of rows for each customer that have 'Milk'. The > 0 says there is at least one. It then counts the number of rows that have 'Bread'. The = 0 says that there are none.
SELECT P.Id ,C.Customers
FROM Customers AS C , Product AS P
WHERE (C.Id = P.CustomerId)
AND (P.ProductName = 'Milk')
AND NOT EXISTS (
SELECT 1
FROM Products
WHERE CustomerId = C.Id
AND ProductName = 'Bread'
)
If normalization is not the case, efficiency isn't either. Here you go:
select CustomerName from dbo.Customers
where
Id in (select CustomerId from dbo.Products where ProductName = 'Milk')
and Id not in (select CustomerId from dbo.Products where ProductName = 'Bread')
Ah. Just saw it could be considered a duplicate of Eric's answer. Using exists may be a little faster indeed.

Sum of all values except the first

I have the following three tables:
Customers:
Cust_ID,
Cust_Name
Products:
Prod_ID,
Prod_Price
Orders:
Order_ID,
Cust_ID,
Prod_ID,
Quantity,
Order_Date
How do I display each costumer and how much they spent excluding their very first purchase?
[A] - I can get the total by multiplying Products.Prod_Price and Orders.Quantity, then GROUP by Cust_ID
[B] - I also can get the first purchase by using TOP 1 on Order_Date for each customer.
But I couldnt figure out how to produce [A]-[B] in one query.
Any help will be greatly appreciated.
For SQL-Server 2005, 2008 and 2008R2:
; WITH cte AS
( SELECT
c.Cust_ID, c.Cust_Name,
Amount = o.Quantity * p.Prod_Price,
Rn = ROW_NUMBER() OVER (PARTITION BY c.Cust_ID
ORDER BY o.Order_Date)
FROM
Customers AS c
JOIN
Orders AS o ON o.Cust_ID = c.Cust_ID
JOIN
Products AS p ON p.Prod_ID = o.Prod_ID
)
SELECT
Cust_ID, Cust_Name,
AmountSpent = SUM(Amount)
FROM
cte
WHERE
Rn >= 2
GROUP BY
Cust_ID, Cust_Name ;
For SQL-Server 2012, using the FIRST_VALUE() analytic function:
SELECT DISTINCT
c.Cust_ID, c.Cust_Name,
AmountSpent = SUM(o.Quantity * p.Prod_Price)
OVER (PARTITION BY c.Cust_ID)
- FIRST_VALUE(o.Quantity * p.Prod_Price)
OVER (PARTITION BY c.Cust_ID
ORDER BY o.Order_Date)
FROM
Customers AS c
JOIN
Orders AS o ON o.Cust_ID = c.Cust_ID
JOIN
Products AS p ON p.Prod_ID = o.Prod_ID ;
Another way (that works in 2012 only) using OFFSET FETCH and CROSS APPLY:
SELECT
c.Cust_ID, c.Cust_Name,
AmountSpent = SUM(x.Quantity * x.Prod_Price)
FROM
Customers AS c
CROSS APPLY
( SELECT
o.Quantity, p.Prod_Price
FROM
Orders AS o
JOIN
Products AS p ON p.Prod_ID = o.Prod_ID
WHERE
o.Cust_ID = c.Cust_ID
ORDER BY
o.Order_Date
OFFSET
1 ROW
-- FETCH NEXT -- not needed,
-- 20000000000 ROWS ONLY -- can be removed
) AS x
GROUP BY
c.Cust_ID, c.Cust_Name ;
Tested at SQL-Fiddle
Note that the second solution returns also the customers with only one order (with the Amount as 0) while the other two solutions do not return those customers.
Which version of SQL? If 2012 you might be able to do something interesting with OFFSET 1, but I'd have to ponder much more how that works with grouping.
EDIT: Adding a 2012 specific solution inspired by #ypercube
I wanted to be able to use OFFSET 1 within the WINDOW to it al in one step, but the syntax I want isn't valid:
SUM(o.Quantity * p.Prod_Price) OVER (PARTITION BY c.Cust_ID
ORDER BY o.Order_Date
OFFSET 1)
Instead I can specify the row boxing, but have to filter the result set to the correct set. The query plan is different from #ypercube's, but the both show 50% when run together. They each run twice as as fast as my original answer below.
WITH cte AS (
SELECT c.Cust_ID
,c.Cust_Name
,SUM(o.Quantity * p.Prod_Price) OVER(PARTITION BY c.Cust_ID
ORDER BY o.Order_ID
ROWS BETWEEN 1 FOLLOWING
AND UNBOUNDED FOLLOWING) AmountSpent
,rn = ROW_NUMBER() OVER(PARTITION BY c.Cust_ID ORDER BY o.Order_ID)
FROM Customers AS c
INNER JOIN
Orders AS o ON o.Cust_ID = c.Cust_ID
INNER JOIN
Products AS p ON p.Prod_ID = o.Prod_ID
)
SELECT Cust_ID
,Cust_Name
,ISNULL(AmountSpent ,0) AmountSpent
FROM cte WHERE rn=1
My more general solution is similar to peter.petrov's, but his didn't work "out of the box" on my sample data. That might be an issue with my sample data or not. Differences include use of CTE and a NOT EXISTS with a correlated subquery.
CREATE TABLE Customers (Cust_ID INT, Cust_Name VARCHAR(10))
CREATE TABLE Products (Prod_ID INT, Prod_Price MONEY)
CREATE TABLE Orders (Order_ID INT, Cust_ID INT, Prod_ID INT, Quantity INT, Order_Date DATE)
INSERT INTO Customers SELECT 1 ,'Able'
UNION SELECT 2, 'Bob'
UNION SELECT 3, 'Charlie'
INSERT INTO Products SELECT 1, 10.0
INSERT INTO Orders SELECT 1, 1, 1, 1, GetDate()
UNION SELECT 2, 1, 1, 1, GetDate()
UNION SELECT 3, 1, 1, 1, GetDate()
UNION SELECT 4, 2, 1, 1, GetDate()
UNION SELECT 5, 2, 1, 1, GetDate()
UNION SELECT 6, 3, 1, 1, GetDate()
;WITH CustomersFirstOrder AS (
SELECT Cust_ID
,MIN(Order_ID) Order_ID
FROM Orders
GROUP BY Cust_ID
)
SELECT c.Cust_ID
,c.Cust_Name
,ISNULL(SUM(Quantity * Prod_Price),0) CustomerOrderTotalAfterInitialPurchase
FROM Customers c
LEFT JOIN (
SELECT Cust_ID
,Quantity
,Prod_Price
FROM Orders o
INNER JOIN
Products p ON o.Prod_ID = p.Prod_ID
WHERE NOT EXISTS (SELECT 1 FROM CustomersFirstOrder a WHERE a.Order_ID=o.Order_ID)
) b ON c.Cust_ID = b.Cust_ID
GROUP BY c.Cust_ID
,c.Cust_Name
DROP TABLE Customers
DROP TABLE Products
DROP TABLE Orders
Try this. It should do it.
SELECT c1.cust_name ,
c1.cust_id ,
SUM(p1.Prod_Price)
FROM orders o1
JOIN products p1 ON o1.prod_id = p1.prod_id
JOIN customers c1 ON o1.cust_id = c1.cust_id
LEFT JOIN ( SELECT o2.cust_id ,
MIN(o2.Order_Date) AS Order_Date
FROM orders o2
GROUP BY o2.cust_id
) t ON o1.cust_id = t.cust_id
AND o1.Order_Date = t.Order_Date
WHERE t.Order_Date IS NULL
GROUP BY c1.cust_name ,
c1.cust_id
You have to number orders by Customer and then you can have the amount for the first order and next orders with a CTE and ROW_NUMBER() like this:
; WITH NumberedOrders
AS ( SELECT Customers.Cust_Id ,
Customers.Cust_Name ,
ROW_NUMBER() OVER ( ORDER BY Customers.Cust_id ) AS Order_Number ,
Orders.Order_Date ,
Products.Prod_price * Orders.Quantity AS Amount
FROM Orders
INNER JOIN Customers ON Orders.Cust_Id = Customers.Cust_Id
INNER JOIN Products ON Orders.Prod_Id = Products.Prod_Id
)
SELECT Cust_Id ,
SUM(CASE WHEN Order_Number = 1 THEN Amount
ELSE 0
END) AS A_First_Order ,
SUM(CASE WHEN Order_Number = 1 THEN 0
ELSE Amount
END) AS B_Other_orders ,
SUM(Amount) AS C_All_orders
FROM NumberedOrders
GROUP BY Cust_Id
ORDER BY Cust_Id

How could I optimize this dynamic SQL query?

I've been having difficulties with the following query. I've been trying to optimize it and perhaps make it more readable. Let's say I have 3 tables orders_returned, orders_completed, orders_delivered with matching columns oder_id, customer_id. Depending on selected options, I might need to retrieve orders which were delivered, then returned and finally completed (same order_id occurs in all three tables) which have the same customer_id. Also I might only need to retrieve only delivered and returned orders in which case I would omit AND order_id IN (SELECT order_id FROM ORDERS_COMPLETED) from the WHERE clause. For example, Get delivered and returned orders by customers John and Tim
As of now my query looks like this:
SELECT order_id
FROM
(
SELECT order_id, customer_id
FROM ORDERS_RETURNED
UNION
SELECT order_id, customer_id
FROM ORDERS_COMPLETED
UNION
SELECT order_id, customer_id
FROM ORDERS_DELIVERED
)
WHERE
customer_id IN ('customer1', 'customer2', ...)
AND order_id IN (SELECT order_id FROM ORDERS_RETURNED)
AND order_id IN (SELECT order_id FROM ORDERS_COMPLETED)
AND order_id IN (SELECT order_id FROM ORDERS_DELIVERED)
I'm still learning SQL and would like to see if there are better options.
EDIT: I am using Oracle database. There is also Orders table which has distinct order_ids and some other irrelevant columns. It does not store customer_ids.
Also, the order might occur in one table or in two of them only, so joins, I think, are of no use here.
Since you have an Order table, I presume you are also storing the CustomerId in that table as well. Assuming so, try this:
SELECT DISTINCT O.OrderId
FROM Orders O
LEFT JOIN Orders_Completed OC ON O.OrderId = OC.OrderId
LEFT JOIN Orders_Delivered OD ON O.OrderId = OD.OrderId
LEFT JOIN Orders_Returned ORE ON O.OrderId = ORE.OrderId
WHERE O.CustomerId IN (...)
AND OD.OrderId IS NOT NULL AND ORE.OrderId IS NOT NULL AND OC.OrderId IS NULL
This particular query will return you all distinct orders where customer in (...) where the order has been delivered and returned, but not completed. Toggle the use of the IS NULL and IS NOT NULL to get your desired output.
Good luck.
You should use Joins instead of Inner/Nested queries.
Try below instead ::
SELECT A.order_id, A.customer_id FROM ORDERS_RETURNED A
INNER JOIN ORDERS_COMPLETED B ON A.order_id = B.order_id AND A.customer_id = B.customer_id
INNER JOIN ORDERS_DELIVERED C ON A.order_id = C.order_id AND A.customer_id = C.customer_id
Where A.customer_id IN ('customer1', 'customer2', ...)
you could do this with something like:
with data
as (select customer_id,
order_id,
nvl(max(case status when 'RETURNED' then 'Y' end), 'N') returned,
nvl(max(case status when 'COMPLETED' then 'Y' end), 'N') completed,
nvl(max(case status when 'DELIVERED' then 'Y' end), 'N') delivered
from (select 'RETURNED' status, order_id, customer_id
from orders_returned
union all
select 'COMPLETED' status, order_id, customer_id
from orders_completed
union all
select 'DELIVERED' status, order_id, customer_id
from orders_delivered)
group by customer_id, order_id)
select *
from data
where returned = 'Y'
and delivered = 'Y'
and customer_id in ('xx', 'xxx') ;
or
with data
as (select customer_id,
order_id,
max(returned) returned,
max(completed) completed,
max(delivered) delivered
from (select 'Y' returned, null completed, null delivered, order_id, customer_id
from orders_returned
union all
select null, 'Y', null, order_id, customer_id
from orders_completed
union all
select null, null, 'Y', order_id, customer_id
from orders_delivered)
group by customer_id, order_id)
select *
from data
where returned = 'Y'
and delivered = 'Y'
and customer_id in ('xx', 'xxx');
eg: http://sqlfiddle.com/#!4/3e2fb/2

Last order item in Oracle SQL

I need to list columns from customer table, the date from first order and all data from last one, in a 1:N relationship between customer and order tables. I'm using Oracle 10g.
How the best way to do that?
TABLE CUSTOMER
---------------
id NUMBER
name VARCHAR2(200)
subscribe_date DATE
TABLE ORDER
---------------
id NUMBER
id_order NUMBER
purchase_date DATE
purchase_value NUMBER
Here is one way of doing it, using the row_number function, one join, and on aggregation:
select c.*,
min(o.purchase_date) as FirstPurchaseDate,
min(case when seqnum = 1 then o.id_order end) as Last_IdOrder,
min(case when seqnum = 1 then o.purchase_date end) as Last_PurchaseDate,
min(case when seqnum = 1 then o.purchase_value end) as Last_PurchaseValue
from Customer c join
(select o.*,
row_number() over (partition by o.id order by purchase_date desc) as seqnum
from orders o
) o
on c.customer_id = o.order_id
group by c.customer_id, c.name, c.subscribe_date
It's not obvious how to join the customer table to the orders table (order is a reserved word in Oracle so your table can't be named order). If we assume that the id_order in orders joins to the id in customer
SELECT c.id customer_id,
c.name name,
c.subscribe_date,
o.first_purchase_date,
o.id last_order_id,
o.purchase_date last_order_purchase_date,
o.purchase_value last_order_purchase_value
FROM customer c
JOIN (SELECT o.*,
min(o.purchase_date) over (partition by id_order) first_purchase_date,
rank() over (partition by id_order order by purchase_date desc) rnk
FROM orders o) o ON (c.id = o.id_order)
WHERE rnk = 1
I'm confused by your field names, but I'm going to assume that ORDER.id is the id in the CUSTOMER table.
The earliest order date is easy.
select CUSTOMER.*, min(ORDER.purchase_date)
from CUSTOMER
inner join ORDER on CUSTOMER.id = ORDER.id
group by CUSTOMER.*
To get the last order data, join this to the ORDER table again.
select CUSTOMER.*, min(ORD_FIRST.purchase_date), ORD_LAST.*
from CUSTOMER
inner join ORDER ORD_FIRST on CUSTOMER.id = ORD_FIRST.id
inner join ORDER ORD_LAST on CUSTOMER.id = ORD_LAST.id
group by CUSTOMER.*, ORD_LAST.*
having ORD_LAST.purchase_date = max(ORD_FIRST.purchase_date)
Maybe something like this assuming the ID field in the Order table is actually the Customer ID:
SELECT C.*, O1.*, O2.purchase_Date as FirstPurchaseDate
FROM Customer C
LEFT JOIN
(
SELECT Max(purchase_date) as pdate, id
FROM Orders
GROUP BY id
) MaxPurchaseOrder
ON C.Id = MaxPurchaseOrder.Id
LEFT JOIN Orders O1
ON MaxPurchaseOrder.pdate = O1.purchase_date
AND MaxPurchaseOrder.id = O1.id
LEFT JOIN
(
SELECT Min(purchase_date) as pdate, id
FROM Orders
GROUP BY id
) MinPurchaseOrder
ON C.Id = MinPurchaseOrder.Id
LEFT JOIN Orders O2
ON MinPurchaseOrder.pdate = O2.purchase_date
AND MinPurchaseOrder.id = O2.id
And the sql fiddle.

How to group by having the same id?

I want the customerid who bought product X and Y and Z, from the following schema:
Sales(customerid, productName, rid);
I could do the intersection:
select customerid from sales where productName='X'
INTERSECT
select customerid from sales where productName='X'
INTERSTECT
select customerid from sales where productName='Z'
Is this the best I could do?
Not sure if this works in postrgesql, but try:
select customerid
from sales
where productName in ('X', 'Y', 'Z')
group by customerid
having count(distinct productName) = 3
You could also select from sales 3 times:
select s1.customerID
from sales s1, sales s2, sales s3
where s1.productName = 'X'
and S2.productName = 'Y'
and S3.productName = 'Z'
and (S1.customerID = S2.customerID
and s2.customerID = s3.customerID);
Or rewrite using proper join syntax (might not be 100% though...)
select s1.customerID
from sales s1
inner join sales S2
on s1.customerId = S2.customerID
inner join sales S3
on s2.customerID = S3.customerId
where s1.productName = 'X'
and S2.productName = 'Y'
and S3.productName = 'Z';