SQL Server 中的三种分页方式
USE tempdb GO SET NOCOUNT ON --创建表结构 IF OBJECT_ID(N'ClassB', N'U') IS NOT NULL DROP TABLE ClassB GO CREATE TABLE ClassB(ID INT PRIMARY KEY, Name VARCHAR(16), CreateDate DATETIME, AID INT, Status INT) CREATE INDEX IDX_CreateDate ON ClassB(CreateDate) CREATE INDEX IDX_AID ON ClassB(AID) GO --插入测试数据 DECLARE @ID INT SET @ID = 1 WHILE @ID <= 100000 BEGIN INSERT INTO ClassB VALUES(@ID, 'fx', GETDATE(), @ID % 20, @ID % 20) SET @ID = @ID + 1 END --统计总行数 SELECT 'ClassB' AS ClassB, count(1) AS Count FROM ClassB
查询条件如下:根据CreateDate倒序排序,CreateDate一致时按照ID倒序;时间区间在2014-07-13和2014-07-14之间;每页20条,当前页数第3页;
方案A:双Top,取出之前页的所有ID后,使用NOT IN排除这些ID进行查询,这是性能最差一种,因为他使用相同的查询谓词,查询了两次数据,且两次排序;
DECLARE @page_size INT = 20; DECLARE @page_index INT = 3; --A: 双Top:取出之前页的所有ID后,使用NOT IN排除这些ID进行查询,性能最差 WITH ID_List_Excluded AS ( SELECT TOP (@page_size * (@page_index - 1)) ID FROM ClassB WHERE CreateDate BETWEEN '2014-07-13' AND '2014-07-14' ORDER BY CreateDate DESC, ID DESC ) SELECT TOP (@page_size) * FROM ClassB WHERE ID NOT IN (SELECT ID FROM ID_List_Excluded) AND CreateDate BETWEEN '2014-07-13' AND '2014-07-14' ORDER BY CreateDate DESC, ID DESC;
表 'ClassB'。扫描计数 2,逻辑读取 136 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'Worktable'。扫描计数 1,逻辑读取 197 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
查询页数越靠后逻辑读取,下面是查询第30页的逻辑读取情况:
表 'ClassB'。扫描计数 2,逻辑读取 1243 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
表 'Worktable'。扫描计数 1,逻辑读取 2574 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
方案B:ROW_NUMBER,取出截止到当前页所有符合查询谓词的记录,并给每条记录加上序号,然后根据序号查询,性能次之;
DECLARE @page_size INT = 20; DECLARE @page_index INT = 3; --B: ROW_NUMBER:取出截至到当前页所有符合查询谓词的记录,并给每条记录加上序号,然后根据序号查询,性能次之 WITH NewClassB AS ( SELECT TOP (@page_size * @page_index) ROW_NUMBER() OVER (ORDER BY CreateDate DESC, ID DESC) AS ROWID, * FROM ClassB WHERE CreateDate BETWEEN '2014-07-13' AND '2014-07-14' ) SELECT TOP (@page_size) * FROM NewClassB WHERE ROWID BETWEEN (@page_size * (@page_index - 1) + 1) AND (@page_size * @page_index);
表 'ClassB'。扫描计数 1,逻辑读取 134 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
方案B已经比方案A快了近一倍,少了一个Worktable,同样,page_index越大,扫描次数越多,因为子查询中要查询多余的(之前页)数据和字段,下面是查询第30页时的IO读取情况;
表 'ClassB'。扫描计数 1,逻辑读取 1241 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
方案C:双Top,取出截止到当前页为止的所有的ID,去掉之前页的ID,然后根据ID(通常是主键)作常规查询,性能最优;
DECLARE @page_size INT = 20; DECLARE @page_index INT = 3; WITH ID_List AS ( SELECT TOP (@page_size * @page_index) ID FROM ClassB WHERE CreateDate BETWEEN '2014-07-13' AND '2014-07-14' ORDER BY CreateDate DESC, ID DESC ) ,ID_List_Current AS ( SELECT ID FROM ID_List WHERE ID NOT IN(SELECT TOP (@page_size * (@page_index - 1)) ID FROM ID_List) ) SELECT * FROM ClassB WHERE ID IN (SELECT ID FROM ID_List_Current);
表 'ClassB'。扫描计数 2,逻辑读取 52 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。
查询第30页的数据仅比查询第3页的数据多出两次逻辑读取,方案C和B的唯一在于:子查询中的查询列,方案B中返回了所有字段,所有要通过聚集索引走lookup,而方案C直接返回索引键即可。
表 'ClassB'。扫描计数 2,逻辑读取 54 次,物理读取 0 次,预读 0 次,lob 逻辑读取 0 次,lob 物理读取 0 次,lob 预读 0 次。