Execute uma consulta com um LIMIT / OFFSET e obtenha também o número total de linhas

103

Para fins de paginação, preciso executar uma consulta com as cláusulas LIMITe OFFSET. Mas também preciso contar o número de linhas que seriam retornadas por essa consulta sem as cláusulas LIMITe OFFSET.

Eu quero correr:

SELECT * FROM table WHERE /* whatever */ ORDER BY col1 LIMIT ? OFFSET ?

E:

SELECT COUNT(*) FROM table WHERE /* whatever */

Ao mesmo tempo. Existe uma maneira de fazer isso, particularmente uma maneira que permite que o Postgres otimize isso, de modo que seja mais rápido do que executar ambos individualmente?

Tim
fonte

Respostas:

179

Sim. Com uma função de janela simples:

SELECT *, count(*) OVER() AS full_count
FROM   tbl
WHERE  /* whatever */
ORDER  BY col1
OFFSET ?
LIMIT  ?

Esteja ciente de que o custo será substancialmente maior do que sem o número total, mas normalmente ainda mais barato do que duas consultas separadas. O Postgres tem que contar todas as linhas de qualquer maneira, o que impõe um custo dependendo do número total de linhas qualificadas. Detalhes:

No entanto , como Dani apontou , quando OFFSETé pelo menos tão grande quanto o número de linhas retornadas da consulta base, nenhuma linha é retornada. Então também não entendemos full_count.

Se isso não for aceitável, uma possível solução alternativa para sempre retornar a contagem completa seria com um CTE e um OUTER JOIN:

WITH cte AS (
   SELECT *
   FROM   tbl
   WHERE  /* whatever */
   )
SELECT *
FROM  (
   TABLE  cte
   ORDER  BY col1
   LIMIT  ?
   OFFSET ?
   ) sub
RIGHT  JOIN (SELECT count(*) FROM cte) c(full_count) ON true;

Você obtém uma linha de valores NULL com o full_countanexado se OFFSETfor muito grande. Caso contrário, é anexado a todas as linhas, como na primeira consulta.

Se uma linha com todos os valores NULL for um resultado válido possível, você deve verificar offset >= full_countpara desambiguar a origem da linha vazia.

Isso ainda executa a consulta de base apenas uma vez. Mas adiciona mais sobrecarga à consulta e só paga se isso for menor do que repetir a consulta de base para a contagem.

Se os índices que suportam a ordem de classificação final estiverem disponíveis, pode valer a pena incluir o ORDER BYno CTE (redundantemente).

Erwin Brandstetter
fonte
3
Por LIMIT e condições, temos linhas a serem retornadas, mas com o deslocamento fornecido, ele não retornaria nenhum resultado. Nessa situação, como poderíamos obter a contagem de linhas?
Dani Mathew
muito legal, obrigado, funciona muito bem quando vc usa paginação, datatables, basta adicionar isso no inicio do seu sql, e usar, salve uma consulta extra para contagem total.
Ahmed Sunny
Você poderia explicar melhor se a contagem pudesse ser ativada dinamicamente na consulta por meio de um parâmetro de entrada? Tenho um requisito semelhante, mas o usuário decide se deseja a contagem em linha ou não.
julealgon,
1
@julealgon: Por favor, comece uma nova pergunta com os detalhes definidores. Você sempre pode vincular a este para contexto e adicionar deixe um comentário aqui para vincular de volta (e chamar minha atenção) se desejar.
Erwin Brandstetter,
1
@ JustinL .: A sobrecarga adicionada deve ser significativa apenas para consultas de base relativamente baratas. Além disso, o Postgres 12 melhorou o desempenho CTE de várias maneiras. (Embora este CTE ainda seja o MATERIALIZEDpadrão, sendo referenciado duas vezes.)
Erwin Brandstetter
0

editar: esta resposta é válida ao recuperar a tabela não filtrada. Vou deixar isso acontecer, caso possa ajudar alguém, mas pode não responder exatamente à pergunta inicial.

A resposta de Erwin Brandstetter é perfeita se você precisa de um valor preciso. No entanto, em tabelas grandes, você geralmente só precisa de uma boa aproximação. O Postgres oferece exatamente isso e será muito mais rápido, pois não precisará avaliar cada linha:

SELECT *
FROM (
    SELECT *
    FROM tbl
    WHERE /* something */
    ORDER BY /* something */
    OFFSET ?
    LIMIT ?
    ) data
RIGHT JOIN (SELECT reltuples FROM pg_class WHERE relname = 'tbl') pg_count(total_count) ON true;

Na verdade, não tenho certeza se há uma vantagem em externalizar o RIGHT JOINou tê-lo como em uma consulta padrão. Isso mereceria alguns testes.

SELECT t.*, pgc.reltuples AS total_count
FROM tbl as t
RIGHT JOIN pg_class pgc ON pgc.relname = 'tbl'
WHERE /* something */
ORDER BY /* something */
OFFSET ?
LIMIT ?
François Gueguen
fonte
2
Sobre a estimativa de contagem rápida: stackoverflow.com/a/7945274/939860 Como você disse: válido ao recuperar a tabela inteira - o que é contradito pela WHEREcláusula em suas consultas. A segunda consulta é logicamente errada (recupera uma linha para cada tabela no banco de dados) - e mais cara quando corrigida.
Erwin Brandstetter
0

Embora a resposta de Erwin Brandstetter funcione perfeitamente, ela retorna a contagem total de linhas em cada linha como a seguir:

col1 - col2 - col3 - total
--------------------------
aaaa - aaaa - aaaa - count
bbbb - bbbb - bbbb - count
cccc - cccc - cccc - count

Você pode querer considerar o uso de uma abordagem que retorna a contagem total apenas uma vez , como a seguir:

total - rows
------------
count - [{col1: 'aaaa'},{col2: 'aaaa'},{col3: 'aaaa'}
         {col1: 'bbbb'},{col2: 'bbbb'},{col3: 'bbbb'}
         {col1: 'cccc'},{col2: 'cccc'},{col3: 'cccc'}]

Consulta SQL:

SELECT 
    (SELECT COUNT(*) FROM table) as count, 
    (SELECT json_agg(t.*) FROM (
        SELECT * FROM table
        WHERE /* whatever */
        ORDER BY col1
        OFFSET ?
        LIMIT ?
    ) AS t) AS rows 
Treecon
fonte
-6

É uma má prática chamar duas vezes a mesma consulta para Just para obter o número total de linhas do resultado returend. Isso levará tempo de execução e desperdiçará os recursos do servidor.

Melhor, você pode usar SQL_CALC_FOUND_ROWSna consulta que dirá ao MySQL para buscar o número total de contagem de linhas junto com os resultados da consulta limite.

Exemplo definido como:

SELECT SQL_CALC_FOUND_ROWS employeeName, phoneNumber FROM employee WHERE employeeName LIKE 'a%' LIMIT 10;

SELECT FOUND_ROWS();

Na consulta acima, basta adicionar a SQL_CALC_FOUND_ROWSopção na consulta restante necessária e executar a segunda linha, ou seja, SELECT FOUND_ROWS()retorna o número de linhas no conjunto de resultados retornado por essa instrução.

Mohd Rashid
fonte
1
A solução requer postgres, não mysql.
MuffinMan
@MuffinMan, você pode usar o mesmo no mysql. Desde o MYSQL 4.0, está sendo usada a opção SQL_CALC_FOUND_ROWS na consulta. Mas a partir do MYSQL 8.0 ele está obsoleto.
Mohd Rashid
Não é relevante. Esta pergunta foi respondida anos atrás. Se você deseja contribuir, poste uma nova questão com o mesmo assunto, mas específica para MySQL.
MuffinMan
sempre seja relevante
Ali Hussain
-15

Não.

Talvez haja um pequeno ganho que você poderia ganhar teoricamente ao executá-los individualmente com maquinários complicados suficientes sob o capô. Mas, se você quiser saber quantas linhas correspondem a uma condição, você terá que contá-las em vez de apenas um subconjunto LIMITed.

Richard Huxton
fonte