Возможно ли выбрать все блюда, содержащие только ингредиенты A и B, в одном запросе?

#mysql #sql

#mysql #sql

Вопрос:

Я знаю, что вы могли бы добиться этого, получив все блюда, содержащие A и B, а затем проверяя каждое блюдо в результатах вторым запросом, который подсчитывает, сколько ингредиентов содержит каждое блюдо. Тогда, если число равно 2, это должно быть блюдо из 2 ингредиентов, которое содержит A и B, поэтому оно содержит ТОЛЬКО A и B. Вот так:

Получение всех блюд, содержащих A и B.

 SELECT dishes.id, dishes.name 
FROM dishes 
JOIN dishes_ingredients ON dishes.id = dishes_ingredients.dishes_id
JOIN ingredients ON dishes_ingredients.ingredients_id = ingredients.id
WHERE ingredients.name IN ('A', 'B')
GROUP BY dishes.id
HAVING COUNT(DISTINCT ingredients.id) = 2;
  

Получение количества ингредиентов в каждом блюде.

 SELECT COUNT(ingredients_id) 
FROM dishes_ingredients 
WHERE dishes_id = (each dish in previous query's results - could be also done in PHP)
  

Проблема, которую я вижу, заключается в том, что вы не можете использовать один и тот же запрос HAVING COUNT(DISTINCT ingredients.id) 2 раза, один на невыбранном уровне, чтобы получить общее количество ингредиентов блюда, а другой на выбранном уровне, чтобы получить количество выбранных строк с учетом WHERE используемого вами. Я думаю, это потому HAVING , что относится к GROUP BY … Но, может быть, я ошибаюсь? Если бы это было возможно, то вы могли бы проверить оба числа и получить только те блюда, которые содержат только A и B.

Ответ №1:

Удалите WHERE предложение и задайте условия только в HAVING предложении:

 SELECT d.id, d.name 
FROM dishes d 
JOIN dishes_ingredients di ON d.id = di.dishes_id
JOIN ingredients i ON di.ingredients_id = i.id
GROUP BY d.id, d.name 
HAVING COUNT(DISTINCT CASE WHEN i.name IN ('A', 'B') THEN i.name END) = 2
   AND SUM(i.name NOT IN ('A', 'B')) = 0
  

Если каждый ингредиент появляется только один раз для каждого блюда, то HAVING предложение можно упростить до:

 HAVING SUM(i.name IN ('A', 'B')) = 2
   AND SUM(i.name NOT IN ('A', 'B')) = 0
  

Другой способ сделать это с использованием GROUP_CONCAT() :

 SELECT d.id, d.name 
FROM dishes d 
JOIN dishes_ingredients di ON d.id = di.dishes_id
JOIN ingredients i ON di.ingredients_id = i.id
GROUP BY d.id, d.name 
HAVING GROUP_CONCAT(DISTINCT i.name ORDER BY i.name) = 'A,B'
  

Если каждый ингредиент появляется только один раз для каждого блюда, тогда нет необходимости DISTINCT .

Комментарии:

1. … вам также может потребоваться добавить DISTINCT в GROUP_CONCAT .

2. Я не думаю, что это необходимо. , Как я упоминаю в своем ответе, я предполагаю, что каждый ингредиент появляется только один раз для каждого блюда, поэтому нет необходимости в ОТДЕЛЬНЫХ

3. На самом деле ингредиенты появляются более одного раза (я должен был указать это в вопросе).

4. @Pirula более одного раза для каждого блюда? Почему? Это странно.

5. Извините за странность. Как заявил Гордон Линофф в своем ответе, НАЛИЧИЕ COUNT (ОТДЕЛЬНЫЙ СЛУЧАЙ … выполняет свою работу, НО я все еще получаю блюда, содержащие ингредиенты, отличные от A и B.

Ответ №2:

Вы можете легко сделать это с помощью условной агрегации:

 SELECT d.id, d.name, COUNT(*) as num_ingredients
FROM dishes d JOIN
     dishes_ingredients di
     ON d.id = di.dishes_id JOIN
     ingredients i
     ON di.ingredients_id = i.id
GROUP BY d.id
HAVING SUM(i.name IN ('A', 'B')) = 2;
  

Если ингредиенты могут повторяться в рецепте, то используйте:

 HAVING COUNT(DISTINCT CASE WHEN i.name IN ('A', 'B') THEN i.id END) = 2;
  

Как и в случае с вашим предыдущим вопросом, я настоятельно рекомендую вам использовать псевдонимы таблиц в своих запросах, чтобы их было легче писать и читать.

Только для этих ингредиентов, затем:

 HAVING COUNT(DISTINCT CASE WHEN i.name IN ('A', 'B') THEN i.id END) = 2 AND
       COUNT(DISTINCT i.name) = 2
  

Или, чтобы избежать COUNT(DISTINCT) (который может иметь низкую производительность при некоторых обстоятельствах):

 HAVING SUM(i.name = 'A') > 0 AND
       SUM(i.name = 'B') > 0 AND
       SUM(i.name NOT IN ('A', 'B')) = 0
  

Комментарии:

1. Зачем выбирать count(*) как num_ingredients? Вы не используете его после. Когда я включаю это в запрос, он выдает 0 результатов. Когда я его опустил, он работает, но показывает блюда, которые содержат и другие ингредиенты.

2. Ингредиенты на самом деле могут повторяться, поэтому я попробовал ваш ответ с помощью COUNT(ОТДЕЛЬНЫЙ СЛУЧАЙ … и он становится ближе, не выбирая блюда, которые содержат 2 раза A и без B. Проблема в том, что он по-прежнему показывает блюда, содержащие другие ингредиенты.

3. @Pirula . , , я пропустил эту часть вопроса, но обновил ответ.

Ответ №3:

Несмотря на то, что я принял ответ forpas, то, что я закончил, больше похоже на то, что Гордон Линофф добавил в конце. Итак, это то, что я реализовал:

 SELECT d.id, d.name 
FROM dishes d
JOIN dishes_ingredients di ON d.id = di.dishes_id
JOIN ingredients i ON di.ingredients_id = i.id
GROUP BY d.id
HAVING COUNT(DISTINCT CASE WHEN i.name IN ('A', 'B') THEN i.name END) = 2
AND COUNT(DISTINCT i.name) = 2