在 SQL Server 上执行分页的最佳方法是啥?

Posted

技术标签:

【中文标题】在 SQL Server 上执行分页的最佳方法是啥?【英文标题】:Which is the best way to perform pagination on SQL Server?在 SQL Server 上执行分页的最佳方法是什么? 【发布时间】:2012-12-29 15:32:49 【问题描述】:

我有一个包含超过 200 万 条记录的数据库,我需要执行分页以显示在我的 Web 应用程序上,DataGrid 中每页必须有 10 条记录。

我已经尝试使用ROW_NUMBER(),但是这种方式会选择所有200万条记录,然后只得到10条记录。我也尝试使用TOP 10,但我必须保存第一个和最后一个 id 来控制页面。而且我读过使用DataAdapter.Fill() 将选择所有内容,然后获取我需要的10条记录。

最好的方法是什么?我应该使用DataAdapter.Fill() 吗?还是使用SQL Server的函数ROW_NUMBER()?或者尝试使用TOP 10

【问题讨论】:

看到这个blog.sqlauthority.com/2010/12/15/… 请阅读:asp.net/web-forms/tutorials/data-access/paging-and-sorting/… 这是一个非常完整的教程(VB.NET,但在这种情况下并不重要)。 @Guilherme 你真的还在使用 sql-server-2000 吗? (标签)如果是这样,那将大大限制您的选择。 他使用的是 SQL 2008(他称之为 2010),@Soner 编辑了他的问题并添加了错误的标签 @Guilherme Oliveira: any 方法在后面的页面上会变慢,因为数据库计算这些只是更多的工作 - 毕竟,要确定一个确切的行号row 它通常需要至少唯一地标识它之前的所有行。 【参考方案1】:
ALTER PROCEDURE [dbo].[SP_tblTest_SelectSpecificRecordsWithCTE]
    @FromRow int = 1000000,
    @PgSize int = 10
AS
BEGIN
    ;WITH RecordsRN AS
    (
        select ID, colValue, ROW_NUMBER() over(order by colvalue) as Num from tblTest
    )
    SELECT ID Value, colValue Text FROM RecordsRN WHERE Num between @FromRow AND (@FromRow+@PgSize-1)
END

这是我用于分页的查询。使用它,您将在 4-5 秒内获得您想要的 10 条记录。我在 3 秒内获得 10 条记录,我的数据库中的总记录为 1000 万条,不要使用前 10 名,它每次只会带来相同的 10 条记录。在我的情况下,我在会话中维护页面大小和起始行号(@FromRow),并将这两个值传递给下面给定的存储过程并获得结果。 此外,如果您使用的是 SQL 2012,您可能希望使用 OFFSET 和 Fetch next 10 rows 之类的东西。在 google 上搜索 OFFSET 关键字,您会在顶部看到您想要的结果。

谢谢

【讨论】:

所有答案都非常好,但您的答案与我所做的更相似,我仍然认为这是最好的方法。不幸的是,我的 SQL Server 是 2008,所以我没有要使用的 OFFSET 语句。谢谢。【参考方案2】:

我使用以下模式(自动)生成分页子查询:

select top (@takeN) <your-column-list>
from (
    select qSub2.*, _row=row_number() over (order by SomeColumn Asc, SomethingElse Desc)
    from (
        select top (@takeN + @skipN) <your-column-list> 
        from ( 
            select <your-subquery-here>
        ) as qSub1 
        order by SomeColumn Asc, SomethingElse Desc
    ) as qSub2
) qSub3
where _row > @skipN
order by _row

关于这种模式的注释:

子查询在概念上跳过@skipN 行,然后采用下一个@takeN 行。 如果您不关心结果中的额外列_row,您可以将&lt;your-column-list&gt; 替换为*;我使用显式列列表,因为它允许我在运行时对列集进行子集化,这可能很有用,例如只查找主键列等。 您的order by 子句应该相同; sql server 的优化器通常足够聪明,可以理解这一点。重复是用于截断结果的top 子句的副作用; top 对于未排序的子查询是不合法的。 top 有助于帮助查询优化器了解此查询可能会返回几行。 使用@takeN@skipN 而不是基于页码+ 大小的参数的原因相当小。首先,它在查询中更灵活一点,更简单一点,其次,它更好地发挥了 sql server 的优势:DB 在优化这类查询方面并不是特别出色,希望是像这样的外部简单顶部子句使优化器了解可能的最大行数变得微不足道。一般来说,我尽量避免在 sql 中进行计算,我可以在代码中同样出色地完成,因为它往往会混淆优化器(尽管在特定情况下 @pagecount*@pagesize 实验表明这不是一个大问题)

请注意,sql server 2012 支持一个新的offset...fetch clause 正是这种情况要简单得多。

【讨论】:

【参考方案3】:

使用ROW_NUMBER() 并实现一个静态实用函数(如我的代码中的GetPaginatedSQL),它会自动将您的原始SQL 查询包装成一个有限/分页的查询。

这是我用的:

namespace Persistence.Utils

    public class SQLUtils
    
        /// <summary>
        /// Builds a paginated/limited query from a SELECT SQL.
        /// </summary>
        /// <param name="startRow">Start row</param>
        /// <param name="numberOfRows">Number/quatity of rows to be expected</param>
        /// <param name="sql">Original SQL (without its ordering clause)</param>
        /// <param name="orderingClause">MANDATORY: ordering clause (including ORDER BY keywords)</param>
        /// <returns>Paginated SQL ready to be executed.</returns>
        /// <remarks>SELECT keyword of original SQL must be placed exactly at the beginning of the SQL.</remarks>
        public static string GetPaginatedSQL(int startRow, int numberOfRows, string sql, string orderingClause)
        
            // Ordering clause is mandatory!
            if (String.IsNullOrEmpty(orderingClause))
                throw new ArgumentNullException("orderingClause");

            // numberOfRows here is checked of disable building paginated/limited query
            // in case is not greater than 0. In this case we simply return the
            // query with its ordering clause appended to it. 
            // If ordering is not spe
            if (numberOfRows <= 0)
            
                return String.Format("0 1", sql, orderingClause);
            
            // Extract the SELECT from the beginning.
            String partialSQL = sql.Remove(0, "SELECT ".Length);

            // Build the limited query...
            return String.Format(
                "SELECT * FROM ( SELECT ROW_NUMBER() OVER (0) AS rn, 1 ) AS SUB WHERE rn > 2 AND rn <= 3",
                orderingClause,
                partialSQL,
                startRow.ToString(),
                (startRow + numberOfRows).ToString()
            );
        
    

上述功能可能会有所改进,但只是初步实现。

那么,在你的 DAO 中,你应该只做这样的事情:

using (var conn = new SqlConnection(CONNECTION_STRING))

    using (var cmd = conn.CreateCommand())
    
        String SQL = "SELECT * FROM MILLIONS_RECORDS_TABLE";
        String SQLOrderBy = "ORDER BY DATE ASC "; //GetOrderByClause(Object someInputParams);
        String limitedSQL = GetPaginatedSQL(0, 50, SQL, SQLOrderBy);

        DataSet ds = new DataSet();
        SqlDataAdapter adapter = new SqlDataAdapter();

        cmd.CommandText = limitedSQL;

        // Add named parameters here to the command if needed...

        adapter.SelectCommand = cmd;
        adapter.Fill(ds);

        // Process the dataset...
    
    conn.Close();

希望对你有帮助。

【讨论】:

以上是关于在 SQL Server 上执行分页的最佳方法是啥?的主要内容,如果未能解决你的问题,请参考以下文章

使用 SQL 查询时,对我的网站进行分页的最有效方法是啥?

在Django REST List API视图中对原始SQL查询进行分页的最佳方法是什么?

SQL server分页的四种方法(算很全面了)

使用 SPQR 进行分页的推荐方法是啥?

在 SQL Server 中创建和管理全局 Procs 和 UDF 的最佳方法是啥?

Spring3+ibatis (SQL Server)+pager-taglib.tld查询分页的实现