Introduction
Paging is almost perfunctory when dealing with large lists of online data. Problem is, most paging solutions out there (at least those I’ve seen) perform this vital function on the client side – either through dangerous dynamic SQL or even worse – pulling the entire record set down and disseminating pages for the user afterward.
You wouldn’t (I hope) trust the client to put data into your tables, so why trust it to filter and page? That’s what your database engine was specifically designed to do! Server side paging gives you a couple of big advantages:
- Security – See above. I would never, and will never trust a client generated SQL. You’re just asking for it.
- Scaling – Client side record processing may seem faster at first because of the instant response. Then your record count passes the VERY modest six digit mark, and suddenly you’re looking for ways to mediate ten minute load times.
The only real downsides to sever side paging are reloading and complexity of initial setup. The former can be dealt with using an AJAX or similar solution. The later is where I come in. The following stored procedure completely encapsulates paging, all of any other data extraction.
Implementation
Execute within data procedure after all other record processing is complete on the primary data table. Assumes primary data is in temp table #cache_primary. Pass following arguments:
- page_current – Current record page to view as requested by control code.
- page_rows (optional, uses default value if NULL) – Number of rows (records) to output per page.
Outputs following record set for use by control code:
Column | Type | Description |
---|---|---|
row_count_total | int | Total number of rows in the paged record set. |
page_rows | int | Maximum number of rows per page. Will be same as the maximum row argument passed form control code unless that argument was null, in which case this will reflect the default maximum rows. |
page_last | int | Last page number / total number of pages. |
row_first | int | ID of first record in requested page. |
row_last | int | ID of last record in requested page. |
SQL
-- Master Paging -- Caskey, Damon V. -- 2016-07-08 -- -- Output recordset in divided pages. Also creates and outputs -- a recordset of paging data for control code. Execute in another -- stored procedure after all other record work (filters, sorting, joins, etc.) -- is complete. Make sure final table variable name is #cache_primary. -- Set standard ISO behavior for handling NULL -- comparisons and quotations. ALTER PROCEDURE [dbo].[master_paging] -- Parameters. @param_page_current int = 1, -- Current page of records to display. @param_page_rows smallint = 25 -- (optional) max number of records to display in a page. AS BEGIN -- If non paged layout is requested (current = -1), then just -- get all records and exit the procedure immediately. IF @param_page_current = -1 BEGIN SELECT * FROM #cache_primary RETURN END -- Verify arguments from control code. If something -- goes out of bounds we'll use stand in values. This -- also lets the paging "jumpstart" itself without -- needing input from the control code. -- Current page default. IF @param_page_current IS NULL OR @param_page_current < 1 SET @param_page_current = 1 -- Rows per page default. IF @param_page_rows IS NULL OR @param_page_rows < 1 SET @param_page_rows = 10 -- Declare the working variables we'll need. DECLARE @row_count_total int, -- Total row count of primary table. @page_last float, -- Number of the last page of records. @row_first int, -- Row ID of first record. @row_last int -- Row ID of last record. -- Set up table var so we can reuse results. CREATE TABLE #cache_paging ( id_row int, id_paging int ) -- Populate paging cache. This is to add an -- ordered row number column we can use to -- do paging math. INSERT INTO #cache_paging (id_row, id_paging) (SELECT ROW_NUMBER() OVER(ORDER BY @@rowcount) AS id_row, id FROM #cache_primary _main) -- Get total count of records. SET @row_count_total = (SELECT COUNT(id_row) FROM #cache_paging); -- Get paging first and last row limits. Example: If current page -- is 2 and 10 records are allowed per page, the first row should -- be 11 and the last row 20. SET @row_first = (@param_page_current - 1) * @param_page_rows SET @row_last = (@param_page_current * @param_page_rows + 1); -- Get last page number. SET @page_last = (SELECT CEILING(CAST(@row_count_total AS FLOAT) / CAST(@param_page_rows AS FLOAT))) IF @page_last = 0 SET @page_last = 1 -- Extract paged rows from page table var, join to the -- main data table where IDs match and output as a recordset. -- This gives us a paged set of records from the main -- data table. SELECT TOP (@row_last-1) * FROM #cache_paging _paging JOIN #cache_primary _primary ON _paging.id_paging = _primary.id WHERE id_row > @row_first AND id_row < @row_last ORDER BY id_row -- Output the paging data as a recordset for use by control code. SELECT @row_count_total AS row_count_total, @param_page_rows AS page_rows, @page_last AS page_last, @row_first AS row_first, @row_last AS row_last END