作者: Erwin@ODS
标题: Paging records in SQL Server 2005, SQL Server 2000, MS Access and MySQL
Introduction
As a Web developer, you should know by now that using the default paging capabilities of ASP.NET Webcontrols like DataGrid and GridView is NOT recommended, simply because with every roundtrip to the data-server, you get ALL the records ALL of the time. This is fine perhaps for very small databases (the kind, say, a Microsoft programmer would use to develop a new feature), but in the real, ugly world outside, databases tend to have tables with several tenthousands of records.
Hence the need for custom paging in a WebControl.
Custom paging various databases
Custom paging poses 2 problems :
- How to get a page of records from a database (I use "a page of records" or "paged records" to indicate a limited recordset, consisting of only those records that are needed to be displayed by the ASP.NET Webcontrol). Getting these records however depends on the data-server, so you need to know the various methods. This is what this article is about.
- The second problem is how to display only the limited recordset in the webcontrol, but yet let it automatically show a pagerrow. With the DataGrid (ASP.NET 1.x) that was rather easy, but with the GridView (ASP.NET 2) it's a whole other story, especially when the binding has to be done not at design time, i.e. declaratively, but at run time (programmatically). I did not find anything useful on the Web to solve this problem, so I came up with one my own. You'll find it in the previous article I wrote (Custom_Paging_GridView.asp).
However, as far as the first problem is concerned (getting paged records from a database), there are lots of solutions on the web. Most of this article will just point to those solutions, and give some examples (using the Northwind sample database).
Let's start with the simplest solution, provided by MySQL.
MYSQL
MySQL comes with a LIMIT clause to be used in the SELECT statement.
Ex.
SELECT * FROM Products LIMIT 0, 10
The first argument denotes the offset from the 1 record found, the second argument indicates how many records should be returned. So the previous example limits the recordset to the first 10 records in the table Products.
To find the next batch, you use "SELECT * FROM Products LIMIT 10, 10", and then LIMIT 20, 10 etc..
(see http://dev.mysql.com/doc/refman/5.0/en/select.html)
The LIMIT clause is available in MySQL versions 4 and 5, but I don't know if previous versions support it.
SQL Server 2005
Long in need of something like LIMIT, Microsoft provided SQL Server 2005 with a new function ROWNUMBER()
(used in conjuction with keyword OVER
), which can be used to retrieve a limited recordset.
I'm not going into great detail of this solution, because of two things :
- it is not nearly as simple as the LIMIT clause in MySQL (Microsoft should REALLY get working on something like it !)
- it's SQL Server 2005 only, so it does not work for previous versions
However, if you want to use the GridView and the ObjectDataSourceControl declaratively in ASP.NET 2, I strongly suggest you learn more about this approach. To help you, here are 2 links :
http://www.asp.net/learn/dataaccess/tutorial25cs.aspx?tabid=63
http://weblogs.asp.net/Firoz/archive/2005/06/12/411949.aspx
SQL Server 2005, SQL Server 2000, SQL Server 7 and MS Acces
There are several solutions for paging records if you work with the database-servers listed above. But many of them involve working with temparory tables or cursors, which of course take a heavy toll performance-wise.
However, there is one solution that consists only of 1 sql-statement, and so is efficient and quick, even with large databases. The good news is : it will work ! Always ! But on the other hand, it's rather complicated.
First, let me give you the link where I found it :
http://josephlindsay.com/archives/2005/05/27/paging-results-in-ms-sql-server/
(especially the original entry is interesting and also reply # 39)
Here is the template of the SQL statement you should use :
SELECT t.fields FROM (
SELECT TOP x id_field, sort_field FROM (
SELECT TOP y id_field, sort_field
FROM table
WHERE conditions
ORDER BY sort_field ASC, id_field ASC) AS foo
ORDER BY sort_field DESC, id_field DESC) AS bar
INNER JOIN table AS t ON bar.id_field = t.id_field
ORDER BY bar.sort_field ASC, bar.id_field ASC
Legend :
SQL SERVER KEYWORDS : the words in uppercase should not be changed
t = alias of table/view
fields = name of the field(s) you want to display in the webcontrol. They should include the primary key field(s) of the table/view (whether it's displayed or not).
x = rows per page (number of rows you want to have returned)
id_field = primary key field(s) of table/view
sort_field = fields on which you want to sort the table (optional). Notice however that the sort fields always include the primary key field(s) ! This is to avoid ambiguous results.
y = the maximum number of records required. This requires a little calculation : y = pagenumber * records_per_page. So if you want to display the 3rd page, and there are 10 records per page, then y = 3 x 10 = 30. (However, the SQL statement will only return 10 records, but y has to be the maximum number of records required).
table = name of the table/view from which to get the recordset
conditions = usual WHERE-clauses go here
foo = just a name, you can use it or choose another one
bar = just a name, idem
Example
This will give you the first 10 products from the table Products of which the UnitsInStock is less than 200, ordered by ProductName. Fields you want to display in the GridView are ProductID, ProductName, UnitPrice and UnitsInStock :
SELECT p.ProductID, p.ProductName, p.UnitPrice, p.UnitsInStock FROM
(SELECT TOP 10 ProductID, ProductName FROM
(SELECT TOP 10 ProductID, ProductName
FROM Products
WHERE UnitsInStock < 200
ORDER BY ProductName ASC, ProductID ASC) AS foo
ORDER BY ProductName DESC, ProductID DESC) as bar
INNER JOIN Products p ON bar.ProductID = p.ProductID
ORDER BY bar.ProductName ASC, bar.ProductID ASC
The following will give the second page. Note that it is just the 'y' value that's changed !
SELECT p.ProductID, p.ProductName, p.UnitPrice, p.UnitsInStock FROM
(SELECT TOP 10 ProductID, ProductName FROM
(SELECT TOP 20 ProductID, ProductName
FROM Products
WHERE UnitsInStock < 200
ORDER BY ProductName ASC, ProductID ASC) AS foo
ORDER BY ProductName DESC, ProductID DESC) as bar
INNER JOIN Products p ON bar.ProductID = p.ProductID
ORDER BY bar.ProductName ASC, bar.ProductID ASC
How does this work ?
You should read the sqlstatement from the inside out :
1. The innermost SELECT statement
- The innermost SELECT statement just gives you the primary key fields (+ optionally the sort fields) of the relevant records (so, the WHERE clause is placed in the innermost SELECT statement)
- These records are sorted in ASCending order (see remark 2 if you want it in DESCending order)
- Also the number of rows are limited to the maximum rows required. Assuming that your page contains 10 rows, the number of records required for the 1st page = 10, the 2nd = 20, the 3rd = 30, etc. This means that if you have a table with 10.000 records, and you want to have the last 10 records, the innermost SELECT statement will indeed retrieve 10.000 records. This of course has it's ramifications on performance, but since it's only the primary key fields (which are indexed) and optionally some sort fields, the impact will be minimal. Also, the WHERE clause will limit the number of the base recordset.
- This recordset is named 'foo'.
2. The middle SELECT statement
- From the 'foo' recordset, the middle SELECT statement only selects the records to display. The number of rows to display is set by the TOP x clause (where x = the number of rows to display). It will be the last 'x' records because of the reverse sorting order.
- This gives you a recordset, named 'bar'.
- This recordset also consists of only 2 kind of fields : the primary key fields and the sort fields.
3. The outer SELECT statement
- In the outer SELECT statement, the primary key field(s) of 'bar' is shortcircuited with the primary key field(s) of the source table/view (with the INNER JOIN clause) so now it is possible to retrieve other fields (like UnitPrice and UnitsInStock). Again, this recordset has to be sorted in ascending order.
Conclusion
The SQL statement plays with the sorting orders to limit the records, thus resulting in paged recordsets.
This means that there should be at least ONE field to be sorted. If there isn't any, sort on the primary key field(s) !
Remarks
- When sorting, put the primary key fields AFTER the sorting fields
- If you want to have the results in DESCending order, then you have to change all the ASC keywords in DESC, and all the DESC in ASC, and there you have it !
- A problem arises when you come to the end of the table. Say you have 84 records in your table, and you want to have the last page, there should only be 4 records returned. Unlike the LIMIT statement in MySQL, you have to do the calculation yourself. This means that you need to keep track if the last page is requested, and if so, you need to calculate the number of records returned. This then should be the value of x in the SQL statement.