【问题标题】:How to return a page of results from SQL?如何从 SQL 返回一页结果?
【发布时间】:2010-09-05 19:52:57
【问题描述】:

许多应用程序都有网格,一次显示一页数据库表中的数据。其中许多还允许用户选择每页的记录数,按任何列排序,并在结果中来回导航。

什么是实现这种模式的好算法,而不是将整个表带到客户端,然后在客户端过滤数据。如何只将要显示的记录提供给用户?

LINQ 是否简化了解决方案?

【问题讨论】:

标签: .net sql linq pagination


【解决方案1】:

在 MS SQL Server 2005 及更高版本上,ROW_NUMBER() 似乎可以工作:

T-SQL: Paging with ROW_NUMBER()

DECLARE @PageNum AS INT;
DECLARE @PageSize AS INT;
SET @PageNum = 2;
SET @PageSize = 10;

WITH OrdersRN AS
(
    SELECT ROW_NUMBER() OVER(ORDER BY OrderDate, OrderID) AS RowNum
          ,OrderID
          ,OrderDate
          ,CustomerID
          ,EmployeeID
      FROM dbo.Orders
)

SELECT * 
  FROM OrdersRN
 WHERE RowNum BETWEEN (@PageNum - 1) * @PageSize + 1 
                  AND @PageNum * @PageSize
 ORDER BY OrderDate
         ,OrderID;

【讨论】:

  • ROW_NUMBER() 分页仿真或 SQL Server 2012 的 OFFSET .. FETCH 子句对于高页码可能会非常慢:4guysfromrolla.com/webtech/042606-1.shtml。在这种情况下,seek method 可能是更好的选择,因为它允许在恒定时间内进行分页。
【解决方案2】:

我建议要么使用 LINQ,要么尝试复制它的功能。我有一个应用程序,我在其中使用 LINQ Take 和 Skip 方法来检索分页数据。代码如下所示:

MyDataContext db = new MyDataContext();
var results = db.Products
    .Skip((pageNumber - 1) * pageSize)
    .Take(pageSize);

运行 SQL Server Profiler 显示 LINQ 正在将此查询转换为类似于以下内容的 SQL:

SELECT [ProductId], [Name], [Cost], and so on...
FROM (
    SELECT [ProductId], [Name], [Cost], [ROW_NUMBER]
    FROM (
       SELECT ROW_NUMBER() OVER (ORDER BY [Name]) AS [ROW_NUMBER], 
           [ProductId], [Name], [Cost]
       FROM [Products]
    )
    WHERE [ROW_NUMBER] BETWEEN 10 AND 20
)
ORDER BY [ROW_NUMBER]

简单的英语:
1. 过滤您的行并使用 ROW_NUMBER 函数按您想要的顺序添加行号。
2. 过滤器 (1) 仅返回页面上所需的行号。
3. 按行号对 (2) 进行排序,这与您想要的顺序相同(在本例中为按名称)。

【讨论】:

  • 你知道为什么 LINQ 会双重嵌套实际的 SELECT 语句吗? (是吗?)对于某些 SQL Server 版本来说,这是一个微妙的性能调整吗?我觉得2级可以和1级合并。
【解决方案3】:

在数据库中进行分页基本上有两种方法(我假设您使用的是 SQL Server):

使用偏移量

其他人已经解释了如何使用ROW_NUMBER() OVER() 排名功能来执行页面。值得一提的是,SQL Server 2012 终于包含了对 SQL 标准 OFFSET .. FETCH 子句的支持:

SELECT first_name, last_name, score
FROM players
ORDER BY score DESC
OFFSET 40 ROWS FETCH NEXT 10 ROWS ONLY

如果您使用的是 SQL Server 2012 并且向后兼容性不是问题,您可能应该更喜欢这个子句,因为在极端情况下 SQL Server 会更优化地执行它。

使用 SEEK 方法

有一种完全不同的、速度更快但鲜为人知的方式在 SQL 中执行分页。这通常被称为this blog post here 中描述的“seek 方法”。

SELECT TOP 10 first_name, last_name, score
FROM players
WHERE (score < @previousScore)
   OR (score = @previousScore AND player_id < @previousPlayerId)
ORDER BY score DESC, player_id DESC

@previousScore@previousPlayerId 值分别是上一页最后一条记录的值。这允许您获取“下一页”。如果ORDER BY 方向是ASC,只需使用&gt; 代替。

使用上述方法,您不能在没有先获取前 40 条记录的情况下立即跳转到第 4 页。但通常,无论如何你都不想跳那么远。相反,您将获得更快的查询,该查询可能能够在恒定时间内获取数据,具体取决于您的索引。此外,无论基础数据是否发生变化(例如,在第 1 页上,当您在第 4 页上时),您的网页都会保持“稳定”。

这是在 web 应用程序中延迟加载更多数据时实现分页的最佳方式。

注意,“seek 方法”也称为keyset paging

【讨论】:

    【解决方案4】:

    LINQ 结合 .Net 3.5 中的 lambda 表达式和匿名类极大地简化了这类事情。

    查询数据库:

    var customers = from c in db.customers
                    join p in db.purchases on c.CustomerID equals p.CustomerID
                    where p.purchases > 5
                    select c;
    

    每页记录数:

    customers = customers.Skip(pageNum * pageSize).Take(pageSize);
    

    按任意列排序:

    customers = customers.OrderBy(c => c.LastName);
    

    仅从服务器获取选定的字段:

    var customers = from c in db.customers
                    join p in db.purchases on c.CustomerID equals p.CustomerID
                    where p.purchases > 5
                    select new
                    {
                        CustomerID = c.CustomerID,
                        FirstName = c.FirstName,
                        LastName = c.LastName
                    };
    

    这将创建一个静态类型的匿名类,您可以在其中访问其属性:

    var firstCustomer = customer.First();
    int id = firstCustomer.CustomerID;
    

    默认情况下,查询的结果是延迟加载的,因此在您真正需要数据之前,您不会与数据库对话。 .Net 中的 LINQ 还通过保留您所做更改的数据上下文并仅更新您更改的字段来极大地简化更新。

    【讨论】:

    • 第一个语句似乎从数据库中拉回所有内容,然后第二个语句获取一个子集。如果您一开始只想要一个子集怎么办?我有 90,000 行,只想要 10 行中的第 4 页。
    • @ScSub LINQ 表达式是延迟加载的,因此第一次调用一开始实际上并没有做任何事情。你可以打电话给customers = customers.Skip(30).Take(10),它只会拉回你想要的。
    【解决方案5】:

    甲骨文解决方案:

    select * from (
        select a.*, rownum rnum from (
            YOUR_QUERY_GOES_HERE -- including the order by
        ) a
        where rownum <= MAX_ROW
     ) where rnum >= MIN_ROW
    

    【讨论】:

      【解决方案6】:

      我在 MS SQL 2005 中使用了一些解决方案。

      其中一个是 ROW_NUMBER()。但是,就个人而言,我不喜欢 ROW_NUMBER() 因为它不适用于大结果(我工作的数据库非常大 - 超过 1TB 的数据在一秒钟内运行数千个查询 - 你知道 - 大型社交网络网站)。

      这是我最喜欢的解决方案。

      我将使用一种 T-SQL 的伪代码。

      让我们找到按姓、名排序的第 2 页用户,其中每页有 10 条记录。

      @page = 2 -- input parameter
      @size = 10 -- can be optional input parameter
      
      if @page < 1 then begin
          @page = 1 -- check page number
      end
      @start = (@page-1) * @size + 1 -- @page starts at record no @start
      
      -- find the beginning of page @page
      SELECT TOP (@start)
          @forename = forename,
          @surname = surname
          @id = id
      FROM
          users
      ORDER BY
          forename,
          surname,
          id -- to keep correct order in case of have two John Smith.
      
      -- select @size records starting from @start
      SELECT TOP (@size)
          id,
          forename,
          surname
      FROM
          users
      WHERE
          (forename = @forename and surname = @surname and id >= @id) -- the same name and surname, but bigger id
          OR (forename = @forename and surname > @surname) -- the same name, but bigger surname, id doesn't matter
          OR (forename > @forename) -- bigger forename, the rest doesn't matter
      ORDER BY
          forename,
          surname,
          id
      

      【讨论】:

        【解决方案7】:

        实际上,LINQ 有 Skip 和 Take 方法,可以组合起来选择获取哪些记录。

        检查一下。

        对于数据库:Pagination In SQL Server 2005

        【讨论】:

          【解决方案8】:

          有关于这个Here的讨论

          该技术在 78 毫秒内从 150,000 行的数据库中获取页码 100,000

          使用优化器知识和 SET ROWCOUNT,请求的页面中的第一个 EmployeeID 存储在一个局部变量中作为起点。接下来,将 ROWCOUNT 设置为 @maximumRows 中请求的最大记录数。这允许以更有效的方式对结果集进行分页。使用此方法还可以利用表上预先存在的索引,因为它直接进入基表而不是本地创建的表。

          恐怕我无法判断它是否比当前接受的答案更好。

          【讨论】:

          • 另一种非常快速的方法是seek method,它允许在恒定时间内分页(甚至可能快于 78 毫秒)
          猜你喜欢
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 2010-11-15
          • 2021-02-23
          • 2023-02-06
          • 2017-01-03
          相关资源
          最近更新 更多