MySQL JOIN "WHEN"- is such a thing possible? - mysql

Background:
I'm running a query which gets the total widgets, sprockets, and gizmos ordered by the customer during the last month
I also have a Total Orders column which runs a subquery counting every order in that month
The reason I do Total Orders in a subquery, instead of just adding the three columns, is because I also have a further two Total Orders columns for the two previous months
I have an orders table that stores financial records, and separate tables for actual product details for widgets, sprockets, and gizmos
The problem:
Occasionally, a widget might be 'half-deleted' (don't ask!) - where it has an orders record, but not a corresponding widgets record
I do not want to count this in the Total Widgets column - which is easy enough, as I just do a JOIN
However, I also do not want to count this in the Total Orders column...
My current query looks like this for Total Widgets:
SELECT
COUNT(orders.id)
FROM orders
JOIN widgets ON widgets.id = orders.item_id
WHERE orders.product_id = 1 -- Product ID 1 is a Widget
AND orders.date BETWEEN "2014-09-01 00:00:00" AND "2014-09-30 23:59:59"
So this will get all 'accurate' widgets, with an intact widget table record.
Here is my current query for Total Orders:
SELECT
COUNT(orders.id) AS count
FROM orders
JOIN widgets ON widgets.id = orders.item_id AND orders.product_id = 1
WHERE orders.date BETWEEN "2014-09-01 00:00:00" AND "2014-09-30 23:59:59"
So my thinking is that the above query should only JOIN when the order has a product_id of 1. However, it JOINs in every case. Which means if we've ordered 10 widgets (2 of which have been half-deleted), 5 sprockets, and 5 gizmos, rather than showing 18 orders, it only shows 8. Changing to a LEFT JOIN shows 20, which is still wrong, it should be 18.
Hopefully the above makes sense - thanks in advance.

okay so... the straight join doesn't work because you filter for orders.product_id = 1 and so it won't count sprockets or gizmos. just widgets. and if you have 2 half deleted, but 10 on record... 8 is the correct answer. the left outer join lets it join on rows where product_id <> 1, too, so all rows from orders (which contains order records on widgets, sprockets, gizmos - half deleted or not). but this also allows those 2 half deleted rows.
why don't you just do a straight join? if you want the answer 18, just join orders and products and count it. do not filter for widgets only. do not do a left outer if you don't want half deleted.
create table product (
product_id int,
product_name varchar(20)
);
insert into product values (1,'widget');
insert into product values (2,'sprocket');
insert into product values (3,'gizmo');
create table `order` (
order_id int,
widget_id int
);
insert into `order` values (1,1);
insert into `order` values (1,2);
insert into `order` values (1,3);
insert into `order` values (1,4); -- half deleted
insert into `order` values (2,1);
insert into `order` values (2,2);
insert into `order` values (3,3);
insert into `order` values (3,4); -- half deleted
insert into `order` values (4,4); -- half deleted
select o.order_id, count(*)
from `order` as o
join product as p
on o.widget_id=p.product_id
group by o.order_id
+----------+----------+
| order_id | count(*) |
+----------+----------+
| 1 | 3 |
| 2 | 2 |
| 3 | 1 |
+----------+----------+
3 rows in set (0.00 sec)
mysql> select o.order_id,
-> p.product_name,
-> (case when p.product_id is null
-> then 'half_deleted' else '' end) as half_deleted
-> from `order` as o
-> left outer join product as p
-> on o.widget_id=p.product_id
-> order by order_id ;
+----------+--------------+--------------+
| order_id | product_name | half_deleted |
+----------+--------------+--------------+
| 1 | gizmo | |
| 1 | NULL | half_deleted |
| 1 | widget | |
| 1 | sprocket | |
| 2 | widget | |
| 2 | sprocket | |
| 3 | gizmo | |
| 3 | NULL | half_deleted |
| 4 | NULL | half_deleted |
+----------+--------------+--------------+

Related

Output rows with null value only if there isn't the same row with a non-null value

Trying to formulate my question as good as I can...
I have a pricing table with historic data in it. So per item there is a price for certain dates. The difficulty is that the rows also have a type (1 = price for purchase order, 2 = sales order) and a VendorID.
That VendorID can be filled on a row: the price on that row is then for that specific vendor. If there is no row for a certain item in this table that has a VendorID, but it does have a row where VendorID is null, that row should be in the result.
So, if there are two rows in the result, one with a VendorID value and one with the VendorID being null, the row with the value should be in the result set and the row with the null value may not be in the result set.
Also, the result set should only contain the prices that are the newest, so i have to take in account the 'FromDate'.
The name of the column 'VendorID' is not well chosen because the rows with type = 2 are for sales orders, but let's forget about that for now ;-)
If I want all items for type = 1, would like to have the following result set:
ID | ItemID | FromDate | Type | VendorID | price
------------------------------------------------
1 | 1. | 2020-01-01 | 1 | 97 | 2.45
9 | 2 | 2020-02-15 | 1 | 97 | 3.88
7 | 3 | 2020-01-01 | 1 | 97 | 2.55
Suppose IDs 3,4 and 9 wheren't in the table (so, no pricing for item 2 for specific VendorID 97), the result should be:
ID | ItemID | FromDate | Type | VendorID | price
------------------------------------------------
1 | 1 | 2020-01-01 | 1 | 97 | 2.45
13 | 2 | 2020-01-01 | 1 | NULL | 999.45
7 | 3 | 2020-01-01 | 1 | 97 | 2.55
For ItemID 2 this would mean that there isn't a specific price set for VendorID 97 but there is a general price set (VendorID is null) and this price should now be placed in the result set.
I hope I explained it more clearly now....
I've written loads of queries now and also googled a lot but I cannot find how to make it do what I want. I tried distinct, sorting, but no luck. Must be something simple but I can't find it.
Up until now I have the following Mysql query but a) it outputs both the rows where VendorID is null and where it has value and b) I think its very overcomplicated but can't figure out how to make it simpler and faster.
SELECT I.ItemID, I.Name, b.vendorID, b.max_date, IP.Price, T.Percentage
FROM Items I
JOIN ( SELECT ItemID, VendorID, MAX(FromDate) max_date, type
FROM ItemPrices
WHERE Type = 1 AND FromDate < '2020-02-30'
AND VendorID = (SELECT ID
FROM Vendors
WHERE VendorID = 'V001')
OR VendorID IS NULL
GROUP BY ItemID, VendorID
) b ON I.ID = b.ItemID
JOIN ItemPrices IP ON IP.ItemID = b.ItemID
AND IP.Type = b.type
AND IP.FromDate = b.max_date
AND (IP.VendorID = b.VendorID OR IP.VendorID IS NULL)
LEFT JOIN TaxCodes T ON T.ID =
( SELECT TC.TaxCodeID
FROM TaxCombinations TC
WHERE TC.Direction = 1
AND TC.TaxAreaID = (SELECT TaxArea
FROM Vendors
WHERE ID = (SELECT ID
FROM Vendors
WHERE VendorID = 'V001') )
AND TC.ItemTaxID = I.ItemTaxID )
ORDER BY I.ItemID ASC
Also looked at the following urls but still don't know what to do:
Distinct rows with non-null values taking precedence over nulls
Select the non-null value if exists else null, but always select the row
Can someone please help me?
If you want to get the newest for each ItemID corresponding to the Type selection, you can make a sub-query to return the newest price first then join the original table to show it in the end output. Below are the example query:
SELECT A.*
FROM ItemPrices A
JOIN ( SELECT itemid,
TYPE,
MAX(fromdate) AS mdt
FROM ItemPrices
GROUP BY itemid,TYPE ) B
ON A.itemid=B.itemid
AND A.type=B.type
AND A.fromdate=B.mdt
WHERE A.type=1
ORDER BY A.itemid;
You can view the demo here : https://www.db-fiddle.com/f/7YCaiLYz9DE11wnijWEdi/3

SQL query to get SUM from more dependent tables

I have the following MySQL DB structure:
table sales_order - id, name, ...
id | name
------------------
1 | Order Test
table sales_order_item - id, order_id, name, amount_dispatched ...
id | order_id | name | amount_dispatched
------------------------------------------
1 | 1 | Item 1 | 5
2 | 1 | Item 2 | 10
table sales_order_item_invoice - id, item_id, amount, ...
id | item_id | amount
---------------------
1 | 1 | 3
2 | 2 | 5
3 | 2 | 5
These three tables are in chain via the foreign keys. Table "invoice" can have more rows for one row in "item". Table "item" can have more rows for one row in "order".
Now, I need to create SQL query that returns all rows from table sales_order and appends there some data from the other tables - amount_dispatched and amount_invoiced:
dispatched = sum of all order's items' amount_dispatched
invoiced = sum of all invoices' amount (or 0 if no invoice exists)
Such query seems to be straightforward:
SELECT
`sales_order`.*,
SUM(`sales_order_item`.`amount_dispatched`) AS dispatched,
SUM(`sales_order_item_invoice`.`amount`) AS invoiced,
FROM `sales_order`
LEFT JOIN `sales_order_item` ON `sales_order`.`id` = `sales_order_item`.`order_id`
LEFT JOIN `sales_order_item_invoice` ON `sales_order_item`.`id` =`sales_order_item_invoice`.`item_id`
GROUP BY `sales_order`.`id`
The result contains all orders - ok
The result contains sum of invoices amount - ok
The result of "amount_dispatched" is invalid - if the item has more rows in item_invoice, the item's amount is summed several times, so for the example above, I get:
id | name | dispatched | invoiced
---------------------------------------
1 | Order Test | 25 | 13
Amount_dispatched is 25, but I would expect it to be 15.
Any idea how to correct my SQL query?
Thank you.
Firstly, use subquery do aggregation for invoice amount in sales_order_item_invoice, then left join.
SELECT
`sales_order`.*,
SUM(`sales_order_item`.`amount_dispatched`) AS dispatched,
SUM(t.`amount`) AS invoiced
FROM `sales_order`
LEFT JOIN `sales_order_item` ON `sales_order`.`id` = `sales_order_item`.`order_id`
LEFT JOIN (
SELECT item_id, SUM(amount) AS amount
FROM `sales_order_item_invoice`
GROUP BY item_id
) t ON `sales_order_item`.`id` = t.`item_id`
GROUP BY `sales_order`.`id`

Insert multiple rows from one column in a table to the same column from the same table

Okay, so I have the following structure in my product_to_store table:
+----------------------+--------------------+
| product_id - int(11) | store_id - int(11) |
+----------------------+--------------------+
+----------------------+--------------------+
| 1000 | 0 |
| 1000 | 6 |
| 1005 | 0 |
| 1010 | 0 |
| 1010 | 6 |
...
Basically, I need to have a store_id of value 6 for every product_id. For example, the product (product_id) with ID 1005 only has a store_id record of 0. I want it to have another record/row where product_id is equal to 6. Products with ID 1000 and 1010 are what they should be like (they have a record of store_id that is equal to 6).
I tried to run the following query in order insert a new row where only product_id is set:
INSERT INTO `product_to_store`
(product_id) SELECT `product_id`
FROM `product_to_store`
WHERE product_id != 6
And then consider running another query to update all rows where store_id is null with the value of 6. However, I get:
#1062 - Duplicate entry '1000-0' for key 'PRIMARY'.
Any way in which I can accomplish this without having to use a loop in PHP or something rather unpractical?
INSERT INTO `product_to_store` (product_id,store_id)
SELECT DISTINCT p1.product_id, 6 as store_id
FROM
product_to_store p1
LEFT JOIN product_to_store x
ON p1.product_id = x.product_id
AND x.store_id = 6
WHERE
x.product_id IS NULL;
http://sqlfiddle.com/#!9/01ac7a
You basically want to insert products where there doesn't already exist a product with store_id of 6.
INSERT INTO `product_to_store` (product_id,store_id)
SELECT DISTINCT product_id, 6 as store_id
FROM product_to_store p1
WHERE NOT EXISTS (SELECT 1 FROM product_to_store p2 WHERE p2.store_id = 6
AND p2.product_id = p1.product_id)

How to calculate running total grouped by Order No

Trying to create a running total for orders in SQL Server 2008, similar to the below table (Order No & Order Total columns exist in my SQL Server table), tried using a recursive cte but my results were a running total for all orders, not grouped by order no. Any suggestions how to have the running total grouped by the order no? Thanks
---------------------------------------------------------
| Order No. | Order Total | Running Total for Order No |
---------------------------------------------------------
| 1 | $10,000 | $10,000 |
---------------------------------------------------------
| 1 | -$5,000 | $5,000 |
---------------------------------------------------------
| 1 | $3,000 | $8,000 |
---------------------------------------------------------
| 2 | $2,500 | $2,500 |
---------------------------------------------------------
| 2 | $5,000 | $7,500 |
---------------------------------------------------------
| 2 | $4,000 | $11,000 |
---------------------------------------------------------
I would do this is with an Instead of Insert Trigger. The trigger would subtract/add from the groups first value. Obviously this should of been done at the creation of the table but you could add it after you make table update.
Keep in mind in order for the below code to work, you would need a primary key on the Order table
CREATE TABLE Orders
(
id INT IDENTITY(0, 1) PRIMARY KEY
, orderNo INT
, orderTotal MONEY
, runningTotal MONEY
);
INSERT INTO Orders
VALUES
(1,10000,10000),
(1,-5000,5000),
(1,3000,8000),
(2,2500,2500),
(2,5000,7500),
(2,4000,11500);
GO
--CREATE TRIGGER
CREATE TRIGGER trg_RunningTotal ON Orders
INSTEAD OF INSERT
AS
BEGIN
DECLARE #PreviousTotal MONEY =
(
SELECT TOP 1
a.runningTotal
FROM Orders AS a
INNER JOIN INSERTED AS b ON a.orderNo = b.orderNo
WHERE a.orderno = b.Orderno
ORDER BY a.id DESC
);
INSERT INTO Orders
SELECT
orderno,
orderTotal,
(#PreviousTotal + orderTotal) AS runningTotal
FROM INSERTED;
END;
--Insert new record
INSERT INTO orders
VALUES
(1,1000,NULL);
--View newly added record
SELECT
*
FROM orders
WHERE orderno = 1;
You need to following query:
SELECT orderno,
SUM((CASE WHEN ISNUMERIC(ordertotal)=1
THEN CONVERT(MONEY,ordertotal) ELSE 0 END)
)
AS [Converted to Numeric]
FROM price group by orderno

left join, return non matching rows, where clause on right table, group by

Sorry about the complicated title.
I have two tables, customers and orders:
customers - names may be duplicated, ids are unique:
name | cid
a | 1
a | 2
b | 3
b | 4
c | 5
orders - pid is unique, join on cid:
pid | cid | date
1 | 1 | 01/01/2012
2 | 1 | 01/01/2012
3 | 2 | 01/01/2012
4 | 3 | 01/01/2012
5 | 3 | 01/01/2012
6 | 3 | 01/01/2012
So I used this code to get a count:
select customers.name, orders.date, count(*) as count
from customers
left JOIN orders ON customers.cid = orders.cid
where date between '01/01/2012' and '02/02/2012'
group by name,date
which worked fine but didnt give me null rows when the cid of customers didnt match a cid in orders, e.g. name-c, id-5
select customers.name, orders.date, count(*) as count
from customers
left JOIN orders ON customers.cid = orders.cid
AND date between '01/01/2012' and '02/02/2012'
group by name,date
So I changed the where to apply to the join instead, which works fine, it gives me the null rows.
So in this example I would get:
name | date | count
a | 01/01/2012 | 3
b | null | 1
b | 01/01/2012 | 3
c | null | 1
But because names have different cid's it is giving me a null row even if the name itself does have rows in orders, which I don't want.
So I'm looking for a way for the null rows to only be returned when any other cid's that share the same name also do not have any rows in orders.
Thanks for any help.
---EDIT---
I have edited the counts for null rows, count never returns null but 1.
The result of
select * from (select customers.name, orders.date, count(*) as count
from customers
left JOIN orders ON customers.cid = orders.cid
AND date between '01/01/2012' and '02/02/2012'
group by name,date) as t1 group by name
is
name | date | count
a | 01/01/2012 | 3
b | null | 1
c | null | 1
First, select your date grouped by (name, date), excluding NULLs, then join with a set of distinct names:
SELECT names.name, grouped.date, grouped.count
FROM ( SELECT DISTINCT name FROM customers ) as names
LEFT JOIN (
SELECT customers.name, orders.date, COUNT(*) as count
FROM customers
LEFT JOIN orders ON customers.cid = orders.cid
WHERE date BETWEEN '01/01/2012' AND '02/02/2012'
GROUP BY name,date
) grouped
ON names.name = grouped.name
The best approach would be Group them together based on Cid's and then other parameters.
So you would get the proper output with NULL values based on Left Outer Join.