¿Paginación MySQL sin doble consulta?

115

Me preguntaba si había una manera de obtener el número de resultados de una consulta MySQL y, al mismo tiempo, limitar los resultados.

La forma en que funciona la paginación (según tengo entendido), primero hago algo como

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

Después de obtener num_rows (consulta), tengo el número de resultados. Pero luego, para limitar realmente mis resultados, tengo que hacer una segunda consulta como:

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

Mi pregunta: ¿Hay alguna forma de recuperar el número total de resultados que se darían y limitar los resultados devueltos en una sola consulta? O cualquier forma más eficiente de hacer esto. ¡Gracias!

atp
fuente
7
Aunque no tendría COUNT (*) en query2
dlofrodloh

Respuestas:

66

No, esa es la cantidad de aplicaciones que quieran paginar tienen que hacerlo. Es confiable y a prueba de balas, aunque hace la consulta dos veces. Pero puede almacenar en caché el recuento durante unos segundos y eso ayudará mucho.

La otra forma es usar SQL_CALC_FOUND_ROWScláusula y luego llamar SELECT FOUND_ROWS(). Aparte del hecho de que tienes que poner la FOUND_ROWS()llamada después, hay un problema con esto: hay un error en MySQL que hace cosquillas que afecta las ORDER BYconsultas haciéndolo mucho más lento en tablas grandes que el enfoque ingenuo de dos consultas.

estático
fuente
2
Sin embargo, no es una prueba de condición de carrera, a menos que realice las dos consultas dentro de una transacción. Sin embargo, esto generalmente no es un problema.
NickZoic
Por "confiable" me refiero a que el SQL en sí siempre devolverá el resultado que desea, y por "a prueba de balas" quise decir que no hay errores de MySQL que obstaculicen el SQL que puede usar. A diferencia de usar SQL_CALC_FOUND_ROWS con ORDER BY y LIMIT, de acuerdo con el error que mencioné.
Estadística:
5
En consultas complejas, el uso de SQL_CALC_FOUND_ROWS para obtener el recuento en la misma consulta casi siempre será más lento que hacer dos consultas separadas. Esto se debe a que significa que todas las filas deberán recuperarse por completo, independientemente del límite, luego solo se devuelven las especificadas en la cláusula LIMIT. Vea también mi respuesta que tiene enlaces.
thomasrutter
Dependiendo de la razón por la que lo necesite, es posible que también desee pensar en no recuperar los resultados totales. Se está convirtiendo en una práctica más común implementar métodos de paginación automática. Sitios como Facebook, Twitter, Bing y Google han estado usando este método durante años.
Thomas B
68

Casi nunca hago dos consultas.

Simplemente devuelva una fila más de la necesaria, solo muestre 10 en la página, y si hay más de las que se muestran, muestre un botón "Siguiente".

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.

Su consulta debe aparecer en el orden más relevante primero. Lo más probable es que a la mayoría de las personas no les importe ir a la página 236 de 412.

Cuando realiza una búsqueda en Google y sus resultados no están en la primera página, es probable que vaya a la página dos, no a la nueve.

Derrick
fuente
42
De hecho, si no lo encuentro en la primera página de una consulta de Google, normalmente paso a la página nueve.
Phil
3
@Phil Escuché esto antes, pero ¿por qué hacer eso?
TK123
5
Un poco tarde, pero aquí está mi razonamiento. Algunas búsquedas están dominadas por granjas de enlaces optimizadas para motores de búsqueda. Entonces, las primeras páginas son las diferentes granjas que luchan por la posición número 1, es probable que el resultado útil todavía esté asociado con la consulta, pero no en la parte superior.
Phil
4
COUNTes una función agregada. ¿Cómo devuelve el recuento y todos los resultados en una consulta? La consulta anterior solo devolverá 1 fila, sin importar en qué LIMITse establezca. Si agrega GROUP BY, devolverá todos los resultados, pero COUNTserán inexactos
pixelfreak
2
Este es uno de los enfoques recomendados por Percona: percona.com/blog/2008/09/24/…
techdude
27

Otro enfoque para evitar la doble consulta es buscar todas las filas de la página actual usando primero una cláusula LIMIT, luego solo hacer una segunda consulta COUNT (*) si se recuperó el número máximo de filas.

En muchas aplicaciones, el resultado más probable será que todos los resultados quepan en una página y tener que hacer la paginación es la excepción y no la norma. En estos casos, la primera consulta no recuperará el número máximo de resultados.

Por ejemplo, las respuestas a una pregunta de stackoverflow rara vez se extienden a una segunda página. Los comentarios sobre una respuesta rara vez superan el límite de 5 o más necesarios para mostrarlos todos.

Entonces, en estas aplicaciones, puede simplemente hacer una consulta con un LÍMITE primero, y luego, mientras no se alcance ese límite, sabrá exactamente cuántas filas hay sin la necesidad de hacer una segunda consulta COUNT (*), que debería cubre la mayoría de situaciones.

thomasrutter
fuente
1
@thomasrutter Tuve el mismo enfoque, pero descubrí un defecto hoy. La página final de resultados no tendrá los datos de paginación. es decir, digamos que cada página debería tener 25 resultados, la última página probablemente no tendrá tantos, digamos que tiene 7 ... eso significa que el recuento (*) nunca se ejecutará, por lo que no se mostrará ninguna paginación en el usuario.
duellsy
2
No, si dice, 200 resultados, consulta los siguientes 25 y solo obtiene 7 de vuelta, eso le indica que el número total de resultados es 207 y, por lo tanto, no necesita hacer otra consulta con COUNT (*) porque ya sabes lo que va a decir. Tienes toda la información que necesitas para mostrar la paginación. Si tiene un problema con la paginación que no se muestra al usuario, entonces tiene un error en otro lugar.
thomasrutter
15

En la mayoría de las situaciones, es mucho más rápido y requiere menos recursos hacerlo en dos consultas separadas que hacerlo en una, aunque eso parezca contrario a la intuición.

Si usa SQL_CALC_FOUND_ROWS, entonces para tablas grandes hace que su consulta sea mucho más lenta, significativamente más lenta incluso que ejecutar dos consultas, la primera con COUNT (*) y la segunda con LIMIT. La razón de esto es que SQL_CALC_FOUND_ROWS hace que la cláusula LIMIT se aplique después de obtener las filas en lugar de antes, por lo que recupera la fila completa para todos los resultados posibles antes de aplicar los límites. Esto no puede ser satisfecho por un índice porque en realidad recupera los datos.

Si adopta el enfoque de dos consultas, la primera solo obtiene COUNT (*) y no obtiene datos reales, esto se puede satisfacer mucho más rápidamente porque generalmente puede usar índices y no tiene que obtener los datos de fila reales para cada fila que mira. Luego, la segunda consulta solo necesita mirar las primeras filas $ offset + $ limit y luego regresar.

Esta publicación del blog de rendimiento de MySQL explica esto con más detalle:

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

Para obtener más información sobre cómo optimizar la paginación, consulte esta publicación y esta publicación .

thomasrutter
fuente
2

Mi respuesta puede llegar tarde, pero puede omitir la segunda consulta (con el límite) y simplemente filtrar la información a través de su script de back-end. En PHP, por ejemplo, podría hacer algo como:

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

Pero, por supuesto, cuando tiene que considerar miles de registros, se vuelve ineficaz muy rápido. El recuento precalculado puede ser una buena idea para analizar.

Aquí hay una buena lectura sobre el tema: http://www.percona.com/ppc2009/PPC2009_mysql_pagination.pdf

Kama
fuente
Link está muerto, supongo que este es el correcto: percona.com/files/presentations/ppc2009/… . No editaré porque no estoy seguro de si lo es.
hectorg87
1
query = SELECT col, col2, (SELECT COUNT(*) FROM `table`) AS total FROM `table` WHERE `some_condition` LIMIT 0, 10
Cris McLaughlin
fuente
16
Esta consulta solo devuelve el número total de registros en la tabla; no el número de registros que coinciden con la condición.
Lawrence Barsanti
1
El número total de registros es lo que se necesita para la paginación (@Lawrence).
Imme el
Oh, bueno, simplemente agregue la wherecláusula a la consulta interna y obtendrá el "total" correcto junto con los resultados paginados (la página está seleccionada con la limitcláusula
Erenor Paz
el recuento de subconsultas (*) requeriría la misma cláusula where o de lo contrario no devolverá el número correcto de resultados
AKrush95
1

Para cualquiera que busque una respuesta en 2020. Según la documentación de MySQL:

"El modificador de consulta SQL_CALC_FOUND_ROWS y la función FOUND_ROWS () que lo acompaña están obsoletos a partir de MySQL 8.0.17 y se eliminarán en una versión futura de MySQL. Como reemplazo, considere ejecutar su consulta con LIMIT y luego una segunda consulta con COUNT (*) y sin LIMIT para determinar si hay filas adicionales ".

Supongo que eso soluciona eso.

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

Igor K
fuente
0

Puede reutilizar la mayor parte de la consulta en una subconsulta y establecerla en un identificador. Por ejemplo, una consulta de películas que busca películas que contengan las letras ordenadas por tiempo de ejecución se vería así en mi sitio.

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;

Tenga en cuenta que no soy un experto en bases de datos y espero que alguien pueda optimizar eso un poco mejor. Tal como está, ejecutarlo directamente desde la interfaz de línea de comandos SQL, ambos toman ~ 0.02 segundos en mi computadora portátil.

Philip Rollins
fuente
-14
SELECT * 
FROM table 
WHERE some_condition 
ORDER BY RAND()
LIMIT 0, 10
Juan
fuente
3
Esto no responde a la pregunta, y un pedido por rand es una muy mala idea.
Dan Walmsley