2009-01-25 16 views
29

Esta es una consulta muy básica que no puedo averiguar ....¿Seleccionar valores que cumplen diferentes condiciones en filas diferentes?

Digamos que tengo una tabla de dos columnas de esta manera:

userid | roleid 
--------|-------- 
    1 | 1 
    1 | 2 
    1 | 3 
    2 | 1 

Quiero obtener todos los ID de usuario distintos que tienen roleids 1, 2 Y 3. Utilizando el ejemplo anterior, el único resultado que deseo devolver es userid 1. ¿Cómo hago esto?

+1

Se han agregado numerosos detalles a mi respuesta. – cletus

+3

Cualquier pregunta que provoque una respuesta como la que @cletus dio merece +1 – cori

Respuesta

22
SELECT userid 
FROM UserRole 
WHERE roleid IN (1, 2, 3) 
GROUP BY userid 
HAVING COUNT(DISTINCT roleid) = 3; 

Para cualquiera que lea esto: mi respuesta es sencilla y directa, y consiguió el estatus 'aceptado', pero por favor ir a leer el answer dada por @cletus. Tiene un rendimiento mucho mejor.


Justing pensando en voz alta, otra manera de escribir la autocombinación descrito por @cletus es:

SELECT t1.userid 
FROM userrole t1 
JOIN userrole t2 ON t1.userid = t2.userid 
JOIN userrole t3 ON t2.userid = t3.userid 
WHERE (t1.roleid, t2.roleid, t3.roleid) = (1, 2, 3); 

esto podría ser más fácil de leer para usted, y MySQL soporta las comparaciones de tuplas como esa . MySQL también sabe cómo utilizar los índices de cobertura de forma inteligente para esta consulta. Simplemente ejecútelo en EXPLAIN y vea "Usar índice" en las notas para las tres tablas, lo que significa que está leyendo el índice y que ni siquiera tiene que tocar las filas de datos.

Ejecuté esta consulta en más de 2,1 millones de filas (el volcado de datos de Stack Overflow July para PostTags) utilizando MySQL 5.1.48 en mi Macbook, y devolvió el resultado en 1.08 segundos. En un servidor decente con suficiente memoria asignada al innodb_buffer_pool_size, debería ser aún más rápido.

+0

¡sí! ¡eso es lo que necesitaba! – John

109

Ok, me downvoted en este, así que decidí probarlo:

CREATE TABLE userrole (
    userid INT, 
    roleid INT, 
    PRIMARY KEY (userid, roleid) 
); 

CREATE INDEX ON userrole (roleid); 

Ejecutar este:

<?php 
ini_set('max_execution_time', 120); // takes over a minute to insert 500k+ records 

$start = microtime(true); 

echo "<pre>\n"; 
mysql_connect('localhost', 'scratch', 'scratch'); 
if (mysql_error()) { 
    echo "Connect error: " . mysql_error() . "\n"; 
} 
mysql_select_db('scratch'); 
if (mysql_error()) { 
    echo "Selct DB error: " . mysql_error() . "\n"; 
} 

$users = 200000; 
$count = 0; 
for ($i=1; $i<=$users; $i++) { 
    $roles = rand(1, 4); 
    $available = range(1, 5); 
    for ($j=0; $j<$roles; $j++) { 
     $extract = array_splice($available, rand(0, sizeof($available)-1), 1); 
     $id = $extract[0]; 
     query("INSERT INTO userrole (userid, roleid) VALUES ($i, $id)"); 
     $count++; 
    } 
} 

$stop = microtime(true); 
$duration = $stop - $start; 
$insert = $duration/$count; 

echo "$count users added.\n"; 
echo "Program ran for $duration seconds.\n"; 
echo "Insert time $insert seconds.\n"; 
echo "</pre>\n"; 

function query($str) { 
    mysql_query($str); 
    if (mysql_error()) { 
     echo "$str: " . mysql_error() . "\n"; 
    } 
} 
?> 

Salida:

499872 users added. 
Program ran for 56.5513510704 seconds. 
Insert time 0.000113131663847 seconds. 

que añade 500.000 por el usuario al azar combinaciones de roles y hay aproximadamente 25,000 que coinciden con los criterios elegidos.

Primera consulta:

SELECT userid 
FROM userrole 
WHERE roleid IN (1, 2, 3) 
GROUP by userid 
HAVING COUNT(1) = 3 

tiempo de consulta: tiempo 0.312s

SELECT t1.userid 
FROM userrole t1 
JOIN userrole t2 ON t1.userid = t2.userid AND t2.roleid = 2 
JOIN userrole t3 ON t2.userid = t3.userid AND t3.roleid = 3 
AND t1.roleid = 1 

Consulta: 0.016s

Eso es correcto. La versión de unión que propuse es veinte veces más rápida que la versión agregada.

Lo siento pero lo hago para vivir y trabajar en el mundo real y en el mundo real probamos SQL y los resultados hablan por sí mismos.

La razón para esto debería ser bastante clara. La consulta agregada se escalará en costo con el tamaño de la tabla. Cada fila se procesa, agrega y filtra (o no) a través de la cláusula HAVING. La versión de unión (utilizando un índice) seleccionará un subconjunto de usuarios en función de un rol dado, luego verificará ese subconjunto con el segundo rol y, finalmente, ese subconjunto con el tercer rol. Cada selection (en relational algebra términos) funciona en un subconjunto cada vez más pequeño. De esto puede concluir:

El rendimiento de la versión de combinación mejora aún más con una menor incidencia de coincidencias.

Si solo había 500 usuarios (de la muestra de 500k anterior) que tenían los tres roles indicados, la versión de unión se volverá significativamente más rápida. La versión agregada no lo hará (y cualquier mejora en el rendimiento es el resultado de transportar 500 usuarios en lugar de 25k, lo que obviamente también obtiene la versión de unión).

También tenía curiosidad por ver cómo una base de datos real (es decir, Oracle) se ocuparía de esto. Así que básicamente repetí el mismo ejercicio en Oracle XE (ejecutándose en la misma máquina de escritorio de Windows XP que el MySQL del ejemplo anterior) y los resultados son casi idénticos.

Las uniones parecen estar mal vistas pero, como he demostrado, las consultas agregadas pueden ser de un orden de magnitud más lentas.

Actualización: Después de algúnextensive testing, la situación es más complicada y la respuesta dependerá de sus datos, su base de datos y otros factores. La moraleja de la historia es prueba, prueba, prueba.

+5

¿No hay comentarios con voto a favor? Esto realmente funciona – cletus

+0

Este dv no vino de mí ... pero en serio ... ¿pondrías esto en tu sistema? –

+1

No lo he usado tampoco, pero usaré esto si tengo que hacerlo ... ¿Debería rediseñar mi base de datos para no tener que hacer consultas como esta? – John

-5

Si necesita algún tipo de generalidad aquí (diferentes combinaciones de 3 roles o diferentes combinaciones de n-role) ... Le sugiero que use un sistema de enmascaramiento de bits para sus roles y use los operadores bit a bit para realizar sus consultas ...

+5

-1 Idea terrible. Use una base de datos relacional como una base de datos relacional. – cletus

3

Suponiendo ID de usuario, roleid están contenidos en un índice único (lo que significa que no puede ser de 2 registros donde userid = x y roleid = 1

select count(*), userid from t 
where roleid in (1,2,3) 
group by userid 
having count(*) = 3 
2

La manera clásica de hacerlo es tratarla como una división relacional . problema

En Inglés:. Seleccionar los usuarios para los que falte ninguno de los valores deseados roleid

voy a asumir que tiene una tabla de usuarios a los que se refiere la tabla UserRole, y voy a asumir el deseado los valores de roleid están en una tabla:

create table RoleGroup(
    roleid int not null, 
    primary key(roleid) 
) 
insert into RoleGroup values (1); 
insert into RoleGroup values (2); 
insert into RoleGroup values (3); 

También supondré que todas las columnas relevantes no son NULL, por lo que no hay sorpresas con IN o NOT EXISTS. He aquí una consulta SQL que expresa el Inglés arriba:

select userid from Users as U 
where not exists (
    select * from RoleGroup as G 
    where not exists (
    select R.roleid from UserRole as R 
    where R.roleid = G.roleid 
    and R.userid = U.userid 
) 
); 

Otra manera de escribir es este

select userid from Users as U 
where not exists (
    select * from RoleGroup as G 
    where G.roleid not in (
    select R.roleid from UserRole as R 
    where R.userid = U.userid 
) 
); 

Esto puede o no puede llegar a ser eficiente, en función de los índices, plataforma, datos, etc. Busque en la web "división relacional" y encontrará mucho.

+0

¿Puedes explicar un poco más, es decir, qué hace cada sub consulta? –

+0

Es más o menos una traducción directa de lo que escribí en la parte superior, aquí de nuevo con anotaciones entre paréntesis: Seleccione aquellos usuarios (SELECT más externo) para los que ninguno (primero NO EXISTE) de los valores de roleid deseados (SELECT más interno) Falta) ("No falta ninguno" es lo mismo que "no hay ninguno que no esté entre los que están allí" j –

1
select userid from userrole where userid = 1 
intersect 
select userid from userrole where userid = 2 
intersect 
select userid from userrole where userid = 3 

¿Esto no solucionará el problema? ¿Qué tan buena es esta solución en las DB relacionales típicas? ¿El optimizador de consultas optimizará esto automáticamente?

+0

Esta habría sido la respuesta perfecta para cualquier RDBMS que se precie. No obstante, no obstante, mysql: http: // bugs. mysql.com/bug.php?id=31336 – sayap

Cuestiones relacionadas