Il problema di dover paginare i risultati di una query a SQL Server (ma più in generale ad un qualunque DBMS) ricorre spesso, specialmente nelle applicazioni distribuite dove è fondamentale ridurre al minimo la quantità di dati trasportati sul canale.
SQL Server 2005 offre alcuni operatori SQL che permettono di costruire uno "schema" per tradurre una qualunque query nella equivalente versione che restituisce la sola pagina di dati voluta.
Supponiamo ad es. di voler paginare i dati della seguente query (Northwind):
SELECT *
FROM Customers
ORDER BY CustomerID ASC
Isoliamo la clausola ORDER BY dal resto della query ottenendo quindi qualcosa del tipo
SELECT *
FROM (
SELECT
*,
ROW_NUMBER() OVER (ORDER BY CustomerID ASC) AS [_RowNum]
FROM Customers
) AS T
WHERE [_RowNum] BETWEEN @Min AND @Max
ORDER BY [_RowNum] ASC
I colori mostrano le parti della query SQL prima e dopo la trasformazione. I parametri @Min e @Max devono essere sostituiti rispettivamente con l'indice del primo record e dell'ultimo record della pagina desiderata.
Lo schema indicato, con alcune limitazioni (ad es. è obbligatorio specificare esplicitamente la clausola ORDER BY), permette di trasformare una query complessa a piacere nell'equivalente paginabile. Detto questo, è possibile costruire una stored procedure che, facendo uso degli statement SQL dinamici, permette di paginare i risultati di una qualunque query:
CREATE PROCEDURE [dbo].[spQuery]
@Sql NVARCHAR(MAX) = N'',
@OrderBy NVARCHAR(MAX) = N'',
@PageNumber INT = 1 OUT,
@PageSize INT = 0,
@NoRecordCount SMALLINT = 0,
@RecordCount INT = 0 OUT
AS
DECLARE @SqlString NVARCHAR(MAX)
DECLARE @PageCount INT
DECLARE @Min INT
DECLARE @Max INT
DECLARE @RowNum INT
SET @Sql = LTRIM(RTRIM(ISNULL(@Sql, N'')))
IF @Sql = N''
RETURN 1 -- Missed Sql.
SET @OrderBy = LTRIM(RTRIM(ISNULL(@OrderBy, N'')))
IF @OrderBy = N''
RETURN 2 -- Missed OrderBy.
IF @PageNumber <= 0
SET @PageNumber = 1
-- Vengono contati i record.
SET @RecordCount = -1
IF @NoRecordCount = 0
BEGIN
SET @SqlString = N'SELECT @RecordCount = COUNT(*) FROM (' + @Sql + N') AS T'
EXEC sp_executesql @SqlString, N'@RecordCount int out', @RecordCount OUT
END
-- Se occorre contare i record...
IF @PageSize = 0
BEGIN
SET @PageCount = 1
SET @PageNumber = 1
END
ELSE
BEGIN
IF @RecordCount < 0
BEGIN
SET @PageCount = 0
END
ELSE
BEGIN
SET @PageCount = CEILING(CAST(@RecordCount AS FLOAT) / CAST(@PageSize AS FLOAT))
-- @PageNumber = 0 => ultima pagina
-- @PageNumber = N => N-esima pagina o ultima se @PageNumber > @PageCount
-- @PageNumber = -N => @PageNumber = @PageCount - N (penultima, ...)
-- Se alla fine @PageNumber < 0 => @PageNumber = @PageCount
IF @PageNumber <= 0
OR @PageNumber > @PageCount
SET @PageNumber = @PageCount - @PageNumber
IF @PageNumber <= 0
SET @PageNumber = @PageCount
END
END
-- Deve essere rimpiazzata solo la prima occorrenza di SELECT ... => SELECT ROW_NUMBER() OVER (...) AS [_RowNum]
IF @PageSize > 0 AND @NoRecordCount <> 0
SET @SqlString = N'SELECT TOP ' + CAST((@PageNumber * @PageSize) AS NVARCHAR(MAX)) + N' * '
ELSE
SET @SqlString = N'SELECT * '
SET @SqlString = @SqlString +
N'FROM (' +
N'SELECT ROW_NUMBER() OVER (ORDER BY ' + @OrderBy + N') AS [_RowNum],' + SUBSTRING(@Sql, 7, LEN(@Sql) - 6) +
N') AS T'
IF @PageSize > 0
BEGIN
SET @Min = @PageSize * ( @PageNumber - 1 ) + 1
SET @Max = @Min + @PageSize - 1
SET @SqlString = @SqlString + N' WHERE [_RowNum] BETWEEN @Min AND @Max'
END
SET @SqlString = @SqlString + N' ORDER BY [_RowNum] ASC'
EXEC sp_executesql @SqlString, N'@Min int, @Max int', @Min = @Min, @Max = @Max
RETURN 0 -- No error.
Esempio di invocazione:
DECLARE @RC INT
EXEC dbo.spQuery
N'SELECT * FROM Customers', -- Query SQL a meno della clausola ORDER BY principale
N'CustomerID ASC', -- Clausola ORDER BY principale
1, -- Numero di pagina voluto (pagina 1 di ...)
3, -- Numero di record per pagina (3 record per pagina)
0, -- Mi interessa avere il conteggio dei record totali (senza paginazione)
@RC OUT -- Numero di record totali
mercoledì 26 marzo 2008 01:55