Question

C’est une requête très basique que je n'arrive pas à comprendre ....

Disons que j'ai un tableau à deux colonnes comme celui-ci:

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

Je souhaite obtenir tous les identificateurs d'utilisateur distincts dotés de identificateurs de rôle 1, 2 AND 3. À l'aide de l'exemple ci-dessus, le seul résultat que je souhaite obtenir est identificateur d'utilisateur 1. Je fais ça?

Était-ce utile?

La solution

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

Pour tous ceux qui liront ceci: ma réponse est simple et directe, et a le statut "accepté", mais s'il vous plaît, allez lire le réponse donnée par @cletus. Ses performances sont bien meilleures.

Simplement penser à voix haute, une autre façon d’écrire la jointure automatique décrite par @cletus est la suivante:

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

Cela pourrait être plus facile à lire pour vous, et MySQL supporte les comparaisons de n-uplets comme ça. MySQL sait également utiliser intelligemment les index de recouvrement pour cette requête. Il suffit de l’exécuter via EXPLAIN et de consulter la section "Utilisation de l’index". dans les notes des trois tables, ce qui signifie qu’il lit l’index et n’a même pas à toucher les lignes de données.

J'ai exécuté cette requête sur plus de 2,1 millions de lignes (vidage de données de juillet Stack Overflow pour PostTags) à l'aide de MySQL 5.1.48 sur mon Macbook. Le résultat a été renvoyé en 1,08 seconde. Sur un serveur correct avec assez de mémoire allouée à innodb_buffer_pool_size , le processus devrait être encore plus rapide.

Autres conseils

Ok, j'ai eu un vote négatif, j'ai donc décidé de le tester:

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

CREATE INDEX ON userrole (roleid);

Exécutez ceci:

<?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";
    }
}
?>

Sortie:

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

Cela ajoute 500 000 combinaisons de rôles d'utilisateur aléatoires et environ 25 000 correspondent aux critères choisis.

Première requête:

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

Temps d'interrogation: 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

Temps d'interrogation: 0.016s

C'est vrai. La version de jointure que j'ai proposée est vingt fois plus rapide que la version agrégée.

Désolé, je fais cela pour gagner ma vie et travailler dans le monde réel et dans le monde réel, nous testons SQL et les résultats parlent d'eux-mêmes.

La raison à cela devrait être assez claire. La requête agrégée va évoluer en coût avec la taille de la table. Chaque ligne est traitée, agrégée et filtrée (ou non) à l’aide de la clause HAVING . La version jointe (à l'aide d'un index) sélectionne un sous-ensemble d'utilisateurs en fonction d'un rôle donné, puis vérifie ce sous-ensemble par rapport au deuxième rôle et enfin ce sous-ensemble par rapport au troisième rôle. Chaque sélection (dans algèbre relationnelle ) fonctionne sur un sous-ensemble de plus en plus petit. À partir de là, vous pouvez conclure:

Les performances de la version jointe s'améliorent encore avec une incidence moindre de correspondances.

S'il n'y avait que 500 utilisateurs (sur l'échantillon de 500 Ko ci-dessus) dotés des trois rôles spécifiés, la version jointe deviendrait beaucoup plus rapide. La version globale ne le fera pas (et toute amélioration des performances est le résultat du transport de 500 utilisateurs au lieu de 25 Ko, ce que la version jointe obtient évidemment aussi).

J'étais aussi curieux de voir comment une vraie base de données (c.-à-d. Oracle) pourrait gérer cela. J'ai donc essentiellement répété le même exercice sur Oracle XE (exécuté sur le même ordinateur de bureau Windows XP que MySQL de l'exemple précédent) et les résultats sont presque identiques.

Les jointures semblent être mal vues, mais comme je l’ai déjà démontré, les requêtes globales peuvent être beaucoup plus lentes.

Mise à jour: après quelques des tests approfondis , l’image est plus compliquée et la réponse dépend de vos données, de votre base de données et d’autres facteurs. La morale de l'histoire est test, test, test.

La manière classique de le faire est de le traiter comme un problème de division relationnelle.

En anglais: sélectionnez les utilisateurs pour lesquels aucune des valeurs d'identificateur de rôle souhaitées ne manque.

Je présume que vous avez une table Utilisateurs à laquelle la table UserRole fait référence et que les valeurs d'identificateur de rôle souhaitées se trouvent dans une table:

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

Je suppose également que toutes les colonnes pertinentes ne sont pas NULLables. Par conséquent, il n'y a pas de surprises avec IN ou NOT EXISTS. Voici une requête SQL qui exprime l'anglais ci-dessus:

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

Une autre façon de l'écrire est la suivante

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

Cela peut être efficace ou non, en fonction des index, de la plate-forme, des données, etc. Recherchez sur "" division relationnelle "sur le Web. et vous en trouverez beaucoup.

En supposant que l'ID utilisateur, roleid sont contenus dans un index unique (ce qui signifie qu'il ne peut pas y avoir 2 enregistrements où userid = x et roleid = 1

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

Cela ne résoudra-t-il pas le problème? Quelle est la qualité de cette solution sur des bases de données relationnelles classiques? L’optimiseur de requêtes optimisera-t-il cela automatiquement?

Si vous avez besoin d'une quelconque généralité ici (différentes combinaisons de 3 rôles ou différentes combinaisons de n-rôles) ... Je vous suggérerais d'utiliser un système de masquage de bits pour vos rôles et d'utiliser les opérateurs au niveau du bit pour effectuer vos requêtes. ..

Licencié sous: CC-BY-SA avec attribution
Non affilié à StackOverflow
scroll top