Obtendo a contagem total de linhas de OFFSET / FETCH NEXT

90

Portanto, tenho uma função que retorna uma série de registros para os quais desejo implementar paginação em meu site. Foi sugerido que eu usasse o Offset / Fetch Next no SQL Server 2012 para fazer isso. Em nosso site, temos uma área que lista o número total de registros e em que página você está no momento.

Antes, eu recebia todo o conjunto de registros e era capaz de construir a paginação programaticamente. Mas usando o método SQL com FETCH NEXT X ROWS APENAS, recebo apenas X linhas, então não sei qual é o meu conjunto de registros total e como calcular minhas páginas mínimas e máximas. A única maneira de saber fazer isso é chamar a função duas vezes e fazer uma contagem de linhas na primeira, depois executar a segunda com FETCH NEXT. Existe uma maneira melhor de não me fazer executar a consulta duas vezes? Estou tentando acelerar o desempenho, não diminuí-lo.

Cristal azul
fonte

Respostas:

112

Você pode usar COUNT(*) OVER()... aqui está um exemplo rápido usando sys.all_objects:

DECLARE 
  @PageSize INT = 10, 
  @PageNum  INT = 1;

SELECT 
  name, object_id, 
  overall_count = COUNT(*) OVER()
FROM sys.all_objects
ORDER BY name
  OFFSET (@PageNum-1)*@PageSize ROWS
  FETCH NEXT @PageSize ROWS ONLY;

No entanto, isso deve ser reservado para pequenos conjuntos de dados; em sets maiores, o desempenho pode ser péssimo. Veja este artigo de Paul White para melhores alternativas , incluindo a manutenção de visualizações indexadas (que só funciona se o resultado não for filtrado ou você conhecer as WHEREcláusulas com antecedência) e usar ROW_NUMBER()truques.

Aaron Bertrand
fonte
44
Em uma tabela com 3.500.000 registros, o COUNT (*) OVER () levou 1 minuto e 3 segundos. A abordagem descrita abaixo por James Moberg levou 13 segundos para recuperar o mesmo conjunto de dados. Tenho certeza de que a abordagem Count Over funciona bem para conjuntos de dados menores, mas quando você começa a ficar muito grande, ela diminui consideravelmente.
matthew_360
Ou você pode apenas usar COUNT (1) OVER () que é muito mais rápido, pois não precisa ler os dados reais da tabela, como o count (*) faz
ldx
1
@AaronBertrand realmente? isso deve significar que você tem um índice que inclui todas as colunas ou que isso melhorou muito desde 2008R2. Nessa versão, a contagem (*) funciona sequencialmente, o que significa que primeiro * (como em: todas as colunas) é selecionado e depois contado. Se você fez uma contagem (1), basta selecionar uma constante, que é muito mais rápido do que ler os dados reais.
ldx
5
@idx Não, também não foi assim que funcionou em 2008 R2, desculpe. Uso o SQL Server desde 6.5 e não me lembro de uma época em que o mecanismo não fosse inteligente o suficiente para apenas verificar o índice mais estreito para COUNT (*) ou COUNT (1). Certamente não desde 2000. Mas ei, eu tenho uma instância do 2008 R2, você pode configurar um repro no SQLfiddle que demonstra essa diferença que você afirma existir? Estou feliz em tentar.
Aaron Bertrand
2
em um banco de dados sql server 2016, pesquisando em uma tabela com cerca de 25 milhões de linhas, paginando cerca de 3000 resultados (com várias junções, incluindo uma função com valor de tabela), isso levou milissegundos - incrível!
jkerak
139

Eu encontrei alguns problemas de desempenho usando o método COUNT ( ) OVER (). (Não tenho certeza se era o servidor, pois demorou 40 segundos para retornar 10 registros e depois não teve nenhum problema.) Esta técnica funcionou em todas as condições sem ter que usar COUNT ( ) OVER () e cumpre o mesma coisa:

DECLARE 
    @PageSize INT = 10, 
    @PageNum  INT = 1;

WITH TempResult AS(
    SELECT ID, Name
    FROM Table
), TempCount AS (
    SELECT COUNT(*) AS MaxRows FROM TempResult
)
SELECT *
FROM TempResult, TempCount
ORDER BY TempResult.Name
    OFFSET (@PageNum-1)*@PageSize ROWS
    FETCH NEXT @PageSize ROWS ONLY
James Moberg
fonte
31
Seria realmente incrível se houvesse a possibilidade de salvar o valor COUNT (*) em uma variável. Eu seria capaz de defini-lo como um parâmetro OUTPUT do meu procedimento armazenado. Alguma ideia?
Para Ka
1
Existe alguma maneira de obter a contagem em uma tabela separada? Parece que você só pode usar "TempResult" para a primeira instrução SELECT anterior.
matthew_360
4
Por que isso funciona tão bem? No primeiro CTE, todas as linhas são selecionadas e, em seguida, reduzidas pela busca. Eu teria imaginado que selecionar todas as linhas no primeiro CTE diminuiria significativamente as coisas. Em qualquer caso, obrigado por isso!
jbd
1
no meu caso ficou mais lento do que COUNT (1) OVER () .. talvez porque uma função no select.
Tiju John
1
Isso funciona perfeitamente para bancos de dados pequenos, quando as linhas são milhões, leva muito tempo.
Kia
1

Com base na resposta de James Moberg :

Esta é uma alternativa de uso Row_Number(), se você não tem SQL server 2012 e não pode usar OFFSET

DECLARE 
    @PageNumEnd INT = 10, 
    @PageNum  INT = 1;

WITH TempResult AS(
    SELECT ID, NAME
    FROM Tabla
), TempCount AS (
    SELECT COUNT(*) AS MaxRows FROM TempResult
)

select * 
from
(
    SELECT
     ROW_NUMBER() OVER ( ORDER BY PolizaId DESC) AS 'NumeroRenglon', 
     MaxRows, 
     ID,
     Name
    FROM TempResult, TempCount

)resultados
WHERE   NumeroRenglon >= @PageNum
    AND NumeroRenglon <= @PageNumEnd
ORDER BY NumeroRenglon
elblogdelbeto
fonte