This question already has answers here:
What's the difference between INNER JOIN, LEFT JOIN, RIGHT JOIN and FULL JOIN? [duplicate]
(3 answers)
Closed 5 years ago.
I am having trouble with a MySQL query (version 5.6.37). I think it merely needs a reorganizing of the query components, but I can not make it work.
Problem: when my JOIN returns no rows, the entire query returns no rows, even though the data matches the query.
Here is my current query (where '#' is the input):
SELECT pets.id,pet,collar,GROUP_CONCAT(petData.fleas) AS f_id
FROM titles
JOIN petWear ON pets.id = petWear
JOIN petData ON petWear.id = petData.id
WHERE pets.id = '#'
GROUP BY pets.id,pet,collar
Assuming a "pets" table like this:
id | pet
1 | cat
2 | dog
3 | fish
4 | snake
5 | rabbit
And a JOINed "petData" table like this:
id | fleas
1 | 1
1 | 2
1 | 3
1 | 4
2 | 5
Successful query:
If # = 1, then the query returns a single result:
id | pet | collar | f_id
1 | cat | gold | 1,2,3,4
Unsuccessful query:
If # = 5, then the query returns no result.
What I would like to have returned (for # = 5) is this single result (i.e. no result, or NULL, for "f_id"):
id | pet | collar | f_id
5 | rabbit | red |
Note that I have included the petWear table with the "collar" listing, just to state that a "normal" join needs to also be part of the picture.
Try the following code. Basically, if GROUP_CONCAT returns no values, it will print no_fleas
SELECT pets.id,pet,collar,IFNULL((GROUP_CONCAT(petData.fleas), 'no_fleas') AS f_id
FROM titles
JOIN petWear ON pets.id = petWear
JOIN petData ON petWear.id = petData.id
WHERE pets.id = '#'
GROUP BY pets.id,pet,collar
Related
I am codding a search page with multiple filters and I am wondering if this is the best approach to get the results.
Each result of the search has several attributes, here I am using two attributes to simplify the example.
The main 'items' table:
id_items
1
2
The 'languages' table:
id_languages | language_code
1 es
2 en
The 'attributes_one' table:
id_attributes_one
1
2
The 'attributes_one_translations' table:
id_attributes_one_translations | id_attributes_one | id_language_code | translation
1 | 1 | 1 | Oro
2 | 1 | 2 | Gold
3 | 2 | 1 | Plata
4 | 2 | 2 | Silver
The 'attributes_one_match' table:
id_attributes_one_match | id_attributes_one | id_items
1 | 1 | 1
2 | 2 | 1
3 | 1 | 2
The 'attributes_two' table:
id_attributes_two
1
The 'attributes_two_translations' table:
id_attributes_two_translations | id_attributes_two | id_language_code | translation
1 | 1 | 2 | 99% gold
The 'attributes_two_match' table:
id_attributes_two_match | id_attributes_two | id_items
1 | 1 | 1
The concept is one item can have 0 or more match of each attribute table, and that match can have 0 or more translations.
Here is the query I am using when the user selects the filters to get all the items that have the attribute_one 'Gold' or 'Silver' order by this attribute ascendant:
SELECT
i.id_items AS id,
GROUP_CONCAT(DISTINCT aot.translation ORDER BY aot.translation DESC SEPARATOR '!¡') AS attribute_one,
GROUP_CONCAT(DISTINCT att.translation ORDER BY att.translation DESC SEPARATOR '!¡') AS attribute_two
FROM
items i
LEFT JOIN
languages AS l ON l.language_code = 'en'
LEFT JOIN
attributes_one_match AS aom ON aom.id_items = i.id_items
LEFT JOIN
attributes_one_translations AS aot ON aot.id_attributes_one = aom.id_attributes_one
AND l.id_languages = aot.id_language_code
AND (MATCH (aot.translation) AGAINST ('"Gold"' IN BOOLEAN MODE)
OR MATCH (aot.translation) AGAINST ('"Silver"' IN BOOLEAN MODE))
LEFT JOIN
attributes_one AS ao ON ao.id_attributes_one = aom.id_attributes_one
LEFT JOIN
attributes_two_match AS atm ON atm.id_items = i.id_items
LEFT JOIN
attributes_two_translations AS att ON att.id_attributes_two = atm.id_attributes_two
AND l.id_languages = att.id_language_code
LEFT JOIN
attributes_two AS at ON at.id_attributes_two = atm.id_attributes_two
GROUP BY id
ORDER BY 2 ASC
The result I get is:
id | attribute_one | attribute_two
2 | Gold | null
1 | Silver!¡Gold | 99% gold
That result is what I was expecting. Now:
* The table items will have around 300k entries once the data base is filled.
* There are 28 attributes table to match with the item.
Each attribute table will have around 20k entries, and each translation table will have 2
times the entries of the table that represents.
* Each item will have from 0 to 20 match to each item table, so I think
I wont have problems using the function GROUP_CONCAT
I am concern about the performance because the search filter page I am doing updates itself by ajax each time the user change one of the filters (it updates the filters and the results). The max results per page will be 1000 items, I didn't put the LIMIT in the query of the example.
I am not an sql expert so I don't really know if what I am doing is the best approach. I would appreciate some feedback.
Thanks a lot!
I'm trying to get all courses which has only "module=5", any different course. As you can see below I have 4 courses, such as, course 2, 3, 4, 5. But just two of them has only "module=5", e.g. course 2, 5.
+----+--------+--------+
| id | course | module |
+----+--------+--------+
| 1 | 2 | 5 |
| 2 | 3 | 5 |
| 3 | 3 | 11 |
| 4 | 4 | 5 |
| 5 | 4 | 3 |
| 6 | 5 | 5 |
| 7 | 4 | 6 |
| 8 | 4 | 5 |
+----+--------+--------+
I've tryed do two queries, in the first I return all courses which has module=5 and in the second I return all courses which has module!=5, then I save in 2 files and execute the unix command diff to see the difference between both files.
save in a file all courses which has module=5:
SELECT DISTINCT fullname
FROM mdl_course
LEFT JOIN mdl_course_modules
ON mdl_course.id=mdl_course_modules.course
WHERE mdl_course_modules.module=5
into outfile '/tmp/forum';
save in file all courses which has module!=5:
SELECT DISTINCT fullname
FROM mdl_course
LEFT JOIN mdl_course_modules
ON mdl_course.id=mdl_course_modules.course
WHERE mdl_course_modules.module!=5
into outfile '/tmp/plus_forum';
Then, execute the difference:
$ diff forum plus_forum
But I'd like to return all courses which has only module=5 in only one query. Is it possible?
To make it simple let just solve the mdl_course_modules
SELECT course
FROM mdl_course_modules
GROUP BY course
HAVING SUM(module <> 5) = 0
AND SUM(module = 5) = 1 -- or >= 1
You can use a not in subquery to do this:
SELECT DISTINCT fullname
FROM mdl_course
LEFT JOIN mdl_course_modules
ON mdl_course.id=mdl_course_modules.course
WHERE mdl_course_modules.module=5
AND course.id not in (SELECT course
FROM mdl_course_modules
WHERE module <> 5)
One way to do this is to use the exists predicate with a correlated subquery.
select *
from mdl_course_modules t
where not exists (
select 1
from mdl_course_modules
where t.course = course -- reference the outer table
and module <> 5 -- and find rows that have any other module than 5
)
Sample SQL Fiddle
Since it's not completely clear what data is in what table you'll have to adjust the table and column names to suit your setup, but the concept should be clear.
This question already has answers here:
SQL query of multiple values in one cell
(2 answers)
Closed 7 years ago.
I have two tables:
Orders
+----+------+-------+
| id | name | notes |
+----+------+-------+
| 1 | Adam | 1,2 |
| 2 | Ema | 3 |
| 3 | Petr | 1,3 |
+----+------+-------+
Notes
+----+---------------------+
| id | text |
+----+---------------------+
| 1 | This is first note |
| 2 | This is second note |
| 3 | And third note |
+----+---------------------+
I need to to select row from Orders and group concat Text from second table based on Orders.notes.
If I use this statement
SELECT o.name, o.notes
,GROUP_CONCAT(DISTINCT n.text SEPARATOR ';') AS notes_text
FROM orders o
LEFT JOIN notes n ON n.id IN (1,2)
WHERE o.id = 1;
Result is as expected This is first note;This is second note
But if I use this statement that I need, where notes.id IN (orders.notes)
SELECT o.name, o.notes
,GROUP_CONCAT(DISTINCT n.text SEPARATOR ';') AS notes_text
FROM orders o
LEFT JOIN notes n ON n.id IN (o.notes)
WHERE o.id = 1
It only returns first text This is first note. Why?
SQLFiddle
SQL dbs will NOT "tear apart" a csv value in a field. These three fragments will parse/execute identically:
... n.id IN (o.notes)
... n.id IN ('1,2')
... n.id = '1,2'
Note the quotes. the 1,2 is treated as a monolithic string, not as two separate values spearated by a comma.
If you want to use this bad table design (you REALLY should normalize), then use FIND_IN_SET() instead.
Note that this will chain you to MySQL, and you lose portability.
I have a database in which I need to find some missing entries and fill them in.
I have a table called "menu", each restaurant has multiple dishes and each dish has 4 different language entries (actually 8 in the main database but for simplicity lets go with 4), I need to find out which dishes for a particular restaurant are missing any language entries.
select * from menu where restaurantid = 1
i get stuck there, something along the lines of where language 1 or 2 or 3 or 4 doesn't exist which is the complicated bit because I need to see the languages that exist in order to see the language that's missing because I can't display something that isn't there. I hope that makes sense?
In the example table below restaurant 2 dishid 2 is missing language 3, that's what i need to find.
+--------------+--------+----------+-----------+
| RestaurantID | DishID | DishName | Language |
+--------------+--------+----------+-----------+
| 1 | 1 | Soup | 1 |
| 1 | 1 | Soúp | 2 |
| 1 | 1 | Soupe | 3 |
| 1 | 1 | Soupa | 4 |
| 1 | 2 | Bread | 1 |
| 1 | 2 | Bréad | 2 |
| 1 | 2 | Breade | 3 |
| 1 | 1 | Breada | 4 |
| 2 | 1 | Dish1 | 1 |
| 2 | 1 | Dísh1 | 2 |
| 2 | 1 | Disha1 | 3 |
| 2 | 1 | Dishe1 | 4 |
| 2 | 2 | Dish2 | 1 |
| 2 | 2 | Dísh2 | 2 |
| 2 | 2 | Dishe2 | 4 |
+--------------+--------+----------+-----------+
An anti-join pattern is usually the most efficient, in terms of performance.
Your particular case is a little more tricky, in that you need to "generate" rows that are missing. If every (ResturantID,DishID) should have 4 rows, with Language values of 1,2,3 and 4, we can generate that set of all rows with a CROSS JOIN operation.
The next step is to apply an anti-join... a LEFT OUTER JOIN to the rows that exist in the menu table, so we get all the rows from the CROSS JOIN set, along with matching rows.
The "trick" is to use a predicate in the WHERE clause that filters out rows where we found a match, so we are left rows that didn't have a match.
(It seems a bit strange at first, but once you get your brain wrapped around the anti-join pattern, it becomes familiar.)
So a query of this form should return the specified result set.
SELECT d.RestaurantID
, d.DishID
, lang.id AS missing_language
FROM (SELECT 1 AS id UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) lang
CROSS
JOIN (SELECT e.RestaurantID, e.DishID
FROM menu e
GROUP BY e.RestaurantID, e.DishID
) d
LEFT
JOIN menu m
ON m.RestaurantID = d.RestaurantID
AND m.DishID = d.DishID
AND m.Language = lang.id
WHERE m.RestaurantID IS NULL
ORDER BY 1,2,3
Let's unpack that bit.
First we get a set containing the numbers 1 thru 4.
Next we get a set containing the (RestaurantID, DishID) distinct tuples. (For each distinct Restaurant, a distinct list of DishID, as long as there is at least one row for any Language for that combination.)
We do a CROSS JOIN, matching every row from set one (lang) with every row from set (d), to generate a "complete" set of every (RestaurantID, DishID, Language) we want to have.
The next part is the anti-join... the left outer join to menu to find which of the rows from the "complete" set has a matching row in menu, and filtering out all the rows that had a match.
That may be a little confusing. If we think of that CROSS JOIN operation producing a temporary table that looks like the menu table, but containing all possible rows... we can think of it in terms of pseudocode:
create temporary table all_menu_rows (RestaurantID, MenuID, Language) ;
insert into all_menu_rows ... all possible rows, combinations ;
Then the anti-join pattern is a little easier to see:
SELECT r.RestaurantID
, r.DishID
, r.Language
FROM all_menu_rows r
LEFT
JOIN menu m
ON m.RestaurantID = r.RestaurantID
AND m.DishID = r.DishID
AND m.Language = r.Language
WHERE m.RestaurantID IS NULL
ORDER BY 1,2,3
(But we don't have to incur the extra overhead of creating and populating the temporary table, we can do that right in the query.)
Of course, this isn't the only approach. We could use a NOT EXISTS predicate instead of an anti-join, though this is not usually as efficient. The first part of the query is the same, to generate the "complete" set of rows we expect to have; what differs is how we identify whether or not there is a matching row in the menu table:
SELECT d.RestaurantID
, d.DishID
, lang.id AS missing_language
FROM (SELECT 1 AS id UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4
) lang
CROSS
JOIN (SELECT e.RestaurantID, e.DishID
FROM menu e
GROUP BY e.RestaurantID, e.DishID
) d
WHERE NOT EXISTS ( SELECT 1
FROM menu m
WHERE m.RestaurantID = d.RestaurantID
AND m.DishID = d.DishID
AND m.Language = lang.id
)
ORDER BY 1,2,3
For each row in the "complete" set (generated by the CROSS JOIN operation), we're going to run a correlated subquery that checks whether a matching row is found. The NOT EXISTS predicate returns TRUE if no matching row is found. (This is a little easier to understand, but it usually doesn't perform as well as the anti-join pattern.)
You can use the following statement if each menu item should have a record on each language (8 in real life 4 in example). You can change the number 4 to 8 if you want to see all menu items per restaurant that doesn't have all 8 entries.
SELECT RestaurantID,DishID, COUNT( * )
FROM Menu
GROUP BY RestaurantID,DishID
HAVING COUNT( * ) <4
I have the following (simplified) three tables:
user_reservations:
id | user_id |
1 | 3 |
1 | 3 |
user_kar:
id | user_id | szak_id |
1 | 3 | 1 |
2 | 3 | 2 |
szak:
id | name |
1 | A |
2 | B |
Now I would like to count the reservations of the user by the 'szak' name, but I want to have every user counted only for one szak. In this case, user_id has 2 'szak', and if I write a query something like:
SELECT sz.name, COUNT(*) FROM user_reservations r
LEFT JOIN user_kar k ON k.user_id = r.user_id
LEFT JOIN szak s ON r.szak_id = r.id
It will return two rows:
A | 2 |
B | 2 |
However I want to every reservation counted to only one szak (lets say the highest id only). I tried MAX(k.id) with HAVING, but seems uneffective.
I would like to know if there is a supported method for that in MySQL, or should I first pick all the user ID-s on the backend site first, check their maximum kar.user_id, and then count only with those, removing them from the id list, when the given szak is counted, and then build the data back together on the backend side?
Thanks for the help - I was googling around for like 2 hours, but so far, I found no solution, so maybe you could help me.
Something like this?
SELECT sz.name,
Count(*)
FROM (SELECT r.user_id,
Ifnull(Max(k.szak_id), -1) AS max_szak_id
FROM user_reservations r
LEFT OUTER JOIN user_kar k
ON k.user_id = r.user_id
GROUP BY r.user_id) t
LEFT OUTER JOIN szak sz
ON sz.id = t.max_szak_id
GROUP BY sz.name;