Optimize SQL query for canceled orders - sql

Here is a subset of my tables:
orders:
- order_id
- customer_id
order_products:
- order_id
- order_product_id (unique key)
- canceled
I want to select all orders (order_id) for a given customer(customer_id), where ALL of the products in the order are canceled, not just some of the products. Is there a more elegantly or efficient way of doing it than this:
select order_id from orders
where order_id in (
select order_id from orders
inner join order_products on orders.order_id = order_products.order_id
where order_products.customer_id = 1234 and order_products.canceled = 1
)
and order_id not in (
select order_id from orders
inner join order_products on orders.order_id = order_products.order_id
where order_products.customer_id = 1234 and order_products.canceled = 0
)

If all orders have at least one row in order_products, Try this
Select order_id from orders o
Where Not Exists
(Select * From order_products
Where order_id = o.order_id
And cancelled = 1)
If the above assumption is not true, then you also need:
Select order_id from orders o
Where Exists
(Select * From order_products
Where order_id = o.order_id)
And Not Exists
(Select * From order_products
Where order_id = o.order_id
And cancelled = 1)

The fastest way will be this:
SELECT order_id
FROM orders o
WHERE customer_id = 1234
AND
(
SELECT canceled
FROM order_products op
WHERE op.order_id = o.order_id
ORDER BY
canceled DESC
LIMIT 1
) = 0
The subquery will return 0 if and only if there had been some products and they all had been canceled.
If there were no products at all, the subquery will return NULL; if there is at least one uncanceled product, the subquery will return 1.
Make sure you have an index on order_products (order_id, canceled)

Something like this? This assumes that every order has at least one product, otherwise this query will return also orders without any products.
select order_id
from orders o
where not exists (select 1 from order_products op
where canceled = 0
and op.order_id = o.order_id
)
and o.customer_id = 1234

SELECT customer_id, order_id, count(*) AS product_count, sum(canceled) AS canceled_count
FROM orders JOIN order_products
ON orders.order_id = order_products.order_id
WHERE customer_id = <<VALUE>>
GROUP BY customer_id, order_id
HAVING product_count = canceled_count

You can try something like this
select orders.order_id
from #orders orders inner join
#order_products order_products on orders.order_id = order_products.order_id
where order_products.customer_id = 1234
GROUP BY orders.order_id
HAVING SUM(order_products.canceled) = COUNT(order_products.canceled)

Since we don't know the database platform, here's an ANSI standard approach. Note that this assumes nothing about the schema (i.e. data type of the cancelled field, how the cancelled flag is set (i.e. 'YES',1,etc.)) and uses nothing specific to a given database platform (which would likely be a more efficient approach if you could give us the platform and version you are using):
select op1.order_id
from (
select op.order_id, cast( case when op.cancelled is not null then 1 else 0 end as tinyint) as is_cancelled
from #order_products op
) op1
group by op1.order_id
having count(*) = sum(op1.is_cancelled);

Related

SQL JOIN get all records that do not match certain criteria

I have two tables
Order and Invoice.
Order can have multiple invoices. Each invoice record has a state - paid or unpaid.
Order Invoice
O-123. i1 (paid)
O-123. i2 (unpaid)
O-123. i3(unpaid)
O-456 i4(paid)
O-456 i4(paid)
O-678. i5 (paid)
O-678 i6 (paid)
I need to get a list of all order which have no unpaid invoice. In this case it should return o456 and o678.
Sample query
select * from core.order as o
inner join
invoices as inv
on o.id = inv.order_id
where inv.status is paid
You can use not exists for that. (Assumed datatype of status column as varchar)
select * from core.order as o
where not exists
(
select 1 from invoices as inv where status='unpaid' and o.id=inv.order_id
)
One canonical approach uses aggregation:
SELECT o.id
FROM core.order o
LEFT JOIN invoices inv
ON inv.order_id = o.id
GROUP BY o.id
HAVING COUNT(CASE WHEN inv.status = 'unpaid' THEN 1 END) = 0;
One method is using not exists
select *
from core.order o
where not exists (
select 1
from invoices as inv
where o.id = inv.order_id and inv.status is 'unpaid'
)

Oracle SQL if statement?

how would it be possible to check if a certain value in salesman_id is null, and if it is, assign it to something else (in this case, 0)?
here's what i've wrote up so far:
SELECT O.SALESMAN_ID, SUM(OI.UNIT_PRICE * QUANTITY)
FROM ORDERS O, ORDER_ITEMS OI
GROUP BY O.SALESMAN_ID
ORDER BY O.SALESMAN_ID;
I would suggest:
SELECT COALESCE(O.SALESMAN_ID, 0) as SALESMAN_ID, SUM(OI.UNIT_PRICE * QUANTITY)
FROM ORDERS O JOIN
ORDER_ITEMS OI
ON o.ORDER_ID = OI.ORDER_ID. -- guessing at the relationship
GROUP BY COALESCE(O.SALESMAN_ID, 0)
ORDER BY COALESCE(O.SALESMAN_ID, 0);
Your query as written would produce non-sensical results. Always use proper, explicit, standard, readable JOIN syntax. Never use commas in the FROM clause.
SELECT nvl(O.SALESMAN_ID,0), SUM(OI.UNIT_PRICE * QUANTITY)
FROM ORDERS O, ORDER_ITEMS OI
GROUP BY nvl(O.SALESMAN_ID,0)
ORDER BY 1;
For completeness sake, a further option would be...
SELECT salesman_id, SUM(total)
FROM (
SELECT CASE WHEN o.salesman_id IS NULL
THEN 0
ELSE o.salesman_id as SALESMAN_ID,
OI.UNIT_PRICE * QUANTITY AS total
FROM ORDERS O JOIN
ORDER_ITEMS OI
ON o.ORDER_ID = OI.ORDER_ID
) AS ilv
GROUP BY salesman_id;
But null is a value too....
SELECT CASE WHEN ilv.salesman_id IS NULL
THEN 0
ELSE ilv.salesman_id as SALESMAN_ID,
total
FROM (
SELECT salesman_id
SUM(OI.UNIT_PRICE * QUANTITY) AS total
FROM ORDERS O JOIN
ORDER_ITEMS OI
ON o.ORDER_ID = OI.ORDER_ID
GROUP BY sles,an_id
) AS ilv;
I think the query is incorrect, you need to join orders and ORDER_ITEMS using order_id
scott

How to create a function that totals an order with several items?

I need to create a function which will return the total of an order. I've been given three tables with the following variables
Table 1 - Order
Order_ID
Date_Placed
Date_Fulfilled
Table 2 - Order Product
Order_ID
Product_ID
Product_Quantity
Table 3 - Product
Product_ID
Price
I'm struggling to put together a coherent function. Any help would be greatly appreciated.
I've already attempted to set up the function with joins between both tables, but am unable to figure out where I should be putting my equation.
BEGIN
SELECT order.order_id, SUM(product.price * order_item.quantity)
FROM `order`
JOIN `order_item` ON order.order_id = order_product.order_id
JOIN `product` ON order_product.product_id = product.product_id;
END $$
You might be surprised, but the orders table is not needed for this query. You can just aggregate off the other two tables:
SELECT oi.order_id, SUM(p.price * oi.quantity)
FROM order_item oi JOIN
product p
ON po.product_id = p.product_id
GROUP BY oi.order_id;
You'll need to take your select statement, and group it by your order.order_id. That way you'll have one row per order, with the sum total of that order.
SELECT order.order_id, SUM(product.price * order_item.quantity) as total_price
FROM `order`
JOIN `order_item` ON order.order_id = order_product.order_id
JOIN `product` ON order_product.product_id = product.product_id
GROUP BY order.order_id
this will work:
SELECT order.order_id, SUM(product.price * order_item.quantity)
FROM order o,
JOIN order_item oi,
JOIN product p where
o.order_id = oi.order_id and
oi.product_id = p.product_id
group by order_product.product_id = product.product_id;

Segment purchases based on new vs returning

I'm trying to write a query that can select a particular date and count how many of those customers have placed orders previously and how many are new. For simplicity, here is the table layout:
id (auto) | cust_id | purchase_date
-----------------------------------
1 | 1 | 2010-11-15
2 | 2 | 2010-11-15
3 | 3 | 2010-11-14
4 | 1 | 2010-11-13
5 | 3 | 2010-11-12
I was trying to select orders by a date and then join any previous orders on the same user_id from previous dates, then count how many had orders, vs how many didnt. This was my failed attempt:
SELECT SUM(
CASE WHEN id IS NULL
THEN 1
ELSE 0
END ) AS new, SUM(
CASE WHEN id IS NOT NULL
THEN 1
ELSE 0
END ) AS returning
FROM (
SELECT o1 . *
FROM orders AS o
LEFT JOIN orders AS o1 ON ( o1.user_id = o.user_id
AND DATE( o1.created ) = "2010-11-15" )
WHERE DATE( o.created ) < "2010-11-15"
GROUP BY o.user_id
) AS t
Given a reference data (2010-11-15), then we are interested in the number of distinct customers who placed an order on that date (A), and we are interested in how many of those have placed an order previously (B), and how many did not (C). And clearly, A = B + C.
Q1: Count of orders placed on reference date
SELECT COUNT(DISTINCT Cust_ID)
FROM Orders
WHERE Purchase_Date = '2010-11-15';
Q2: List of customers placing order on reference date
SELECT DISTINCT Cust_ID
FROM Orders
WHERE Purchase_Date = '2010-11-15';
Q3: List of customers who placed an order on reference date who had ordered before
SELECT DISTINCT o1.Cust_ID
FROM Orders AS o1
JOIN (SELECT DISTINCT o2.Cust_ID
FROM Orders AS o2
WHERE o2.Purchase_Date = '2010-11-15') AS c1
ON o1.Cust_ID = c1.Cust_ID
WHERE o1.Purchase_Date < '2010-11-15';
Q4: Count of customers who placed an order on reference data who had ordered before
SELECT COUNT(DISTINCT o1.Cust_ID)
FROM Orders AS o1
JOIN (SELECT DISTINCT o2.Cust_ID
FROM Orders AS o2
WHERE o2.Purchase_Date = '2010-11-15') AS c1
ON o1.Cust_ID = c1.Cust_ID
WHERE o1.Purchase_Date < '2010-11-15';
Q5: Combining Q1 and Q4
There are several ways to do the combining. One is to use Q1 and Q4 as (complicated) expressions in the select-list; another is to use them as tables in the FROM clause which don't need a join between them because each is a single-row, single-column table that can be joined in a Cartesian product. Another would be a UNION, where each row is tagged with what it calculates.
SELECT (SELECT COUNT(DISTINCT Cust_ID)
FROM Orders
WHERE Purchase_Date = '2010-11-15') AS Total_Customers,
(SELECT COUNT(DISTINCT o1.Cust_ID)
FROM Orders AS o1
JOIN (SELECT DISTINCT o2.Cust_ID
FROM Orders AS o2
WHERE o2.Purchase_Date = '2010-11-15') AS c1
ON o1.Cust_ID = c1.Cust_ID
WHERE o1.Purchase_Date < '2010-11-15') AS Returning_Customers
FROM Dual;
(I'm blithely assuming MySQL has a DUAL table - similar to Oracle's. If not, it is trivial to create a table with a single column containing a single row of data. Update 2: bashing the MySQL 5.5 Manual shows that 'FROM Dual' is supported but not needed; MySQL is happy without a FROM clause.)
Update 1: added qualifier 'o1.Cust_ID' in key locations to avoid 'ambiguous column name' as indicated in the comment.
How about
SELECT * FROM
(SELECT * FROM
(SELECT CUST_ID, COUNT(*) AS ORDER_COUNT, 1 AS OLD_CUSTOMER, 0 AS NEW_CUSTOMER
FROM ORDERS
GROUP BY CUST_ID
HAVING ORDER_COUNT > 1)
UNION ALL
(SELECT CUST_ID, COUNT(*) AS ORDER_COUNT, 0 AS OLD_CUSTOMER, 1 AS NEW_CUSTOMER
FROM ORDERS
GROUP BY CUST_ID
HAVING ORDER_COUNT = 1)) G
INNER JOIN
(SELECT CUST_ID, ORDER_DATE
FROM ORDERS) O
USING (CUST_ID)
WHERE ORDER_DATE = [date of interest] AND
OLD_CUSTOMER = [0 or 1, depending on what you want] AND
NEW_CUSTOMER = [0 or 1, depending on what you want]
Not sure if that'll do the whole thing, but it might provide a starting point.
Share and enjoy.
select count(distinct o1.cust_id) as repeat_count,
count(distinct o.cust_id)-count(distinct o1.cust_id) as new_count
from orders o
left join (select cust_id
from orders
where purchase_date < "2010-11-15"
group by cust_id) o1
on o.cust_id = o1.cust_id
where o.purchase_date = "2010-11-15"

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