Desempenho do operador MySQL “IN” em (grande?) Número de valores

94

Tenho experimentado com Redis e MongoDB recentemente e parece que muitas vezes há casos em que você armazenaria uma matriz de id no MongoDB ou Redis. Vou ficar com o Redis para essa pergunta, já que estou perguntando sobre o operador MySQL IN .

Eu queria saber qual é o desempenho de listar um grande número (300-3000) de ids dentro do operador IN, que seria mais ou menos assim:

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 3000)

Imagine algo tão simples como uma tabela de produtos e categorias que você normalmente juntaria para obter os produtos de uma determinada categoria . No exemplo acima, você pode ver que, sob uma determinada categoria no Redis ( category:4:product_ids), eu retorno todos os ids de produto da categoria com id 4 e os coloco na SELECTconsulta acima dentro do INoperador.

Quão performante é isso?

Esta é uma situação do tipo "depende"? Ou existe um concreto "isto é (in) aceitável" ou "rápido" ou "lento" ou devo adicionar um LIMIT 25, ou isso não ajuda?

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 3000)
LIMIT 25

Ou devo cortar a matriz de ids do produto retornada pelo Redis para limitá-la a 25 e apenas adicionar 25 ids à consulta, em vez de 3000 e LIMITcolocá-la em 25 de dentro da consulta?

SELECT id, name, price
FROM products
WHERE id IN (1, 2, 3, 4, ...... 25)

Qualquer sugestão / feedback é muito apreciada!

Michael van Rooijen
fonte
Não tenho certeza do que você está perguntando exatamente? Uma consulta com "id IN (1,2,3, ... 3000))" é mais rápida do que 3000 consultas com "id = valor". Mas uma junção com "categoria = 4" será mais rápida do que ambas as opções anteriores.
Ronnis
Certo, embora um produto possa pertencer a várias categorias, você não pode fazer a "categoria = 4". Usando o Redis, eu armazenaria todos os ids dos produtos que pertencem a uma determinada categoria e depois consultaria sobre isso. Eu acho que a verdadeira questão é: como seria o id IN (1,2,3 ... 3000)desempenho em comparação com a tabela JOIN de products_categories. Ou era isso que você estava dizendo?
Michael van Rooijen
Apenas
tome
É claro que não há razão para que isso não seja tão eficiente quanto qualquer outro método de recuperação de linhas indexadas; depende apenas se os autores do banco de dados o testaram e otimizaram. Em termos de complexidade computacional, vamos fazer, na pior das hipóteses, uma classificação O (n log N) na INcláusula (isso pode até ser linear em uma lista classificada como você mostra, dependendo do algoritmo) e, em seguida, interseção / pesquisas lineares .
jberryman

Respostas:

40

De um modo geral, se a INlista ficar muito grande (para algum valor mal definido de 'muito grande' que normalmente está na região de 100 ou menor), torna-se mais eficiente usar uma junção, criando uma tabela temporária se necessário para conter os números.

Se os números forem um conjunto denso (sem lacunas - o que os dados de amostra sugerem), você pode fazer ainda melhor com WHERE id BETWEEN 300 AND 3000.

No entanto, presumivelmente há lacunas no conjunto, em cujo ponto pode ser melhor ir com a lista de valores válidos depois de tudo (a menos que as lacunas sejam relativamente poucas em número, caso em que você pode usar:

WHERE id BETWEEN 300 AND 3000 AND id NOT BETWEEN 742 AND 836

Ou quaisquer que sejam as lacunas.

Jonathan Leffler
fonte
47
Você pode dar um exemplo de "usar uma junção, criando uma tabela temporária"?
Jake
se o conjunto de dados veio de uma interface (elemento de seleção múltipla) e há lacunas nos dados selecionados e essas lacunas não são uma lacuna sequencial (ausente: 457, 490, 658, ..) então AND id NOT BETWEEN XXX AND XXXnão funcionará e é melhor fique com o equivalente (x = 1 OR x = 2 OR x = 3 ... OR x = 99)como @David Fells escreveu.
deepcell
1
na minha experiência - trabalhando em sites de comércio eletrônico, temos que mostrar resultados de pesquisa de cerca de 50 IDs de produtos não relacionados, tivemos melhores resultados com "1. 50 consultas separadas", versus "2. uma consulta com muitos valores no" IN cláusula"". Não tenho como provar isso no momento, exceto que a consulta # 2 sempre aparecerá como uma consulta lenta em nossos sistemas de monitoramento, enquanto a # 1 nunca aparecerá, independentemente da quantidade de execuções em os milhões ... alguém tem a mesma experiência? (talvez possamos relacionar isso a um melhor armazenamento em cache, ou permitir que outras consultas se entrelacem entre as consultas ...)
Chaim Klar
1
@Chaim, é claro que a consulta separada não é lenta. Cada um só precisa buscar um registro. O criador de perfil não sabe que um conjunto de consultas está relacionado e precisa ser agregado para comparação.
Daniel de
24

Tenho feito alguns testes e, como David Fells diz em sua resposta , está muito bem otimizado. Como referência, criei uma tabela InnoDB com 1.000.000 de registros e fazendo um select com o operador "IN" com 500.000 números aleatórios, leva apenas 2,5 segundos no meu MAC; selecionar apenas os registros pares leva 0,5 segundos.

O único problema que tive é que tive que aumentar o max_allowed_packetparâmetro do my.cnfarquivo. Caso contrário, um erro misterioso “MYSQL desapareceu” é gerado.

Aqui está o código PHP que uso para fazer o teste:

$NROWS =1000000;
$SELECTED = 50;
$NROWSINSERT =15000;

$dsn="mysql:host=localhost;port=8889;dbname=testschema";
$pdo = new PDO($dsn, "root", "root");
$pdo->setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);

$pdo->exec("drop table if exists `uniclau`.`testtable`");
$pdo->exec("CREATE  TABLE `testtable` (
        `id` INT NOT NULL ,
        `text` VARCHAR(45) NULL ,
        PRIMARY KEY (`id`) )");

$before = microtime(true);

$Values='';
$SelValues='(';
$c=0;
for ($i=0; $i<$NROWS; $i++) {
    $r = rand(0,99);
    if ($c>0) $Values .= ",";
    $Values .= "( $i , 'This is value $i and r= $r')";
    if ($r<$SELECTED) {
        if ($SelValues!="(") $SelValues .= ",";
        $SelValues .= $i;
    }
    $c++;

    if (($c==100)||(($i==$NROWS-1)&&($c>0))) {
        $pdo->exec("INSERT INTO `testtable` VALUES $Values");
        $Values = "";
        $c=0;
    }
}
$SelValues .=')';
echo "<br>";


$after = microtime(true);
echo "Insert execution time =" . ($after-$before) . "s<br>";

$before = microtime(true);  
$sql = "SELECT count(*) FROM `testtable` WHERE id IN $SelValues";
$result = $pdo->prepare($sql);  
$after = microtime(true);
echo "Prepare execution time =" . ($after-$before) . "s<br>";

$before = microtime(true);

$result->execute();
$c = $result->fetchColumn();

$after = microtime(true);
echo "Random selection = $c Time execution time =" . ($after-$before) . "s<br>";



$before = microtime(true);

$sql = "SELECT count(*) FROM `testtable` WHERE id %2 = 1";
$result = $pdo->prepare($sql);
$result->execute();
$c = $result->fetchColumn();

$after = microtime(true);
echo "Pairs = $c Exdcution time=" . ($after-$before) . "s<br>";

E os resultados:

Insert execution time =35.2927210331s
Prepare execution time =0.0161771774292s
Random selection = 499102 Time execution time =2.40285992622s
Pairs = 500000 Exdcution time=0.465420007706s
Jbaylina
fonte
Para o bem dos outros, vou adicionar que rodando no VirtualBox (CentOS) no meu MBP do final de 2013 com um i7, a terceira linha (a relevante para a questão) da saída foi: Seleção aleatória = 500744 Tempo de execução de tempo = 53.458173036575s .. 53 segundos podem ser toleráveis, dependendo de seu aplicativo. Para meu uso, não realmente. Além disso, observe que o teste para números pares não é relevante para a questão em questão, pois ele usa o operador módulo ( %) com um operador igual ( =) em vez de IN().
rinogo
É relevante porque é uma forma de comparar uma consulta com o operador IN com uma consulta semelhante sem essa funcionalidade. Pode ser o maior tempo que você consegue é porque é um tempo de download, porque sua máquina está trocando ou trabalhando em outra máquina virtual.
jbaylina
14

Você pode criar uma tabela temporária onde pode colocar qualquer número de IDs e executar uma consulta aninhada. Exemplo:

CREATE [TEMPORARY] TABLE tmp_IDs (`ID` INT NOT NULL,PRIMARY KEY (`ID`));

e selecione:

SELECT id, name, price
FROM products
WHERE id IN (SELECT ID FROM tmp_IDs);
Vladimir Jotov
fonte
6
é melhor juntar-se à sua tabela temporária em vez de usar uma subconsulta
scharette
3
@loopkin, você pode explicar como faria isso com uma junção e uma subconsulta, por favor?
Jeff Solomon
3
@jeffSolomon SELECT products.id, name, price FROM products JOIN tmp_IDs em products.id = tmp_IDs.ID;
scharette
ESTA RESPOSTA! é o que eu procurava, muito, muito rápido para registros longos
Damián Rafael Lattenero
Muito obrigado, cara. Funciona incrivelmente rápido.
mrHalfer,
4

Usar INcom um grande parâmetro definido em uma grande lista de registros será de fato lento.

No caso que resolvi recentemente tinha duas cláusulas where, uma com 2,50 parâmetros e outra com 3,500 parâmetros, consultando uma tabela de 40 milhões de registros.

Minha consulta levou 5 minutos usando o padrão WHERE IN. Em vez disso, usando uma subconsulta para a instrução IN (colocando os parâmetros em sua própria tabela indexada), reduzi a consulta para DOIS segundos.

Trabalhei para MySQL e Oracle em minha experiência.

Yoyodunno
fonte
1
Não entendi seu ponto de "Ao usar uma subconsulta para a instrução IN (colocando os parâmetros em sua própria tabela indexada)". Você quis dizer que em vez de usar "WHERE ID IN (1,2,3)", devemos usar "WHERE ID IN (SELECT id FROM xxx)"?
Istiyak Tailor de
4

INestá bom e bem otimizado. Certifique-se de usá-lo em um campo indexado e você está bem.

É funcionalmente equivalente a:

(x = 1 OR x = 2 OR x = 3 ... OR x = 99)

No que diz respeito ao motor DB.

David Fells
fonte
1
Não realmente. Eu uso o IN clouse para buscar 5k registros do banco de dados. O clouse IN contém uma lista de PKs para que a coluna relacionada seja indexada e garantida como exclusiva. EXPLAIN diz que a varredura completa da tabela é realizada em vez de usar a pesquisa PK no estilo "fifo-queue-alike".
Antoniossss
No MySQL, não acredito que sejam "funcionalmente equivalentes" . INusa otimizações para melhor desempenho.
Joshua Pinter
1
Josh, a resposta foi de 2011 - tenho certeza de que as coisas mudaram desde então, mas naquela época o IN foi totalmente convertido em uma série de declarações OR.
David Fells em
1
Esta resposta não está correta. Do MySQL de alto desempenho : não é assim no MySQL, que classifica os valores na lista IN () e usa uma pesquisa binária rápida para ver se um valor está na lista. Isso é O (log n) no tamanho da lista, enquanto uma série equivalente de cláusulas OR é O (n) no tamanho da lista (ou seja, muito mais lento para listas grandes).
Bert
Bert - sim. Esta resposta é obsoleta. Sinta-se à vontade para sugerir uma edição.
David Fells de
-2

Quando você fornece muitos valores para o INoperador, ele primeiro deve classificá-los para remover duplicatas. Pelo menos eu suspeito disso. Portanto, não seria bom fornecer muitos valores, pois a classificação leva N log N tempo.

Minha experiência provou que dividir o conjunto de valores em subconjuntos menores e combinar os resultados de todas as consultas no aplicativo oferece melhor desempenho. Admito que acumulei experiência em um banco de dados diferente (Pervasive), mas o mesmo pode se aplicar a todos os motores. Minha contagem de valores por conjunto foi 500-1000. Mais ou menos foi significativamente mais lento.

Jarekczek
fonte
1
Eu sei que isso é 7 anos depois, mas o problema com essa resposta é simplesmente que é um comentário baseado em um palpite.
Giacomo1968