Como posso otimizar a função ORDER BY RAND () do MySQL?

90

Eu gostaria de otimizar minhas consultas para analisar mysql-slow.log.

A maioria das minhas consultas lentas contém ORDER BY RAND(). Não consigo encontrar uma solução real para resolver este problema. Existe uma solução possível em MySQLPerformanceBlog, mas não acho que seja suficiente. Em tabelas mal otimizadas (ou atualizadas com frequência, gerenciadas pelo usuário), ele não funciona ou preciso executar duas ou mais consultas antes de selecionar minha PHPlinha aleatória gerada.

Existe alguma solução para este problema?

Um exemplo fictício:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
ORDER BY
        RAND()
LIMIT 1
Fabrik
fonte

Respostas:

67

Experimente isto:

SELECT  *
FROM    (
        SELECT  @cnt := COUNT(*) + 1,
                @lim := 10
        FROM    t_random
        ) vars
STRAIGHT_JOIN
        (
        SELECT  r.*,
                @lim := @lim - 1
        FROM    t_random r
        WHERE   (@cnt := @cnt - 1)
                AND RAND(20090301) < @lim / @cnt
        ) i

Isso é especialmente eficiente ativado MyISAM(uma vez que COUNT(*)é instantâneo), mas mesmo InnoDBassim é 10mais eficiente do que ORDER BY RAND().

A ideia principal aqui é que não ordenamos, mas, em vez disso, mantemos duas variáveis ​​e calculamos o running probabilityde uma linha a ser selecionada na etapa atual.

Veja este artigo no meu blog para mais detalhes:

Atualizar:

Se você precisar selecionar apenas um único registro aleatório, tente o seguinte:

SELECT  aco.*
FROM    (
        SELECT  minid + FLOOR((maxid - minid) * RAND()) AS randid
        FROM    (
                SELECT  MAX(ac_id) AS maxid, MIN(ac_id) AS minid
                FROM    accomodation
                ) q
        ) q2
JOIN    accomodation aco
ON      aco.ac_id =
        COALESCE
        (
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_id > randid
                AND ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        ),
        (
        SELECT  accomodation.ac_id
        FROM    accomodation
        WHERE   ac_status != 'draft'
                AND ac_images != 'b:0;'
                AND NOT EXISTS
                (
                SELECT  NULL
                FROM    accomodation_category
                WHERE   acat_id = ac_category
                        AND acat_slug = 'vendeglatohely'
                )
        ORDER BY
                ac_id
        LIMIT   1
        )
        )

Isso pressupõe que os seus ac_idestão distribuídos mais ou menos uniformemente.

Quassnoi
fonte
Olá, Quassnoi! Em primeiro lugar, obrigado pela sua resposta rápida! Talvez seja minha culpa, mas ainda não está claro sua solução. Vou atualizar meu post original com um exemplo concreto e ficarei feliz se você explicar sua solução neste exemplo.
fabrik
ocorreu um erro de digitação em "JOIN accomodation aco ON aco.id =" onde aco.id realmente é aco.ac_id. por outro lado, a consulta corrigida não funcionou para mim porque gera um erro # 1241 - Operando deve conter 1 coluna (s) no quinto SELECT (o quarto sub-seleção). Tentei encontrar o problema com parênteses (se não me engano), mas ainda não consigo encontrar o problema.
fabrik de
@fabrik: tente agora. Seria muito útil se você postasse os scripts de tabela para que eu pudesse verificá-los antes de postar.
Quassnoi
Obrigado, funciona! :) Você pode editar a parte JOIN ... ON aco.id para JOIN ... ON aco.ac_id para que eu possa aceitar sua solução. Obrigado novamente! Uma pergunta: eu me pergunto se isso é um pior aleatório como ORDER BY RAND ()? Só porque esta consulta está repetindo algum (s) resultado (s) muitas vezes.
fabrik de
1
@Adam: não, isso é intencional, para que você possa reproduzir os resultados.
Quassnoi
12

Depende de quão aleatório você precisa ser. A solução que você vinculou funciona muito bem IMO. A menos que você tenha grandes lacunas no campo ID, ainda é bastante aleatório.

No entanto, você deve ser capaz de fazer isso em uma consulta usando isto (para selecionar um único valor):

SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*MAX(id)) LIMIT 1

Outras soluções:

  • Adicione um campo flutuante permanente chamado randomà mesa e preencha-o com números aleatórios. Você pode então gerar um número aleatório em PHP e fazer"SELECT ... WHERE rnd > $random"
  • Pegue a lista completa de IDs e armazene-os em um arquivo de texto. Leia o arquivo e escolha um ID aleatório dele.
  • Armazene os resultados da consulta em cache como HTML e guarde-o por algumas horas.
DisgruntledGoat
fonte
8
Sou só eu ou esta consulta não funciona? Eu tentei com várias variações e todas lançam "Uso inválido da função de grupo" ..
Sophivorus
Você pode fazer isso com uma subconsulta, SELECT [fields] FROM [table] WHERE id >= FLOOR(RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1mas isso não parece funcionar corretamente, pois nunca retorna o último registro
Marcos
11
SELECT [fields] FROM [table] WHERE id >= FLOOR(1 + RAND()*(SELECT MAX(id) FROM [table])) LIMIT 1Parece estar funcionando para mim
Mark
1

É assim que eu faria:

SET @r := (SELECT ROUND(RAND() * (SELECT COUNT(*)
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != 'draft'
        AND c.acat_slug != 'vendeglatohely'
        AND a.ac_images != 'b:0;';

SET @sql := CONCAT('
  SELECT  a.ac_id,
        a.ac_status,
        a.ac_name,
        a.ac_status,
        a.ac_images
  FROM    accomodation a
  JOIN    accomodation_category c
    ON (a.ac_category = c.acat_id)
  WHERE   a.ac_status != ''draft''
        AND c.acat_slug != ''vendeglatohely''
        AND a.ac_images != ''b:0;''
  LIMIT ', @r, ', 1');

PREPARE stmt1 FROM @sql;

EXECUTE stmt1;
Bill Karwin
fonte
Consulte também stackoverflow.com/questions/211329/…
Bill Karwin
minha tabela não é contínua porque é editada com frequência. por exemplo, atualmente o primeiro id é 121.
fabrik
3
A técnica acima não depende dos valores de id serem contínuos. Ele escolhe um número aleatório entre 1 e COUNT (*), não 1 e MAX (id) como algumas outras soluções.
Bill Karwin
1
Usar OFFSET(que é @rpara isso) não evita uma varredura - até uma varredura completa da tabela.
Rick James
@RickJames, isso mesmo. Se eu fosse responder a essa pergunta hoje, faria a consulta por chave primária. Usar um deslocamento com LIMIT faz a varredura de muitas linhas. A consulta por chave primária, embora muito mais rápida, não garante uma chance igual de escolher cada linha - favorece as linhas que seguem as lacunas.
Bill Karwin
1

(Sim, eu vou ser condenado por não ter carne suficiente aqui, mas você não pode ser vegano por um dia?)

Caso: Consecutivo AUTO_INCREMENT sem lacunas, 1 linha retornada
Caso: Consecutivo AUTO_INCREMENT sem lacunas, 10 linhas
Caso: AUTO_INCREMENT com lacunas, 1 linha retornada
Caso: coluna Extra FLOAT para randomização
Caso: coluna UUID ou MD5

Esses 5 casos podem ser muito eficientes para grandes tabelas. Veja meu blog para os detalhes.

Rick James
fonte
0

Isso lhe dará uma única subconsulta que usará o índice para obter um id aleatório e, em seguida, a outra consulta será acionada obtendo sua tabela associada.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
AND accomodation.ac_id IS IN (
        SELECT accomodation.ac_id FROM accomodation ORDER BY RAND() LIMIT 1
)
Karl Mikko
fonte
0

A solução para o seu exemplo fictício seria:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation,
        JOIN 
            accomodation_category 
            ON accomodation.ac_category = accomodation_category.acat_id
        JOIN 
            ( 
               SELECT CEIL(RAND()*(SELECT MAX(ac_id) FROM accomodation)) AS ac_id
            ) AS Choices 
            USING (ac_id)
WHERE   accomodation.ac_id >= Choices.ac_id 
        AND accomodation.ac_status != 'draft'
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
LIMIT 1

Para ler mais sobre alternativas para ORDER BY RAND(), você deve ler este artigo .

Tereško
fonte
0

Estou otimizando muitas consultas existentes em meu projeto. A solução da Quassnoi me ajudou a agilizar muito as consultas! No entanto, acho difícil incorporar a referida solução em todas as consultas, especialmente para consultas complicadas envolvendo muitas subconsultas em várias tabelas grandes.

Portanto, estou usando uma solução menos otimizada. Basicamente, ele funciona da mesma maneira que a solução de Quassnoi.

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / [accomodation_table_row_count]
LIMIT $size

$size * $factor / [accomodation_table_row_count]calcula a probabilidade de escolher uma linha aleatória. O rand () irá gerar um número aleatório. A linha será selecionada se rand () for menor ou igual à probabilidade. Isso efetivamente executa uma seleção aleatória para limitar o tamanho da tabela. Como há uma chance de que ele retorne menos do que o limite de contagem definido, precisamos aumentar a probabilidade para garantir que estamos selecionando linhas suficientes. Portanto, multiplicamos $ size por um $ fator (geralmente defino $ fator = 2, funciona na maioria dos casos). Finalmente fazemos olimit $size

O problema agora é calcular o accomodation_table_row_count . Se soubermos o tamanho da tabela, PODEMOS codificar o tamanho da tabela. Isso seria executado mais rápido, mas obviamente não é o ideal. Se você estiver usando o Myisam, obter a contagem da mesa é muito eficiente. Como estou usando o innodb, estou apenas fazendo uma simples contagem + seleção. No seu caso, seria assim:

SELECT  accomodation.ac_id,
        accomodation.ac_status,
        accomodation.ac_name,
        accomodation.ac_status,
        accomodation.ac_images
FROM    accomodation, accomodation_category
WHERE   accomodation.ac_status != 'draft'
        AND accomodation.ac_category = accomodation_category.acat_id
        AND accomodation_category.acat_slug != 'vendeglatohely'
        AND ac_images != 'b:0;'
        AND rand() <= $size * $factor / (select (SELECT count(*) FROM `accomodation`) * (SELECT count(*) FROM `accomodation_category`))
LIMIT $size

A parte complicada é calcular a probabilidade certa. Como você pode ver, o código a seguir, na verdade, apenas calcula o tamanho aproximado da tabela temporária (na verdade, muito aproximado!): (select (SELECT count(*) FROM accomodation) * (SELECT count(*) FROM accomodation_category))Mas você pode refinar essa lógica para fornecer uma aproximação mais próxima do tamanho da tabela. Observe que é melhor selecionar OVER do que sub-selecionar as linhas. ou seja, se a probabilidade for definida como muito baixa, você corre o risco de não selecionar linhas suficientes.

Esta solução é executada mais lentamente do que a solução de Quassnoi, pois precisamos recalcular o tamanho da tabela. No entanto, acho essa codificação muito mais gerenciável. Esta é uma troca entre precisão + desempenho e complexidade de codificação . Dito isso, em tabelas grandes isso ainda é muito mais rápido do que Order by Rand ().

Nota: Se a lógica da consulta permitir, execute a seleção aleatória o mais cedo possível antes de qualquer operação de junção.

Lawrenceshen
fonte
-1
function getRandomRow(){
    $id = rand(0,NUM_OF_ROWS_OR_CLOSE_TO_IT);
    $res = getRowById($id);
    if(!empty($res))
    return $res;
    return getRandomRow();
}

//rowid is a key on table
function getRowById($rowid=false){

   return db select from table where rowid = $rowid; 
}
Rokhayakebe
fonte