Forcing all rows from first table of a join - mysql

I have three tables, machines holding vending machines, products holding all possible products, and machines_products which is the intersection of the two, giving how many of each product line is stocked in a particular machine. If a product is not stocked in a machine, there is no corresponding row in the third table.
DESCRIBE machines_products;
+------------+------------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+------------+------------------+------+-----+---------+-------+
| machine_id | int(10) unsigned | NO | PRI | 0 | |
| product_id | int(10) unsigned | NO | PRI | 0 | |
| quantity | int(10) unsigned | NO | | 0 | |
+------------+------------------+------+-----+---------+-------+
Each product has a category (think chocolate bars vs. drinks bottles) and a machine knows what category of products it can vend. I want a result table of all products for the category, with a quantity for a specific machine. I have got as far as this:
SELECT products.*, SUM(quantity) qty
FROM products
LEFT JOIN machines_products USING (product_id)
WHERE machine_id=m AND category_id=c
GROUP BY product_id;
The problem is that this filters out all rows where there is no quantity, whereas what I want is all rows from the left table, and NULL/0 in the qty column if there are no corresponding rows in the right-hand table.
BTW: this is not a homework question! I am 30 and sitting in my office :o)

SELECT p.*
, SUM(mp.quantity) AS qty
FROM products p
LEFT JOIN machine_products mp
ON mp.product_id = p.product_id
AND mp.machine_id = m --- this condition moved from WHERE to ON
WHERE p.category_id = c
GROUP BY p.product_id

Actually I figured out the answer a short while after posting. The trick is to avoid specifying either of the columns from the third table's primary key (i.e. machine_id and product_id) in the WHERE clause. By using an AND in the JOIN's ON condition, and specifying the machine ID there, I get the result I was looking for.
SELECT products.*, quantity
FROM products
LEFT JOIN machines_products
ON products.product_id=machines_products.product_id
AND machine_id=m
WHERE category_id=c
The COALESCE() function suggested by Brendan was not necessary in my case, since I check the value with PHP's empty() function, so NULL is fine.
As it turns out, there was never a need for GROUP BY, which I had been playing with when posting the question.

SUM returns NULL if a single value in the equation is NULL. COALESCE the value first and then SUM:
SELECT p.*, SUM(COALESCE(mp.quantity, 0)) AS qty
FROM products p
LEFT JOIN machine_products mp ON mp.product_id = p.id
WHERE mp.machine_id = m
AND p.category_id = c
GROUP BY p.id
I assumed you have a column in products called id. Rename if it's something different...

SELECT p.id, SUM(mp.quantity) AS qty
FROM products p
LEFT JOIN machines_products mp ON p.id=mp.product_id
WHERE mp.machine_id=m
AND p.category_id=c
GROUP BY p.id;

Related

MySQL - return single record by joining multiple tables with multiple WHERE/OR clauses

Did many searches and were able to write all the JOIN queries except this one. I have 3 tables, which look like:
TABLE - accounts
account_id | account_email
1 | aa#bb.com
2 | cc#dd.com
TABLE - products
product_id | product_name
1 | name1
2 | name2
TABLE - licenses
license_id | account_id | product_id | license_code
1 | 1 | 1 |
2 | 1 | 2 |
3 | 0 | 1 | abc123
I know account_email, product_id and license_code (this one can be empty) variables, and need to check if client has license for selected product (searching by account_email or license_code).
The problem is that account_id sometimes can be 0 (in other words, client has license, but client's profile is not stored in the accounts table).
Trying to use this one, but it returns wrong (duplicated) results:
SELECT * FROM licenses
INNER JOIN products ON licenses.product_id=products.product_id AND products.product_id='X'
INNER JOIN accounts ON licenses.account_id=accounts.account_id AND accounts.account_email='XYZ' OR licenses.license_code='ZZZ'
The question: how do I rewrite query, so I can find a record by account_email or license_code field? Put simply, if account_id is not 0 (profile exists), I should see data from 3 tables (accounts, products, licenses). If account_id is 0, I should see data from 2 tables (values from accounts table should be displayed as empty/null).
Needless to say, account_email and license_code fields are unique.
You just need to adjust the parentheses. However, I would use a separate WHERE clause:
SELECT *
FROM licenses l INNER JOIN
products p
ON l.product_id = p.product_id INNER JOIN
accounts a
ON l.account_id = a.account_id
WHERE p.product_id = 'X' AND
(a.account_email='XYZ' OR l.license_code = 'ZZZ')
You can keep the filters in the ON clauses -- there is nothing wrong with that. I just prefer to separate join conditions from filtering conditions. The important part are the parentheses, so the ON clause is not interpreted as:
ON (l.account_id = a.account_id AND a.account_email = 'XYZ') OR
licenses.license_code = 'ZZZ'
This is the interpretation without parens.
After some more code modification and testing, here's the code which works perfectly. It's a modified Gordon Linoff's code (original code didn't work when only license_code was known, and email address wasn't known). So most of credits go to Gordon Linoff for his efforts.
SELECT *
FROM licenses l JOIN
products p
ON l.product_id = p.product_id LEFT JOIN
accounts a
ON l.account_id = a.account_id
WHERE p.product_id = 'X' AND
(a.account_email='XYZ' OR l.license_code = 'ZZZ')

Greatest n per group in a many to many relationship

I know there are many questions similar to this one but it seems that nothing fits my problem. I've spent quite a few hours researching this problem and came up with a query that doesn't select the last image, more on that later. So, the problem is a have 3 tables
table: images
| Field | Type
+-------------------+------------------
| id | int(10) unsigned
| filename | varchar(255)
| created_at | timestamp
| updated_at | timestamp
table: offers
| Field | Type
+-------------------+------------------
| id | int(10) unsigned
| message | varchar(255)
| created_at | timestamp
and a table connecting them: offer_images
| Field | Type
+----------+------------------
| offer_id | int(10) unsigned
| image_id | int(10) unsigned
So, the question is:
How do I select all offers with the last updated image (based on updated_at) from the images table that is linked to the offer. Here is what I got so far:
SELECT `o`.*, `i`.`filename`
FROM `offer_images` AS `oi`
INNER JOIN `offers` AS `o` on `oi`.`offer_id` = `o`.`id`
INNER JOIN `images` as `i` on `oi`.`photo_id` = `i`.`id`
GROUP BY `o`.`id`
The query selects everything and it's working besides that it ignores the updated_at field.
Try this:
SELECT o.*, i.*
FROM offers AS o
INNER JOIN (
-- Get the latest update_at date per offer_id
SELECT oi.offer_id, MAX(updated_at) AS max_updated_at
FROM offer_images AS oi
INNER JOIN images AS i ON oi.image_id = i.id
GROUP BY oi.offer_id
) AS d ON o.id = d.offer_id
INNER JOIN offer_images AS oi ON oi.offer_id = d.offer_id
INNER JOIN images AS i ON i.id = oi.image_id AND i.updated_at = d.max_updated_at
The query uses a derived table to get the latest update_at date per offer_id. Using this date we can join back to the images table in order to get the greatest-n-per-group record.

SUM() not working correctly when left joining Many to Many table

Note: I see similar SQL questions but nothing specific to MySQL on how to solve this issue.
I have the following query which sums a product value by day for a period based on the sale date from the sales table, products can be filtered based on categories which is why I need to have the left join, categories also need to be displayed along with the rest of the information. Due to project requirements I can not do any processing outside of this MySQL query.
select `sales`.`sell_date` as `date`, SUM(product_value.value) as value from
`sales` left join `products` on `sales`.`product_id` = `products`.`id` left join
`product_value` on `product_value`.`product_id` = `products`.`id` and
`sales`.`sell_date` BETWEEN product_value.date_from AND
IFNULL(product_value.date_to, '2999-01-01')
left join `product_product_category` on `product_product_category`.`product_id`
= `products`.`id` left join `product_categories` on
`product_product_category`.`product_category_id` = `product_categories`.`id`
left join `users` on `sales`.`seller_id` = `users`.`id`
where `sales`.`sell_date` between "2016-02-01" and "2016-02-29" and `product_value`.`deleted_at` is null
and `products`.`id` in ("178") and `sales`.`deleted_at` is null group by
`sales`.`sell_date` order by `sales`.`sell_date` asc
The above query will get a sum which is doubled or trippled when there is two or three categories for a product. Categories can be things such as color, size, etc.
The sum works fine when I remove the following from the query which has lead me to believe that the many to many relationship here is causing the issue.
left join `product_product_category` on `product_product_category`.`product_id` =
`products`.`id` left join `product_categories` on
`product_product_category`.`product_category_id` = `product_categories`.`id`
How can I prevent this left join from causing my SUM() to give me the wrong total value?
Using Distinct on product_value.value will not work as product values can be the same for many products.
My tables
sales
ID | sell_date | product_id
----------------------------
2 | 2016-02-15 | 178
product_value
ID | value | date_from | date_to | product_id
-------------------------------------------------
1 | 500 | 2016-01-01 | NULL | 178
2 | 500 | 2015-01-01 | 2015-12-01 | 392
products
ID | name
----------
178 | ProductName
product_product_category
product_id | product_category_id
--------------------------------
178 | 1
178 | 2
product_categories
ID | name
---------
1 | Red
2 | Large
So to make this clear, if I run the above query on these tables I would get value = 1000 but value should be 500. How can I make sure SUM() shows the correct value when joining many to many relationships?
You can remove left join and add filters in where as subquery
...
where
...
and exists (
select 1
from `product_product_category`
inner join `product_categories` on `product_product_category`.`product_category_id` = `product_categories`.`id`
where `product_product_category`.`product_id` = `products`.`id`
and ....
and ....
)
...

MySQL join and column names

Let's say that I have the following tables in my MySQL database:
TABLE Products
| id | some_column1 | some_column2 |
TABLE ProductProperties
| id | product_id | name |
Oversimplified, but sufficient. Now I want to get all products with properties. I do:
SELECT * FROM `Products` JOIN `ProductProperties` ON Products.id = ProductProperties.product_id
What do I get?
| id | some_column1 | some_column2 | id | product_id | name |
It's not cool, and I want to make it cool in one of the two ways:
1) To get the array of objects like in Product table, but extended by one more member, which would be the array of properties which matched JOIN. I've sort of figured out already that it's impossible?
2) To get the array like this (I'd still have to iterate over it in PHP to join all properties in one product into one object):
| product_id | some_column1 | some_column2 | property_id | product_id | name |
So I'd like to rename the column ProductProperties.id into ProductProperties.property_id. If I could remove ProductProperties.product_id from the output too, that would be ideal, but for now, I only want the way to rename one column in the output. Or to prefix it by table name. Or something like that.
Doable?
You should explicitly name the columns and not use *. Then, don't return redundant columns:
SELECT p.id as productid, p.some_column1, p.some_column2,
pp.id as ProductPropertiesId, pp.name
FROM `Products` p JOIN `ProductProperties` pp
ON p.id = pp.product_id
Also, table aliases make such a query more readable.
SELECT Products.id product_id,
Products.some_column1,
Products.some_column2,
ProductProperties.id property_id,
ProductProperties.name
FROM `Products`
JOIN `ProductProperties`
ON Products.id = ProductProperties.product_id

mysql error in my query

i have to check in my products i am selling (mostly gaming consoles and games)
i want to see which products has which categories and this is my query:
select * From products left join products_categories on (product_id=id) ;
+------+------+------------+-------------+----------
| id | name | product_id | category_id | and more
+------+------+------------+-------------+----------
| 4 | Xbox | 4 | 2 |
| 5 | PS3 | 5 | 2 |
| 7 | BAD | NULL | NULL |
etc...
+------+------+------------+-------------+---------
here i have a product (#7 - BAD) that i don'T want to see since i removed the category,
I don't want to see the product without categories?
The LEFT JOIN command is used to combines null matching rows which are
stored in related tables In order to join these tables, the join table
require a common field (commonly called foreign key) from the left
table. This type of join requires keywords ON or USING.
Example:
SELECT *
From products
LEFT JOIN products_categories ON (product_id=id)
WHERE product_id IS NOT NULL;
Or you can use the INNER JOIN:
The JOIN or INNER JOIN command is used to combines non-null matching
rows which are stored in related tables In order to join these tables,
the join table require a common field (commonly called foreign key)
from the left table. This type of join requires keywords ON or USING.
Example:
SELECT * From products INNER JOIN products_categories ON (product_id=id);
Now, I would recommend to add a flag for inactive or active product, this way you don't need to remove the categories for a product if it's inactive. This way, if you want to re-activate it, simply turn the flag back to 1 or whatever flag you use.
Example:
SELECT *
FROM products
INNER JOIN products_categories ON (product_id=id)
WHERE products.is_active = 1;