Domanda

Questa è una domanda molto semplice che non riesco a capire ....

Diciamo che ho una tabella a due colonne come questa:

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

Voglio ottenere tutti gli userid distinti che hanno roleids 1, 2 AND 3. Utilizzando l'esempio sopra, l'unico risultato che voglio essere restituito è userid 1. Come Lo faccio?

È stato utile?

Soluzione

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

A chiunque legga questo: la mia risposta è semplice e diretta e ha ottenuto lo stato 'accettato', ma per favore vai a leggere rispondi fornito da @cletus. Ha prestazioni molto migliori.


Basta pensare ad alta voce, un altro modo di scrivere l'auto-unione descritto da @cletus è:

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

Potrebbe essere più facile da leggere per te e MySQL supporta confronti di tuple del genere. MySQL sa anche come utilizzare gli indici di copertura in modo intelligente per questa query. Basta eseguirlo attraverso EXPLAIN e vedere " Uso dell'indice " nelle note per tutte e tre le tabelle, il che significa che sta leggendo l'indice e non deve nemmeno toccare le righe dei dati.

Ho eseguito questa query su 2,1 milioni di righe (il dump di dati Stack Overflow di luglio per PostTags) utilizzando MySQL 5.1.48 sul mio Macbook e ha restituito il risultato in 1,08 secondi. Su un server decente con memoria sufficiente allocata a innodb_buffer_pool_size , dovrebbe essere ancora più veloce.

Altri suggerimenti

Ok, ho ottenuto il downgrade di questo, quindi ho deciso di provarlo:

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

CREATE INDEX ON userrole (roleid);

Esegui:

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

Output:

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

Ciò aggiunge 500.000 combinazioni casuali di ruoli utente e ce ne sono circa 25.000 che corrispondono ai criteri scelti.

Prima query:

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

Tempo di query: 0,312 s

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

Tempo query: 0,016 s

Esatto. La versione del join che ho proposto è venti volte più veloce della versione aggregata.

Scusate ma lo faccio per vivere e lavorare nel mondo reale e nel mondo reale testiamo SQL e i risultati parlano da soli.

Il motivo dovrebbe essere abbastanza chiaro. La query aggregata verrà ridimensionata in base al costo della dimensione della tabella. Ogni riga viene elaborata, aggregata e filtrata (o meno) attraverso la clausola HAVING . La versione di join (utilizzando un indice) selezionerà un sottoinsieme degli utenti in base a un determinato ruolo, quindi controllerà quel sottoinsieme rispetto al secondo ruolo e infine quel sottoinsieme rispetto al terzo ruolo. Ogni selezione (in termini di algebra relazionale ) lavora su un sottoinsieme sempre più piccolo. Da questo puoi concludere:

Le prestazioni della versione di join migliorano ulteriormente con una minore incidenza di partite.

Se c'erano solo 500 utenti (su un campione di 500k sopra) con i tre ruoli dichiarati, la versione del join diventerà significativamente più veloce. La versione aggregata non lo farà (e qualsiasi miglioramento delle prestazioni è il risultato del trasporto di 500 utenti invece di 25k, che ovviamente ottiene anche la versione di join).

Ero anche curioso di vedere come un vero database (cioè Oracle) avrebbe gestito questo. Quindi ho sostanzialmente ripetuto lo stesso esercizio su Oracle XE (in esecuzione sullo stesso computer desktop Windows XP come MySQL dell'esempio precedente) e i risultati sono quasi identici.

I join sembrano essere disapprovati ma, come ho dimostrato, le query aggregate possono essere un ordine di grandezza più lento.

Aggiornamento: dopo alcuni test approfonditi , l'immagine è più complicata e la risposta dipenderà dai tuoi dati, dal tuo database e da altri fattori. La morale della storia è test, test, test.

Il modo classico per farlo è quello di trattarlo come un problema di divisione relazionale.

In inglese: selezionare quegli utenti per i quali non manca nessuno dei valori roleid desiderati.

Presumo che tu abbia una tabella Users a cui fa riferimento la tabella UserRole e suppongo che i valori roleid desiderati siano in una tabella:

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

Presumo anche che tutte le colonne pertinenti non siano NULLable, quindi non ci sono sorprese con IN o NOT EXISTS. Ecco una query SQL che esprime l'inglese sopra:

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

Un altro modo di scriverlo è questo

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

Ciò può o meno risultare efficiente, a seconda degli indici, della piattaforma, dei dati, ecc. Cerca nel web la "divisione relazionale". e troverai molto.

Supponendo userid, roleid sono contenuti in un indice univoco (il che significa che non possono esserci 2 record in cui userid = x e 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

Non risolverà il problema? Quanto è valida una soluzione su DB relazionali tipici? Query Optimizer lo ottimizzerà automaticamente?

Se hai bisogno di qualsiasi tipo di generalità qui (diverse combinazioni di 3 ruoli o diverse combinazioni di n ruoli) ... Ti suggerirei di utilizzare un sistema di mascheramento dei bit per i tuoi ruoli e di utilizzare gli operatori bit a bit per eseguire le tue query. ..

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top