2010-06-12 58 views
17

Dadas las siguientes tablas:consulta SQL a través de una tabla intermedia

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 

¿Cómo construir una consulta SQL para encontrar recetas en las que ingredients.name = 'chocolate' y ingredients.name = 'crema'?

+2

fyi ... La recetaIngredientes se llama MappingTable ... –

+3

@Garis Suero: Hay * muchos * sinónimos - xref, búsqueda, mapeo, enlace - hay un nombre estándar definido para una tabla que proporciona una relación de muchos a muchos . –

Respuesta

9

Esto se denomina división relacional. Se discuten una variedad de técnicas here.

Una alternativa todavía no dado es el doble NO 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)) 
+0

Martin, gracias por la referencia y una respuesta que funciona! Hay un error menor en la última línea, debe ser "AND ri.ingredient_id = i.id))" –

+0

@Bryan, Cheers. Lo he solucionado en mi respuesta solo por completitud. –

2

Si está buscando asociaciones múltiples, la forma más sencilla de escribir la consulta es utilizar varias condiciones EXISTS en lugar de una sola recta 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' 
) 

Si sabe con certeza que las asociaciones son únicos (es decir, una única receta sólo puede tener una única instancia de cada ingrediente), entonces se puede engañar un poco utilizando una subconsulta agrupación con una velocidad COUNT función y posiblemente hacia arriba (el rendimiento dependerá de la 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 

O, si una receta podría tener varias instancias del mismo ingrediente (sin UNIQUE restricción en la tabla RecipeIngredients asociación), se puede sustituir la última línea con:

HAVING COUNT(DISTINCT i.name) = 2 
+0

@OMG Ponies: Esta advertencia se mencionó en el segundo párrafo. Pero supongo que no estaría de más agregar la alternativa (se ejecutará más despacio). – Aaronaught

+0

Lo siento, no leí de cerca –

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 
11

Uso:

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 

El punto clave aquí es que el conteo debe ser igual al número de nombres de ingredientes. Si no es un conteo diferenciado, existe el riesgo de falsos positivos debido a duplicados.

+0

La primera opción parece lo que quiero. Lo probé sin alegría usando PostgreSQL 8.3.5 y SQLite3. Intenté con un solo ingrediente y para cada uno de los ingredientes del árbol, a su vez, obtuve 2 filas de vuelta, como era de esperar. Pero, cuando intento con 2 ingredientes, obtengo 0 filas. –

+0

Eso es lo que obtienes por no especificar las bases de datos contra las que estás probando. El S en SQL no significa "estandarizado"; más allá de un SELECT básico, casi no hay posibilidad de ser 100% portátil para todos los proveedores. –

+0

¿Con qué base de datos trabajaría? Tal vez pueda comparar la sintaxis y ver cómo aplicarla a PostgreSQL. Su estilo de solución es más fácil de entender que el NO EXISTE –

1
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 siguiendo el comentario, gracias! Esta es la forma correcta, entonces:

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' 
+3

Esto también devolverá recetas donde la crema se usa sin chocolate o el chocolate se usa sin crema. –

1

una manera diferente:

Version 2 (procedimiento almacenado) 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 de que la gente empiece a gritar] :)

Esto se puede colocar en la parte superior de la versión 2, lo que permitirá consultar por nombre en lugar de pasar la identificación.

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

He probado la versión 2, y funciona. ¡La mayoría de los usuarios se vincularon en la tabla Ingrediente, en este caso, no fue totalmente necesario!

Edición 3: (resultados de la prueba);

Cuando se ejecuta este procedimiento almacenado, estos son los resultados.

Los resultados son del formato (Primera receta_id, en segundo lugar receta_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 

Es evidente que esta consulta no maneja caso cuando ambas restricciones son las mismas, pero funciona para todos los demás casos.

Editar 4: (manipulación caso misma restricción):

sustitución de esta línea:

r.id = (select t1... 

a

r.id in (select t1... 

obras con los casos fallidos para dar:

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' 
+0

Parece que asume que solo un recipe_id puede coincidir y no hay nada que lo limite al chocolate y a la crema –

+0

Pruebe la versión 2 (basada en la misma idea que la primera versión). Lo he probado y funciona correctamente. – Darknight

+0

Aún usa 'r.id =' suponiendo solo un resultado. Por supuesto, se necesitan ingredientes si se relacionan por nombre de ingrediente, pero supongo que esto vendrá de una lista y se conocerán los ID. –

Cuestiones relacionadas