Pergunta

Dadas as seguintes tabelas:

Recipes
| id | name
| 1  | 'chocolate cream pie'
| 2  | 'banana cream pie'
| 3  | 'chocolate banana surprise'

Ingredients
| id | name
| 1  | 'banana'
| 2  | 'cream'
| 3  | 'chocolate'

RecipeIngredients
| recipe_id | ingredient_id
|     1     |      2
|     1     |      3
|     2     |      1
|     2     |      2
|     3     |      1
|     3     |      3

Como faço para construir uma consulta SQL para encontrar receitas onde ingredientes.name = 'chocolate' e ingredientes.name = 'cream'?

Foi útil?

Solução

Isso é chamado de divisão relacional. Uma variedade de técnicas é discutida aqui.

Uma alternativa ainda não dada é o duplo não existe

SELECT r.id, r.name
FROM Recipes r
WHERE NOT EXISTS (SELECT * FROM Ingredients i
                  WHERE name IN ('chocolate', 'cream')
                  AND NOT EXISTS
                      (SELECT * FROM RecipeIngredients ri
                       WHERE ri.recipe_id = r.id
                       AND ri.ingredient_id = i.id))

Outras dicas

Usar:

  SELECT r.name
    FROM RECIPES r
    JOIN RECIPEINGREDIENTS ri ON ri.recipe_id = r.id
    JOIN INGREDIENTS i ON i.id = ri.ingredient_id
                      AND i.name IN ('chocolate', 'cream')
GROUP BY r.name
  HAVING COUNT(DISTINCT i.name) = 2

O ponto principal aqui é que a contagem deve ser igual ao número de nomes de ingredientes. Se não é uma contagem distinta, há um risco de falsos positivos devido a duplicatas.

Se você está procurando várias associações, a maneira mais simples de escrever a consulta é usar múltiplas EXISTS condições em vez de uma única reta JOIN.

SELECT r.id, r.name
FROM Recipes r
WHERE EXISTS
(
    SELECT 1
    FROM RecipeIngredients ri
    INNER JOIN Ingredients i
        ON i.id = ri.ingredient_id
    WHERE ri.recipe_id = r.id
    AND i.name = 'chocolate'
)
AND EXISTS
(
    SELECT 1
    FROM RecipeIngredients ri
    INNER JOIN Ingredients i
        ON i.id = ri.ingredient_id
    WHERE ri.recipe_id = r.id
    AND i.name = 'cream'
)

Se você sabe com certeza que as associações são únicas (ou seja, uma única receita pode ter apenas uma única instância de cada ingrediente), você pode trapacear um pouco usando uma subconsulta de agrupamento com um COUNT função e possivelmente acelerar (o desempenho dependerá do DBMS):

SELECT r.id, r.Name
FROM Recipes r
INNER JOIN RecipeIngredients ri
    ON ri.recipe_id = r.id
INNER JOIN Ingredients i
    ON i.id = ri.ingredient_id
WHERE i.name IN ('chocolate', 'cream')
GROUP BY r.id, r.Name
HAVING COUNT(*) = 2

Ou, se uma receita pode ter várias instâncias do mesmo ingrediente (não UNIQUE restrição no RecipeIngredients tabela de associação), você pode substituir a última linha por:

HAVING COUNT(DISTINCT i.name) = 2
select r.*
from Recipes r
inner join (
    select ri.recipe_id
    from RecipeIngredients ri 
    inner join Ingredients i on ri.ingredient_id = i.id
    where i.name in ('chocolate', 'cream')
    group by ri.recipe_id
    having count(distinct ri.ingredient_id) = 2
) rm on r.id = rm.recipe_id
SELECT DISTINCT r.id, r.name
FROM Recipes r
INNER JOIN RecipeIngredients ri ON
    ri.recipe_id = r.id
INNER JOIN Ingredients i ON
    i.id = ri.ingredient_id
WHERE
    i.name IN ( 'cream', 'chocolate' )

Editado seguinte comentário, obrigado! Este é o caminho certo então:

SELECT DISTINCT r.id, r.name
FROM Recipes r
INNER JOIN RecipeIngredients ri ON
    ri.recipe_id = r.id
INNER JOIN Ingredients i ON
    i.id = ri.ingredient_id AND
    i.name = 'cream'
INNER JOIN Ingredients i2 ON
    i2.id = ri.ingredient_id AND
    i2.name = 'chocolate'

uma maneira diferente:

Versão 2 (como procedimento armazenado) revisada

select   r.name
from   recipes r
where   r.id  = (select  t1.recipe_id
        from  RecipeIngredients t1 inner join
     RecipeIngredients     t2 on t1.recipe_id = t2.recipe_id
     and     t1.ingredient_id = @recipeId1
     and     t2.ingredient_id = @recipeId2)

Editar 2:[antes que as pessoas comecem a gritar] :)

Isto pode ser colocado no topo da versão 2, o que permitirá consultar por nome em vez de passar o id.

select @recipeId1 = recipe_id from Ingredients where name = @Ingredient1
select @recipeId2 = recipe_id from Ingredients where name = @Ingredient2

Testei a versão 2 e funciona.A maioria dos usuários onde o link na tabela de ingredientes, neste caso, era totalmente desnecessário!

Editar 3:(Resultado dos testes);

Quando este procedimento armazenado é executado, estes são os resultados.

Os resultados estão no formato (First Recipe_id ;Segunda Receita_id, Resultado)

1,1, Failed
1,2, 'banana cream pie'
1,3, 'chocolate banana surprise'
2,1, 'banana cream pie'
2,2, Failed
2,3, 'chocolate cream pie'
3,1, 'chocolate banana surprise'
3,2, 'chocolate cream pie'
3,3, Failed

É claro que esta consulta não trata de casos em que ambas as restrições são iguais, mas funciona para todos os outros casos.

Editar 4: (lidando com o mesmo caso de restrição):

substituindo esta linha:

r.id = (select t1...

para

r.id in (select t1...

trabalha com os casos com falha para fornecer:

1,1, 'banana cream pie' and 'chocolate banana surprise'
2,2, 'chocolate cream pie' and 'banana cream pie'
3,3, 'chocolate cream pie' and 'chocolate banana surprise'
Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top