Selecionar valores que atendam a diferentes condições em diferentes linhas?
-
20-08-2019 - |
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?
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 ...