Paginação MySQL sem consulta dupla?

115

Eu queria saber se havia uma maneira de obter o número de resultados de uma consulta MySQL e, ao mesmo tempo, limitar os resultados.

A maneira como a paginação funciona (pelo que entendi), primeiro faço algo como

query = SELECT COUNT(*) FROM `table` WHERE `some_condition`

Depois de obter o num_rows (consulta), tenho o número de resultados. Mas, para realmente limitar meus resultados, tenho que fazer uma segunda consulta como:

query2 = SELECT COUNT(*) FROM `table` WHERE `some_condition` LIMIT 0, 10

Minha pergunta: Existe alguma maneira de recuperar o número total de resultados que seriam fornecidos E limitar os resultados retornados em uma única consulta? Ou qualquer forma mais eficiente de fazer isso. Obrigado!

atp
fonte
8
Embora você não tivesse COUNT (*) na consulta 2
dlofrodloh

Respostas:

66

Não, é assim que muitos aplicativos que desejam paginar precisam fazer isso. É confiável e à prova de balas, embora faça a consulta duas vezes. Mas você pode armazenar a contagem em cache por alguns segundos e isso ajudará muito.

A outra maneira é usar a SQL_CALC_FOUND_ROWScláusula e depois chamar SELECT FOUND_ROWS(). além do fato de você ter que fazer a FOUND_ROWS()chamada depois, há um problema com isso: há um bug no MySQL que isso afeta as ORDER BYconsultas, tornando-as muito mais lentas em tabelas grandes do que a abordagem ingênua de duas consultas.

staticsan
fonte
2
Não é totalmente à prova de condição de corrida, no entanto, a menos que você faça as duas consultas em uma transação. Isso geralmente não é um problema.
NickZoic
Por "confiável" eu quis dizer que o próprio SQL sempre retornará o resultado que você deseja, e por "à prova de balas" eu quis dizer que não há bugs do MySQL impedindo o SQL que você pode usar. Ao contrário de usar SQL_CALC_FOUND_ROWS com ORDER BY e LIMIT, de acordo com o bug que mencionei.
staticsan
5
Em consultas complexas, usar SQL_CALC_FOUND_ROWS para buscar a contagem na mesma consulta quase sempre será mais lento do que fazer duas consultas separadas. Isso porque significa que todas as linhas precisarão ser recuperadas por completo, independentemente do limite, então apenas aquelas especificadas na cláusula LIMIT serão retornadas. Veja também minha resposta que contém links.
thomasrutter
Dependendo do motivo pelo qual você precisa disso, você também pode pensar em não recuperar os resultados totais. Está se tornando uma prática mais comum implementar métodos de paginação automática. Sites como Facebook, Twitter, Bing e Google usam esse método há muito tempo.
Thomas B
68

Quase nunca faço duas consultas.

Basta retornar uma linha a mais do que o necessário, exibir apenas 10 na página e, se houver mais do que exibido, exibir um botão "Avançar".

SELECT x, y, z FROM `table` WHERE `some_condition` LIMIT 0, 11
// iterate through and display 10 rows.

// if there were 11 rows, display a "Next" button.

Sua consulta deve retornar na ordem mais relevante primeiro. Provavelmente, a maioria das pessoas não vai se importar em ir para a página 236 de 412.

Quando você faz uma pesquisa no Google e seus resultados não estão na primeira página, você provavelmente vai para a página dois, não para a nove.

Torre
fonte
42
Na verdade, se eu não encontrar na primeira página de uma consulta do Google, geralmente pulo para a página nove.
Phil
3
@Phil Já ouvi isso antes, mas por que fazer isso?
TK123
5
Um pouco tarde, mas aqui está o meu raciocínio. Algumas pesquisas são dominadas por fazendas de links otimizados para mecanismos de pesquisa. Portanto, as primeiras páginas são os diferentes farms lutando pela posição número 1, o resultado útil provavelmente ainda está associado à consulta, mas não no topo.
Phil
4
COUNTé uma função agregada. Como você retorna a contagem e todos os resultados em uma consulta? A consulta acima retornará apenas 1 linha, não importa como o LIMITestá definido. Se você adicionar GROUP BY, ele retornará todos os resultados, mas COUNTserá impreciso
pixelfreak
2
Esta é uma das abordagens recomendadas por Percona
techdude
27

Outra abordagem para evitar a consulta dupla é buscar primeiro todas as linhas da página atual usando uma cláusula LIMIT e, em seguida, fazer uma segunda consulta COUNT (*) se o número máximo de linhas for recuperado.

Em muitas aplicações, o resultado mais provável será que todos os resultados caibam em uma página, e ter que fazer a paginação é a exceção, e não a norma. Nesses casos, a primeira consulta não recuperará o número máximo de resultados.

Por exemplo, as respostas a uma questão stackoverflow raramente passam para uma segunda página. Os comentários sobre uma resposta raramente ultrapassam o limite de 5 ou mais necessários para mostrá-los todos.

Então, nesses aplicativos, você pode simplesmente fazer uma consulta com um LIMIT primeiro e, em seguida, desde que esse limite não seja atingido, você sabe exatamente quantas linhas existem sem a necessidade de fazer uma segunda consulta COUNT (*) - o que deve cobrir a maioria das situações.

Thomasrutter
fonte
1
@thomasrutter Eu usei a mesma abordagem, porém descobri uma falha hoje. A página final de resultados não terá os dados de paginação. ou seja, digamos que cada página deva ter 25 resultados, a última página provavelmente não terá tantos, digamos que tenha 7 ... isso significa que a contagem (*) nunca será executada e, portanto, nenhuma paginação será exibida para o do utilizador.
duellsy
2
Não - se você disser 200 resultados em, você consulta os próximos 25 e obtém apenas 7 de volta, o que indica que o número total de resultados é 207 e, portanto, você não precisa fazer outra consulta com COUNT (*) porque você já sabe o que vai dizer. Você tem todas as informações de que precisa para mostrar a paginação. Se você está tendo um problema com a paginação não sendo exibida para o usuário, você tem um bug em outro lugar.
thomasrutter
15

Na maioria das situações, é muito mais rápido e menos intensivo em recursos fazê-lo em duas consultas separadas do que em uma, mesmo que pareça um contra-senso.

Se você usar SQL_CALC_FOUND_ROWS, então para tabelas grandes torna sua consulta muito mais lenta, significativamente mais lenta até do que executar duas consultas, a primeira com COUNT (*) e a segunda com LIMIT. A razão para isso é que SQL_CALC_FOUND_ROWS faz com que a cláusula LIMIT seja aplicada depois de buscar as linhas em vez de antes, portanto, busca a linha inteira para todos os resultados possíveis antes de aplicar os limites. Isso não pode ser satisfeito por um índice porque ele realmente busca os dados.

Se você adotar a abordagem de duas consultas, a primeira apenas buscando COUNT (*) e não realmente buscando dados reais, isso pode ser satisfeito muito mais rapidamente porque geralmente pode usar índices e não precisa buscar os dados reais da linha para cada linha que olha. Então, a segunda consulta só precisa olhar as primeiras linhas $ offset + $ limit e depois retornar.

Esta postagem do blog de desempenho do MySQL explica isso melhor:

http://www.mysqlperformanceblog.com/2007/08/28/to-sql_calc_found_rows-or-not-to-sql_calc_found_rows/

Para obter mais informações sobre como otimizar a paginação, verifique esta postagem e esta postagem .

Thomasrutter
fonte
2

Minha resposta pode estar atrasada, mas você pode pular a segunda consulta (com o limite) e apenas filtrar as informações por meio de seu script de back end. Em PHP, por exemplo, você poderia fazer algo como:

if($queryResult > 0) {
   $counter = 0;
   foreach($queryResult AS $result) {
       if($counter >= $startAt AND $counter < $numOfRows) {
            //do what you want here
       }
   $counter++;
   }
}

Mas é claro que, quando você tem milhares de registros a considerar, isso se torna ineficiente muito rápido. A contagem pré-calculada pode ser uma boa ideia.

Aqui está uma boa leitura sobre o assunto: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

Kama
fonte
Link está morto, acho que este é o correto: percona.com/files/presentations/ppc2009/… . Não vou editar porque não tenho certeza se é.
hectorg87
1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
Cris McLaughlin
fonte
16
Esta consulta retorna apenas o número total de registros na tabela; não o número de registros que correspondem à condição.
Lawrence Barsanti
1
O número total de registros é o que é necessário para a paginação (@Lawrence).
Imme
Bem, basta adicionar a wherecláusula à consulta interna e você obterá o "total" correto junto com os resultados paginados (a página é selecionada com a limitcláusula
Erenor Paz
a contagem de subconsultas (*) exigiria a mesma cláusula where ou então não retornaria o número correto de resultados
AKrush95
1

Para quem procura uma resposta em 2020. De acordo com a documentação do MySQL:

"O modificador de consulta SQL_CALC_FOUND_ROWS e a função FOUND_ROWS () que o acompanha estão obsoletos a partir do MySQL 8.0.17 e serão removidos em uma versão futura do MySQL. Como uma substituição, considere executar sua consulta com LIMIT e, em seguida, uma segunda consulta com COUNT (*) e sem LIMIT para determinar se há linhas adicionais. "

Eu acho que isso resolve tudo.

https://dev.mysql.com/doc/refman/8.0/en/information-functions.html#function_found-rows

Igor K
fonte
0

Você pode reutilizar a maior parte da consulta em uma subconsulta e defini-la como um identificador. Por exemplo, uma consulta de filme que encontra filmes contendo a ordem das letras por tempo de execução ficaria assim no meu site.

SELECT Movie.*, (
    SELECT Count(1) FROM Movie
        INNER JOIN MovieGenre 
        ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
    WHERE Title LIKE '%s%'
) AS Count FROM Movie 
    INNER JOIN MovieGenre 
    ON MovieGenre.MovieId = Movie.Id AND MovieGenre.GenreId = 11
WHERE Title LIKE '%s%' LIMIT 8;

Observe que não sou um especialista em banco de dados e espero que alguém consiga otimizar isso um pouco melhor. Como está, executando-o diretamente da interface de linha de comando SQL, ambos levam cerca de 0,02 segundos no meu laptop.

Philip Rollins
fonte
-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10
John
fonte
3
Isso não responde à pergunta, e um pedido por rand é uma péssima ideia.
Dan Walmsley