Combinando coluna única com vários valores sem tabela de auto-junção no MySQL

14

Temos uma tabela que usamos para armazenar respostas a perguntas. Precisamos ser capazes de encontrar usuários que tenham certas respostas para perguntas específicas. Portanto, se nossa tabela consistir nos seguintes dados:

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

e queremos encontrar usuários que respondam 'Pooch' para a pergunta 1 e 'Peach' para a pergunta 2, o seguinte SQL (obviamente) não funcionará:

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

Meu primeiro pensamento foi participar da tabela automaticamente para cada resposta que procuramos:

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'

Isso funciona, mas como permitimos um número arbitrário de filtros de pesquisa, precisamos encontrar algo muito mais eficiente. Minha próxima solução foi algo assim:

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

No entanto, queremos que os usuários possam responder ao mesmo questionário duas vezes, para que possam ter duas respostas para a pergunta 1 na tabela de respostas.

Então, agora estou perdida. Qual é a melhor maneira de abordar isso? Obrigado!

Christopher Armstrong
fonte

Respostas:

8

Eu encontrei uma maneira inteligente de fazer essa consulta sem uma associação automática.

Eu executei esses comandos no MySQL 5.5.8 para Windows e obtive os seguintes 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         |
+---------+-------------+---------------+

Essa exibição revela que John deu duas respostas diferentes para a pergunta 2 e Sally deu duas respostas diferentes para a pergunta 1.

Para capturar quais perguntas foram respondidas de forma diferente por todos os usuários, basta colocar a consulta acima em uma subconsulta e verificar se há vírgula na lista de respostas fornecidas para obter a contagem de respostas distintas da seguinte maneira:

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;

Eu tenho esse:

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

Agora basta filtrar as linhas em que multianswer_count = 1 usando outra 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;

Isto é o que eu tenho:

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

Essencialmente, realizei três varreduras de tabela: 1 na tabela principal, 2 nas subconsultas pequenas. NO junta-se !!!

De uma chance !!!

RolandoMySQLDBA
fonte
1
Eu sempre aprecio o nível de esforço que você coloca em suas respostas.
randomx 29/09/11
7

Eu gosto do método join, eu mesmo:

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

Atualizar Após testar com uma tabela maior (~ 1 milhão de linhas), esse método demorou significativamente mais que o ORmétodo simples mencionado na pergunta original.

Derek Downey
fonte
Obrigado pela resposta. O problema é que essa pode ser uma tabela grande e ter que se juntar a ela de cinco a seis vezes pode significar sofrer um grande impacto no desempenho, correto?
Christopher Armstrong
bom quesiton. Eu estou escrevendo um testcase para testá-lo, como eu não sei ... vou postar resultados quando ele é feito
Derek Downey
1
então eu inseri 1 milhão de linhas com pares aleatórios de usuário / pergunta. A junção ainda está em 557segundos e sua consulta OR terminou em 1,84 segundos ... ficando em um canto agora.
Derek Downey
você tem índices na tabela de teste? Se você estiver digitalizando a tabela de milhões de linhas algumas vezes, será um pouco lento, sem dúvida :-).
Marian
@Marian sim, eu adicionei um índice em (question_id, answer_value) problema é a cardinalidade é extremamente baixo, por isso não ajuda muito (cada juntar foi 100-200k linhas digitalizada)
Derek Downey
5

Estávamos juntando o user_idda answerstabela em uma cadeia de junções para obter dados de outras tabelas, mas isolar a tabela de respostas SQL e escrevê-las em termos tão simples me ajudou a identificar a solução:

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ávamos desnecessariamente usando uma segunda subconsulta.

Christopher Armstrong
fonte
eu gosto de você responder
Kisspa
4

Se você tiver um grande conjunto de dados, eu faria dois índices:

  • question_id, answer_value, user_id; e
  • user_id, question_id, answer_value.

Você precisará ingressar várias vezes devido à maneira como os dados são organizados. Se você souber qual valor para qual pergunta é menos comum, poderá acelerar um pouco a consulta, mas o otimizador deve fazer isso por você.

Tente a consulta como:

SELECIONE a1.user_id FROM respostas a1
WHERE a1.question_id = 1 AND a1.answer_value = 'Pooch'
INNER JOIN responde a2 ON a2.question_id = 2 
   AND a2.answer_value = 'Peach' AND a1.user_id = a2.user_id

A tabela a1 deve usar o primeiro índice. Dependendo da distribuição dos dados, o otimizador pode usar qualquer um dos índices. A consulta inteira deve ser satisfeita nos índices.

BillThor
fonte
2

Uma maneira de abordar isso é obter um subconjunto de user_id e testá-los para a segunda correspondência:

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 a estrutura 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');

Rendimentos:

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
fonte