Hacer coincidir una sola columna con varios valores sin tabla de unión automática en MySQL

14

Tenemos una tabla que usamos para almacenar las respuestas a las preguntas. Necesitamos poder encontrar usuarios que tengan ciertas respuestas a preguntas particulares. Entonces, si nuestra tabla consta de los siguientes datos:

user_id     question_id     answer_value  
Sally        1               Pooch  
Sally        2               Peach  
John         1               Pooch  
John         2               Duke

y queremos encontrar usuarios que respondan 'Pooch' para la pregunta 1 y 'Peach' para la pregunta 2, el siguiente SQL (obviamente) no funcionará:

select user_id 
from answers 
where question_id=1 
  and answer_value = 'Pooch'
  and question_id=2
  and answer_value='Peach'

Mi primer pensamiento fue unirme a la tabla para cada respuesta que buscamos:

select a.user_id 
from answers a, answers b 
where a.user_id = b.user_id
  and a.question_id=1
  and a.answer_value = 'Pooch'
  and b.question_id=2
  and b.answer_value='Peach'

Esto funciona, pero dado que permitimos un número arbitrario de filtros de búsqueda, necesitamos encontrar algo mucho más eficiente. Mi siguiente solución fue algo como esto:

select user_id, count(question_id) 
from answers 
where (
       (question_id=2 and answer_value = 'Peach') 
    or (question_id=1 and answer_value = 'Pooch')
      )
group by user_id 
having count(question_id)>1

Sin embargo, queremos que los usuarios puedan tomar el mismo cuestionario dos veces, por lo que potencialmente podrían tener dos respuestas a la pregunta 1 en la tabla de respuestas.

Entonces, ahora estoy perdido. ¿Cuál es la mejor manera de abordar esto? ¡Gracias!

Christopher Armstrong
fuente

Respuestas:

8

He encontrado una forma inteligente de hacer esta consulta sin una autounión.

Ejecuté estos comandos en MySQL 5.5.8 para Windows y obtuve los siguientes resultados:

use test
DROP TABLE IF EXISTS answers;
CREATE TABLE answers (user_id VARCHAR(10),question_id INT,answer_value VARCHAR(20));
INSERT INTO answers VALUES
('Sally',1,'Pouch'),
('Sally',2,'Peach'),
('John',1,'Pooch'),
('John',2,'Duke');
INSERT INTO answers VALUES
('Sally',1,'Pooch'),
('Sally',2,'Peach'),
('John',1,'Pooch'),
('John',2,'Duck');

SELECT user_id,question_id,GROUP_CONCAT(DISTINCT answer_value) given_answers
FROM answers GROUP BY user_id,question_id;

+---------+-------------+---------------+
| user_id | question_id | given_answers |
+---------+-------------+---------------+
| John    |           1 | Pooch         |
| John    |           2 | Duke,Duck     |
| Sally   |           1 | Pouch,Pooch   |
| Sally   |           2 | Peach         |
+---------+-------------+---------------+

Esta pantalla revela que John dio dos respuestas diferentes a la pregunta 2 y Sally dio dos respuestas diferentes a la pregunta 1.

Para ver qué preguntas fueron respondidas de manera diferente por todos los usuarios, simplemente coloque la consulta anterior en una subconsulta y verifique si hay una coma en la lista de respuestas para obtener el recuento de respuestas distintas de la siguiente manera:

SELECT user_id,question_id,given_answers,
(LENGTH(given_answers) - LENGTH(REPLACE(given_answers,',','')))+1 multianswer_count
FROM (SELECT user_id,question_id,GROUP_CONCAT(DISTINCT answer_value) given_answers
FROM answers GROUP BY user_id,question_id) A;

Tengo esto:

+---------+-------------+---------------+-------------------+
| user_id | question_id | given_answers | multianswer_count |
+---------+-------------+---------------+-------------------+
| John    |           1 | Pooch         |                 1 |
| John    |           2 | Duke,Duck     |                 2 |
| Sally   |           1 | Pouch,Pooch   |                 2 |
| Sally   |           2 | Peach         |                 1 |
+---------+-------------+---------------+-------------------+

Ahora simplemente filtre las filas donde multianswer_count = 1 usando otra subconsulta:

SELECT * FROM (SELECT user_id,question_id,given_answers,
(LENGTH(given_answers) - LENGTH(REPLACE(given_answers,',','')))+1 multianswer_count
FROM (SELECT user_id,question_id,GROUP_CONCAT(DISTINCT answer_value) given_answers
FROM answers GROUP BY user_id,question_id) A) AA WHERE multianswer_count > 1;

Esto es lo que conseguí:

+---------+-------------+---------------+-------------------+
| user_id | question_id | given_answers | multianswer_count |
+---------+-------------+---------------+-------------------+
| John    |           2 | Duke,Duck     |                 2 |
| Sally   |           1 | Pouch,Pooch   |                 2 |
+---------+-------------+---------------+-------------------+

Esencialmente, realicé tres escaneos de tabla: 1 en la tabla principal, 2 en las subconsultas pequeñas. NO SE UNE !!!

Darle una oportunidad !!!

RolandoMySQLDBA
fuente
1
Siempre aprecio el nivel de esfuerzo que pones en tus respuestas.
randomx
7

Me gusta el método de unión, yo mismo:

SELECT a.user_id FROM answers a
INNER JOIN answers a1 ON a1.question_id=1 AND a1.answer_value='Pooch'
INNER JOIN answers a2 ON a2.question_id=2 AND a2.answer_value='Peach'
GROUP BY a.user_id

Actualización Después de probar con una tabla más grande (~ 1 millón de filas), este método tomó mucho más tiempo que el ORmétodo simple mencionado en la pregunta original.

Derek Downey
fuente
Gracias por la respuesta. El problema es que esto podría ser una gran mesa, y tener que unirse a ella de 5 a 6 veces puede significar un gran impacto en el rendimiento, ¿correcto?
Christopher Armstrong
buena pregunta Estoy escribiendo un caso de prueba para probarlo, como no sé ... publicará los resultados cuando termine
Derek Downey
1
así que inserté 1 millón de filas con usuarios aleatorios, pares de preguntas / respuestas. Unirse todavía va a 557 segundos y su consulta OR terminó en 1.84 segundos ... ahora se sentará en una esquina.
Derek Downey
¿tienes índices en la tabla de prueba? Si está escaneando la tabla de millones de filas varias veces, será un poco lento, sin duda :-).
Marian
@Marian, sí, agregué un índice sobre el problema (question_id, answer_value) es que la cardinalidad es extremadamente baja, por lo que no ayuda mucho (cada unión se escanearon 100-200k filas)
Derek Downey
5

Nos unimos user_ida la answerstabla en una cadena de combinaciones para obtener datos de otras tablas, pero aislar la tabla de respuestas SQL y escribirla en términos tan simples me ayudó a detectar la solución:

SELECT user_id, COUNT(question_id) 
FROM answers 
WHERE
  (question_id = 2 AND answer_value = 'Peach') 
  OR (question_id = 1 AND answer_value = 'Pooch')
GROUP by user_id 
HAVING COUNT(question_id) > 1

Estábamos usando innecesariamente una segunda subconsulta.

Christopher Armstrong
fuente
me gusta su respuesta es
Kisspa
4

Si tiene un gran conjunto de datos, haría dos índices:

  • question_id, answer_value, user_id; y
  • user_id, question_id, answer_value.

Tendrá que unirse varias veces debido a la forma en que se organizan los datos. Si sabe qué valor para qué pregunta es menos común, puede acelerar un poco la consulta, pero el optimizador debería hacerlo por usted.

Pruebe la consulta como:

SELECCIONE a1.user_id FROM respuestas a1
DONDE a1.question_id = 1 Y a1.answer_value = 'Pooch'
INNER JOIN responde a2 EN a2.question_id = 2 
   Y a2.answer_value = 'Melocotón' Y a1.user_id = a2.user_id

La tabla a1 debe usar el primer índice. Dependiendo de la distribución de datos, el optimizador puede usar cualquier índice. Toda la consulta debe satisfacerse a partir de los índices.

BillThor
fuente
2

Una forma de abordarlo es obtener un subconjunto de user_id y probarlos para la segunda coincidencia:

SELECT user_id 
FROM answers 
WHERE question_id = 1 
AND answer_value = 'Pooch'
AND user_id IN (SELECT user_id FROM answers WHERE question_id=2 AND answer_value = 'Peach');

Usando la estructura de Rolando:

CREATE TABLE answers (user_id VARCHAR(10),question_id INT,answer_value VARCHAR(20));
INSERT INTO answers VALUES
('Sally',1,'Pouch'),
('Sally',2,'Peach'),
('John',1,'Pooch'),
('John',2,'Duke');
INSERT INTO answers VALUES
('Sally',1,'Pooch'),
('Sally',2,'Peach'),
('John',1,'Pooch'),
('John',2,'Duck');

Rendimientos:

mysql> SELECT user_id FROM answers WHERE question_id = 1 AND answer_value = 'Pooch' AND user_id IN (SELECT user_id FROM answers WHERE question_id=2 AND answer_value = 'Peach');
+---------+
| user_id |
+---------+
| Sally   |
+---------+
1 row in set (0.00 sec)
randomx
fuente