Introduction
The issue of paging large resultsets is well familiar to most application developers and yet still no uniform generic solution exists. The excellent article "Paging of Large Resultsets in ASP.NET" by Jasmin Muharemovic presents different techniques for pagination. The purpose of this article is to suggest a generic, database independent solution for paging and sorting data in .NET using a software component (rather than a stored procedure) to generate effective SQL.
Background
The problem
Many applications need to present data in a grid sorted by a specific column. ASP.NET's DataGrid
class supports paging and sorting of the data in memory, but for this to work, all the relevant data must be retrieved and stored in the page's ViewState, which is not practical in most cases. The acceptable solution in this case is to retrieve just the data you need based on the currently selected sort column and the current page number, but neither SQL Sever nor Oracle provide built in support for such functionality.
Conclusion
Static SQL won't do, so, dynamically generated SQL is required. Basically, it means that you can not write a stored procedure with a single select
statement, instead you must create an SQL statement on the fly, which will:
- Order the query according to the current sort condition.
- Limit the result set to the required page of data.
- Filter the rows based on one or more conditions (parameterized queries).
The solution must be generic so it can be applied on all the queries, sort requirements and filter conditions your application will encounter.
Solution
Sql.Net contains classes which encapsulate the structure of an SQL statement, allowing the developer to create in memory representations of SQL queries and render them for a specific database (SQL Server, Oracle or MySql). Sql.Net can automatically create SQL which produces paged data and yet still supports query parameters, so executing parameterized, paged, sorted queries is as easy as executing a stored procedure.
Using Sql.Net
Generating and executing SQL statements using Sql.Net is a three step process.
- Create an in memory representation of your query using
SelectQuery
class.
- Render the
SelectQuery
instance into the SQL statement using SqlServerRenderer
, OracleRenderer
or MySqlRenderer
.
- Execute the resulting statement using standard ADO.NET query execution methods.
For the purpose of the following examples, we will assume a database with tables "Orders", "Products" and "Customers" where the table "Orders" relates to "Products" and "Customers" in a logical manner.
Example 1
The following code snippet describes a simplified scenario where the source of the data is a single table and no filter is applied.
IDataReader AllProducts(int pageNum, int pageSize, string orderCol, bool asc)
{
SelectQuery query = new SelectQuery();
query.Columns.Add(new SelectColumn("*"));
query.FromClause.BaseTable = FromTerm.Table("products");
query.OrderByTerms.Add(new OrderByTerm(orderCol,
(asc) ? OrderByDirection.Ascending : OrderByDirection.Descending));
SqlServerRenderer renderer = new SqlServerRenderer();
string rowCountSql = renderer.RenderRowCount(query);
int rowCount =
Convert.ToInt32(new SqlCommand(rowCountSql, connection).ExecuteScalar());
string pageSql = renderer.RenderPage(pageNum, pageSize, rowCount, query);
IDataReader data = new SqlCommand(pageSql, connection).ExecuteReader();
return data;
}
So, what happens here? The first part of the method defines a simple query, select * from products order by <col_name> <dir>
. The second part renders and executes the query. As you have probably noticed, there are two commands being executed. The first one returns the total number of rows that the query produces and the second one returns the data itself.
Note: the rendering and execution code is the same for all examples, so the following code snippets will exclude it.
In most cases, your data comes from more than one table. You have three options to specify the source of your data.
- Create a VIEW in your database and pass its name to
FromTerm.Table
method.
- Pass an SQL string to
FromTerm.SubQuery
method. query.FromClause.BaseTable =
FromTerm.SubQuery("select productName, categoryName" +
" from products p inner join categories c " +
"on p.productId = p.categoryId");
- Use Sql.Net to construct your query.
FromTerm tProducts = FromTerm.Table("products");
FromTerm tCategories = FromTerm.Table("categories");
SelectQuery query = new SelectQuery();
query.Columns.Add(new SelectColumn("productName"));
query.Columns.Add(new SelectColumn("categoryName"));
query.FromClause.BaseTable = tProducts;
query.FromClause.Join(JoinType.Inner, tProducts,
tCategories, "categoryId", "categoryId");
query.OrderByTerms.Add(new OrderByTerm(orderCol,
(asc) ? OrderByDirection.Ascending : OrderByDirection.Descending));
Parametric queries
The need to parameterize queries is obvious. You rarely display all the data in a database table; usually, the data is filtered according to the current user or context. Common practice to parameterize queries is encapsulating them in parameterized stored procedures, but unfortunately, it is not applicable to dynamic SQL. Luckily, both SQL Server and Oracle (as well as other database engines) support parameterized queries directly so you can parameterize your paged query as you would a stored procedure:
SelectQuery query = new SelectQuery();
query.Columns.Add(new SelectColumn("*"));
query.FromClause.BaseTable = FromTerm.Table("products");
query.OrderByTerms.Add(new OrderByTerm(orderCol,
(asc) ? OrderByDirection.Ascending : OrderByDirection.Descending));
query.WherePhrase.Terms.Add(WhereTerm.CreateCompare(
SqlExpression.Parameter("@param1"),
SqlExpression.Field("productId"),
CompareOperator.Equal));
SqlServerRenderer renderer = new SqlServerRenderer();
SqlCommand command = new SqlCommand();
command.Connection = connection;
command.Parameters.Add("@param1", categoryId);
command.CommandText = renderer.RenderRowCount(query);
int rowCount = (int)command.ExecuteScalar();
command.CommandText = renderer.RenderPage(pageNum,
pageSize, rowCount, query);
IDataReader data = command.ExecuteReader();
Alternatively, you can inject the parameter value directly into a SQL query like this:
query.WherePhrase.Terms.Add(WhereTerm.CreateCompare(
SqlExpression.Number(categoryId),
SqlExpression.Field("productId"), CompareOperator.Equal));
Performance
There can be three performance concerns when using Sql.Net for paging.
What kind of SQL is created and how well it performs?
Currently, Sql.Net uses the Asc-Desc technique to produce paged results. This technique is explained in the article mentioned above and provides acceptable performance when fetching the first few dozens of pages. In the future, Sql.Net is expected to support the more effective but somewhat limiting RowCount technique.
Will it not work faster when in a stored procedure?
(The following information is correct regarding SQL Server 2000, I do not have enough experience to say the same about Oracle.)
In short, no. Stored procedures provide some performance benefits when they contain static SQL because SQL Server prepares an execution plan from them once and uses it for subsequent calls. When a stored procedure contains no static SQL but rather a script which concatenates strings to produce dynamic SQL, the stored procedure has no performance benefits. Moreover, using ADO.NET and Sql.Net correctly, you can achieve similar performance that you would with a classic stored procedure. When ADO.NET executes a parameterized query, it uses the sp_executesql
stored procedure which attempts to reuse the execution plan (read "Using sp_executesql" on MSDN for more information). For the execution plan to work, object names must be fully qualified. You can use SelectQuery.TableSpace
property to set the table qualification prefix for all tables referenced by FromTerm
objects.
Doesn't the rendering process take a lot of time and resources?
Rendering times are insignificant compared to execution times. Nevertheless, future versions of Sql.Net will provide caching for frequently rendered queries and provide some rendering performance.
Extensibility
Sql.Net is an open source library built with extensibility in mind. You can easily add support for other databases, use an alternative paging technique, or add proprietary features.
More about Sql.Net
Sql.Net is not limited and was not developed specifically to support paging. It can be utilized for any purpose which requires dynamic SQL or database independence. More information, users guide, and object model reference can be found on Sql.Net web site or on its Source Forge project page.