Pergunta

Esta é uma consulta muito básica que não consigo descobrir ....

Digamos que eu tenho uma tabela de duas colunas como esta:

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

Eu quero obter todos os usuários distintos que têm roleids 1, 2 e 3. Usando o exemplo acima, o único resultado que eu quero devolvido é userid 1. Como faço isso?

Foi útil?

Solução

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

Para quem lendo isso: minha resposta é simples e direta e obteve o status 'aceito', mas por favor, leia o responda dado por @cletus. Tem desempenho muito melhor.


Justing Pensando em voz alta, outra maneira de escrever a auto-joia descrita por @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);

Isso pode ser mais fácil de ler para você, e o MySQL suporta comparações de tuplas como essa. O MySQL também sabe como utilizar os índices de cobertura de forma inteligente para esta consulta. Apenas execute -o EXPLAIN e consulte "Usando o índice" nas notas para as três tabelas, o que significa que está lendo o índice e nem precisa tocar nas linhas de dados.

Eu executei essa consulta mais de 2,1 milhões de linhas (o despejo de dados de julho da pilha para pós -Tags) usando o MySQL 5.1.48 no meu MacBook e ele retornou o resultado em 1,08 segundos. Em um servidor decente com memória suficiente alocada para innodb_buffer_pool_size, deve ser ainda mais rápido.

Outras dicas

Ok, fui voto sobre isso, então decidi testá -lo:

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

CREATE INDEX ON userrole (roleid);

Rode isto:

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

Resultado:

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

Isso adiciona 500.000 combinações aleatórias em papel de usuário e existem aproximadamente 25.000 que correspondem aos critérios escolhidos.

Primeira consulta:

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

Tempo de consulta: 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

Tempo de consulta: 0,016s

Isso mesmo. A versão de junção que eu propus é Vinte vezes mais rápido que a versão agregada.

Desculpe, mas eu faço isso para viver e trabalhar no mundo real e no mundo real testamos SQL e os resultados falam por si mesmos.

A razão para isso deve ser bem clara. A consulta agregada escalará em custo com o tamanho da tabela. Cada linha é processada, agregada e filtrada (ou não) através do HAVING cláusula. A versão de junção (usando um índice) selecionará um subconjunto dos usuários com base em uma determinada função, verifique esse subconjunto em relação à segunda função e, finalmente, esse subconjunto em relação à terceira função. Cada seleção (dentro Álgebra relacional termos) funciona em um subconjunto cada vez mais pequeno. A partir disso, você pode concluir:

O desempenho da versão de junção fica ainda melhor com uma menor incidência de partidas.

Se houvesse apenas 500 usuários (da amostra de 500k acima) que tinham as três funções declaradas, a versão de junção ficará significativamente mais rápida. A versão agregada não (e qualquer melhoria de desempenho é resultado do transporte de 500 usuários em vez de 25k, que a versão de junção obviamente obtém).

Eu também estava curioso para ver como um banco de dados real (ou seja, Oracle) lidaria com isso. Por isso, basicamente repeti o mesmo exercício no Oracle XE (executando na mesma máquina de desktop do Windows XP que o MySQL do exemplo anterior) e os resultados são quase idênticos.

As junções parecem ter desaprovadas, mas como eu demonstrei, as consultas agregadas podem ser uma ordem de magnitude mais lenta.

Atualizar: Após alguns testes extensos, a imagem é mais complicada e a resposta dependerá de seus dados, seu banco de dados e outros fatores. A moral da história é teste, teste, teste.

A maneira clássica de fazer isso é tratá -lo como um problema de divisão relacional.

Em inglês: selecione esses usuários para quem nenhum dos valores de Roleid desejado está faltando.

Suponho que você tenha uma tabela de usuários à qual a tabela Userrole se refere, e assumirei que os valores de Roleid desejados estão em uma tabela:

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

Também assumirei que todas as colunas relevantes não são anuláveis; portanto, não há surpresas ou não existe. Aqui está uma consulta SQL que expressa os ingleses acima:

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

Outra maneira de escrever é isso

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

Isso pode ou não ser eficiente, dependendo de índices, plataforma, dados etc. Pesquise na Web por "divisão relacional" e você encontrará muito.

Supondo que o UserID, RoleId esteja contido em um índice exclusivo (o que significa que não pode haver 2 registros em que 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

Isso não resolverá o problema? Quão boa é uma solução no DBS relacional típico? O Otimizador de consultas otimizará isso?

Se você precisar de algum tipo de generalidade aqui (diferentes combinações de 3 funções ou combinações diferentes de furo N) ... Eu sugiro que você use um sistema de mascaramento um pouco para suas funções e use os operadores bit-swise para executar suas consultas ...

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top