Faire correspondre une seule colonne à plusieurs valeurs sans table auto-jointe dans MySQL

14

Nous avons un tableau que nous utilisons pour stocker les réponses aux questions. Nous devons être en mesure de trouver des utilisateurs qui ont certaines réponses à des questions particulières. Donc, si notre tableau comprend les données suivantes:

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

et nous voulons trouver des utilisateurs qui répondent «Pooch» pour la question 1 et «Peach» pour la question 2, le SQL suivant ne fonctionnera (évidemment) pas:

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

Ma première pensée a été de me joindre à la table pour chaque réponse que nous recherchons:

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'

Cela fonctionne, mais comme nous autorisons un nombre arbitraire de filtres de recherche, nous devons trouver quelque chose de beaucoup plus efficace. Ma prochaine solution était quelque chose comme ceci:

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

Cependant, nous voulons que les utilisateurs puissent répondre deux fois au même questionnaire, afin qu'ils puissent potentiellement avoir deux réponses à la question 1 dans le tableau des réponses.

Donc, maintenant je suis perdu. Quelle est la meilleure façon d'aborder cela? Merci!

Christopher Armstrong
la source

Réponses:

8

J'ai trouvé un moyen intelligent de faire cette requête sans auto-jointure.

J'ai exécuté ces commandes dans MySQL 5.5.8 pour Windows et j'ai obtenu les résultats suivants:

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

Cet affichage révèle que John a donné deux réponses différentes à la question 2 et Sally a donné deux réponses différentes à la question 1.

Pour identifier les questions auxquelles tous les utilisateurs ont répondu différemment, placez simplement la requête ci-dessus dans une sous-requête et recherchez une virgule dans la liste des réponses données pour obtenir le nombre de réponses distinctes comme suit:

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;

J'ai compris ceci:

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

Maintenant, filtrez simplement les lignes où multianswer_count = 1 en utilisant une autre sous-requête:

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;

Voici ce que j'ai obtenu:

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

Essentiellement, j'ai effectué trois analyses de table: 1 sur la table principale, 2 sur les petites sous-requêtes. NO JOINS !!!

Essaie !!!

RolandoMySQLDBA
la source
1
J'apprécie toujours le niveau d'effort que vous mettez dans vos réponses.
randomx
7

J'aime la méthode join, moi-même:

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

Mise à jour Après avoir testé avec une table plus grande (~ 1 million de lignes), cette méthode a pris beaucoup plus de temps que la ORméthode simple mentionnée dans la question d'origine.

Derek Downey
la source
Merci pour la réponse. Le problème est que cela pourrait potentiellement être une grande table, et avoir à le rejoindre 5-6 fois peut signifier prendre un énorme coup de performance, n'est-ce pas?
Christopher Armstrong
bonne quesiton. j'écris un testcase pour le tester, comme je ne sais pas ... affichera les résultats quand c'est fait
Derek Downey
1
j'ai donc inséré 1 million de lignes avec des paires aléatoires d'utilisateurs, de questions / réponses. La jointure se poursuit toujours à 557 secondes et votre requête OU s'est terminée en 1,84 secondes ... va rester dans un coin maintenant.
Derek Downey
avez-vous des index sur la table de test? Si vous scannez plusieurs fois la table des millions de lignes, ce sera un peu lent, sans aucun doute :-).
Marian
@Marian ouais, j'ai ajouté un index sur (question_id, answer_value), le problème est que la cardinalité est extrêmement faible, donc cela n'aide pas beaucoup (chaque jointure était de 100 à 200k lignes scannées)
Derek Downey
5

Nous rejoignions le user_idde la answerstable dans une chaîne de jointures pour obtenir des données d'autres tables, mais isoler le SQL de la table de réponses et l'écrire en termes si simples m'a aidé à trouver la solution:

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

Nous utilisions inutilement une deuxième sous-requête.

Christopher Armstrong
la source
j'aime que vous
répondiez
4

Si vous avez un grand ensemble de données, je ferais deux index:

  • question_id, answer_value, user_id; et
  • user_id, question_id, answer_value.

Vous devrez vous joindre plusieurs fois en raison de la façon dont les données sont organisées. Si vous savez quelle valeur pour quelle question est la moins courante, vous pourrez peut-être accélérer un peu la requête, mais l'optimiseur devrait le faire pour vous.

Essayez la requête comme:

SELECT a1.user_id FROM répond a1
OERE a1.question_id = 1 ET a1.answer_value = 'Pooch'
INNER JOIN répond a2 ON a2.question_id = 2 
   AND a2.answer_value = 'Peach' AND a1.user_id = a2.user_id

Le tableau a1 doit utiliser le premier index. Selon la distribution des données, l'optimiseur peut utiliser l'un ou l'autre des index. La requête entière doit être satisfaite à partir des index.

BillThor
la source
2

Une façon de l'aborder est d'obtenir un sous-ensemble de user_id et de les tester pour la deuxième correspondance:

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

En utilisant la structure 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');

Rendements:

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
la source