2012-02-07 33 views
6

Tengo una simple pregunta sobre la forma más eficiente de realizar una unión en particular.Únete entre la tabla de mapeo (unión) con cardinalidad específica

Tome estas tres tablas, nombres reales han sido cambiados para proteger a los inocentes:

Tabla: animal

 
animal_id name ... 
====================== 
1   bunny 
2   bear 
3   cat 
4   mouse 

Tabla: etiquetas

 
tag_id  tag 
================== 
1   fluffy 
2   brown 
3   cute 
4   small 

tabla de asignación: animal_tag

 
animal_id tag_id 
================== 
1   1 
1   2 
1   3 
2   2 
3   4 
4   2 

Quiero encontrar todos los animales etiquetados como 'mullido', 'marrón' y 'lindo'. Es decir que el animal debe etiquetarse con los tres. En realidad, la cantidad de etiquetas requeridas puede variar, pero debería ser irrelevante para esta discusión. Esta es la pregunta que se me ocurrió:

SELECT * FROM animal 
JOIN (
     SELECT at.animal_id FROM animal_tag at 
     WHERE at.tag_id IN (
          SELECT tg.tag_id FROM tag tg 
          WHERE tg.tag='fluffy' OR tg.tag='brown' OR tg.tag='cute' 
         ) 
     GROUP BY at.animal_id HAVING COUNT(at.tag_id)=3 
    ) AS jt 
ON animal.animal_id=jt.animal_id 

sobre una mesa con animales '' miles y cientos y de 'etiquetas', esta consulta realiza respetable ... 10s de milisegundos. Sin embargo, cuando miro el plan de consulta (Apache Derby es el DB), el costo estimado del optimizador es bastante alto (9945.12) y el plan bastante extenso. Para una consulta así de "simple", generalmente intento obtener planes de consulta con un costo estimado de dígitos simples o dobles.

Así que mi pregunta es, ¿hay una mejor manera de realizar esta consulta? Parece una consulta simple, pero he estado perplejo por encontrar algo mejor.

+0

creo que deberías usar 'AND' en lugar de' OR' en 'WHERE tg.tag = 'fluffy' O tg.tag = 'marrón' O tg.tag = 'cute'' –

+0

@johntotetwoo No _single_ fila en 'tag' coincide con más de un valor único, por lo que usar AND no produciría filas coincidentes. –

+0

@BrankoDimitrijevic ¡tienes razón! mi error. que estoy pensando. –

Respuesta

1

Antes que nada, muchas gracias a todos los que se metieron en esto. En última instancia, la respuesta es, como lo mencionan varios comentaristas, la división relacional.

Si bien tomé un curso en el modelo de datos relacionales de Codd hace muchas lunas, el curso como muchos, realmente no cubría la división relacional. Inconscientemente, mi consulta original es en realidad una aplicación de la División relacional.

En una diapositiva 26-27 en this presentation sobre división relacional, mi consulta aplica la técnica de comparar cardinalidades establecidas. Probé algunos de los otros métodos mencionados para aplicar la división relacional pero, al menos en mi caso, el método de conteo proporciona el tiempo de ejecución más rápido. Animo a todos los interesados ​​en este problema a leer la pila de diapositivas antes mencionada, así como el artículo al que se hace referencia en esta página por Mikael Eriksson. De nuevo, gracias a todos.

1

Puede crear una tabla temporal usando DECLARE GLOBAL TEMPORARY TABLE Y luego haga una UNIÓN INTERNA para eliminar el "DONDE EN". Trabajar con uniones que se configuran por lo general es mucho más eficiente que las sentencias Where que se deben evaluar para cada fila.

+2

en la práctica, la consulta dentro de WHERE IN está optimizada por la base de datos de modo que solo se ejecuta una vez, porque no tiene dependencias en la consulta externa. Además, dado que solo devuelve (en este caso, 3 filas, o un pequeño número en la práctica), la sobrecarga de crear y seleccionar en una tabla temporal es mayor que el costo de consulta original. – brettw

1

probar esto:

SELECT DISTINCT f.Animal_ID, g.Name 
FROM Animal f INNER JOIN 
    (SELECT a.Animal_ID, a.Name, COUNT(*) as iCount 
    FROM Animal a INNER JOIN Animal_Tag b 
        ON a.Animal_ID = b.animal_ID 
        INNER JOIN Tags c 
        On b.tag_ID = c.tag_ID 
    WHERE c.tag IN ('fluffy', 'brown', 'cute') -- list all tags here 
    GROUP BY a.Animal_ID) g 
WHERE g.iCount = 3 -- No. of tags 

ACTUALIZACIÓN

SELECT DISTINCT a.Animal_ID, a.Name, COUNT(*) as iCount 
    FROM Animal a INNER JOIN Animal_Tag b 
        ON a.Animal_ID = b.animal_ID 
        INNER JOIN Tags c 
        On b.tag_ID = c.tag_ID 
    WHERE c.tag IN ('fluffy', 'brown', 'cute') -- list all tags here 
    GROUP BY Animal_ID 
    HAVING iCount = 3 -- No. of tags 
+0

Gracias, agradezco el esfuerzo. Esta consulta es correcta ya que produce el mismo resultado que mi consulta. Desafortunadamente, al enchufarlo a nuestro código, tiene un costo estimado ligeramente más alto y un tiempo de ejecución ligeramente más largo (nuestra consulta es 0.28s, la suya es 0.32s). Básicamente equivalente en términos de rendimiento (al menos con nuestro conjunto de datos). Gracias de nuevo. – brettw

+0

@brettw He actualizado esa consulta. ¿Disminuye ese costo estimado? –

+0

@johntotewoo No sé por qué, pero a Derby no le gusta esa consulta. Error: la referencia de columna 'A.NAME' no es válida o es parte de una expresión no válida. Para una lista SELECT con un GROUP BY, las columnas y expresiones que se seleccionen solo pueden contener expresiones de agrupación válidas y expresiones agregadas válidas. – brettw

1

dar a esto un giro:

SELECT a.* 
FROM animal a 
INNER JOIN 
    ( 
    SELECT at.animal_id 
    FROM tag t 
    INNER JOIN animal_tag at ON at.tag_id = t.tag_id 
    WHERE tag IN ('fluffy', 'brown', 'cute') 
    GROUP BY at.animal_id 
    HAVING count(*) = 3 
) f ON a.animal_id = f.animal_id 

Aquí es otra opción, sólo por el gusto de hacerlo:

SELECT a.* 
FROM animal a 
INNER JOIN animal_tag at1 on at1.animal_id = a.animal_id 
INNER JOIN tag t1 on t1.tag_id = at1.tag_id 
INNER JOIN animal_tag at2 on at2.animal_id = a.animal_id 
INNER JOIN tag t2 on t2.tag_id = at2.tag_id 
INNER JOIN animal_tag at3 on at3.animal_id = a.animal_id 
INNER JOIN tag t3 on t3.tag_id = at3.tag_id 
WHERE t1.tag = 'fluffy' AND t2.tag = 'brown' AND t3.tag = 'cute' 

Realmente no espero que esta última opción funcione bien ... las otras opciones evitan tener que volver a la tabla de etiquetas varias veces para resolver un nombre de etiqueta de la identificación ... pero nunca se sabe qué optimizador de consultas funcionará hasta que lo intente.

+0

Excelente. La primera consulta no es una opción con Apache Derby ya que no admite la instrucción WITH. Pero la segunda opción es interesante. Viene con un costo de optimizador más bajo (5966.82) que mi original, pero en la práctica el tiempo de ejecución es aproximadamente un 10% más largo (promediado en 10 ejecuciones). – brettw

+0

@brettw - reescribió la primera consulta para omitir la cte. –

+0

Curiosamente, su primera consulta revisada compila exactamente el mismo plan de acceso que mi consulta, incluido un costo estimado exacto (9945.12). – brettw

0

Me preguntaba qué tan malo sería usar una división relacional allí. ¿Puedes correr? Sé que esto tomará más, pero estoy intrigado por cuánto :) Si puede proporcionar tanto el costo estimado como el tiempo, sería genial.

select a2.animal_id, a2.animal_name from animal2 a2 
where not exists (
    select * from animal1 a1, tags t1 
    where not exists (
     select * from animal_tag at1 
     where at1.animal_id = a1.animal_id and at1.animal_tag = t1.tag_id 
    ) and a2.animal_id = a1.animal_id and t1.tag in ('fluffy', 'brown', 'cute') 
) 

Ahora, buscando una consulta rápida, no puedo pensar en ninguna más rápida que la de John o la tuya. En realidad Juan podría ser un poco más lenta que la tuya porque está realizando operaciones unnencesary (quitar distinta y eliminar la cuenta (*) a partir de selección):

SELECT a.Animal_ID, a.Name FROM Animal a 
INNER JOIN Animal_Tag b ON a.Animal_ID = b.animal_ID 
INNER JOIN Tags c On b.tag_ID = c.tag_ID 
WHERE c.tag IN ('fluffy', 'brown', 'cute') -- list all tags here 
GROUP BY Animal_ID, a.Name 
HAVING count(*) = 3 -- No. of tags 

Esto debería ser tan rápido como el suyo.

PD: ¿Hay alguna forma de eliminar ese maldito 3 sin duplicar la cláusula where? Mi cerebro está hirviendo :)

+0

CTE's te permitiría eliminar la redundancia porque podría hacer referencia al CTE dos veces en la consulta principal (la segunda vez sería una consulta de conteo (*) para obtener el número). Pero Derby no los apoya. –

Cuestiones relacionadas