개요 : 페이징 쿼리 구현 


테스트 환경 구성 

sales.salesOrderdetail 테이블 사용

exec sp_helpindex 'sales.salesOrderdetail'
--pk : SalesOrderID, SalesOrderDetailID

 

인덱스 

orders_x01 : productid, modifieddate, salesorderid,salesorderdetailid

use workload 
go
if object_id('orders') is not null
   drop table orders 
go


select *
into orders
from AdventureWorks2022.sales.salesOrderdetail
go

alter table orders add constraint orders_pk primary key nonclustered (salesorderid, salesorderdetailid)
go
create nonclustered index orders_x01 on orders(productid, modifieddate, salesorderid,salesorderdetailid)
go

--2376
select count(*)
from orders 
where  productid = 922

 

실제 실행계획 출력 설정

SET STATISTICS PROFILE ON
SET STATISTICS IO ON
SET STATISTICS TIME ON

앞쪽 페이지를 주로 조회할 때 

  • 업무 : 한 주문제품에 대한 고객의 주문 목록을 10건 이내로 조회하는 쿼리 
select top 11 *
from orders 
where  productid = 922 
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

 

[실행계획]

select top 11 *  from orders   where  productid = 922   order by ModifiedDate, SalesOrderID, SalesOrderDetailID
  |--Top(TOP EXPRESSION:((11)))
       |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1003]) WITH ORDERED PREFETCH)
            |--Index Seek(OBJECT:([workload].[dbo].[orders].[orders_x01]), SEEK:([workload].[dbo].[orders].[ProductID]=(922)) ORDERED FORWARD)
            |--RID Lookup(OBJECT:([workload].[dbo].[orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)
            
 테이블 'orders'. 스캔 수 1, 논리적 읽기 15, 실제 읽기 3

두 번째 페이지도 출력하려면 21(=2*10+1)건을 추출하고서 맨 마지막에 11건을 잘라내면 된다. 

select top 11 *
from ( select top 21 *
              ,row_number() over(order by ModifiedDate, SalesOrderID, SalesOrderDetailID) as rnum
        from orders 
	   where productid = 922   
	   order by ModifiedDate, SalesOrderID, SalesOrderDetailID
	   ) a 
where a.rnum >= 11 
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

 

[실행계획]

select top 11 *  from ( select top 21 *                ,row_number() over(order by ModifiedDate, SalesOrderID, SalesOrderDetailID) as rnum          from orders       where productid = 922         order by ModifiedDate, SalesOrderID, SalesOrderDetailID      ) a   where a.rnum >= 11   order by ModifiedDate, SalesOrderID, SalesOrderDetailID
  |--Top(TOP EXPRESSION:((11)))
       |--Filter(WHERE:([Expr1003]>=(11)))
            |--Top(TOP EXPRESSION:((21)))
                 |--Sequence Project(DEFINE:([Expr1003]=row_number))
                      |--Segment
                           |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1005]) WITH ORDERED PREFETCH)
                                |--Index Seek(OBJECT:([workload].[dbo].[orders].[orders_x01]), SEEK:([workload].[dbo].[orders].[ProductID]=(922)) ORDERED FORWARD)
                                |--RID Lookup(OBJECT:([workload].[dbo].[orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)
                                
테이블 'orders'. 스캔 수 1, 논리적 읽기 25, 실제 읽기 3

TOP N 쿼리와 변수를 사용하여 전체 페이지 출력에 사용할 수 있다.

declare @pageno int, @rowsbypage int

set @pageno = 2 -- 페이지 번호
set @rowsbypage = 10 -- 페이지별 rows

select top (@rowsbypage + 1) *
 from ( select top (@pageno * @rowsbypage + 1) *
               ,row_number() over(order by ModifiedDate, SalesOrderID, SalesOrderDetailID) as rnum
		 from orders with(index(orders_x01))
		 where productid = 922
		 order by ModifiedDate, SalesOrderID, SalesOrderDetailID
	) a 
where a.rnum>= (@pageno -1) * @rowsbypage + 1 
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

 

[실행계획]

select top (@rowsbypage + 1) *   from ( select top (@pageno * @rowsbypage + 1) *                 ,row_number() over(order by ModifiedDate, SalesOrderID, SalesOrderDetailID) as rnum     from orders with(index(orders_x01))     where productid = 922     order by ModifiedDate, SalesOrderID, SalesOrderDetailID   ) a   where a.rnum>= (@pageno -1) * @rowsbypage + 1   order by ModifiedDate, SalesOrderID, SalesOrderDetailID
  |--Top(TOP EXPRESSION:(CONVERT_IMPLICIT(bigint,[@rowsbypage]+(1),0)))
       |--Filter(WHERE:([Expr1003]>=CONVERT_IMPLICIT(bigint,([@pageno]-(1))*[@rowsbypage]+(1),0)))
            |--Top(TOP EXPRESSION:(CONVERT_IMPLICIT(bigint,[@pageno]*[@rowsbypage]+(1),0)))
                 |--Sequence Project(DEFINE:([Expr1003]=row_number))
                      |--Segment
                           |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1008]) WITH ORDERED PREFETCH)
                                |--Index Seek(OBJECT:([workload].[dbo].[orders].[orders_x01]), SEEK:([workload].[dbo].[orders].[ProductID]=(922)) ORDERED FORWARD)
                                |--RID Lookup(OBJECT:([workload].[dbo].[orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)
                                
테이블 'orders'. 스캔 수 1, 논리적 읽기 25, 실제 읽기 3

앞쪽 페이지 뿐만 아니라 뒤쪽 페이지도 자주 조회한다면 아래와 같은 쿼리를 사용한다. 바로 앞 페이지에서 가져온 마지막 건을 이용해서 쿼리를 작성한다.(이 것 때문에 미리 1건을 더 가져온다.)

 

첫 번째 페이지 쿼리

select top 11
       row_number() over(order by ModifiedDate, SalesOrderID, SalesOrderDetailID) as rnum, *
 from Orders
where productid = 922
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

 

두 번째 페이지부터의 쿼리 

/* 앞 페이지의 11번째 데이터 저장 변수 */
declare @modifieddate char(8), @salesorderid int

set @modifieddate = '20140616'
set @salesorderid = 74677

select top 11 
       row_number() over(order by ModifiedDate, SalesOrderID, SalesOrderDetailID) as rnum, *
 from orders 
where productid = 922
   /* 앞 페이지의 마지막 데이터보다 크거나 같아야 한다. */
and ((ModifiedDate = convert(datetime, @modifieddate, 112) and salesorderid >= @salesorderid)
     or ModifiedDate > convert(datetime, @modifieddate, 112))
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

 

 

 

 

첫 번째 페이지용 쿼리와 두 번째 이상 페이지용 쿼리를 하나로 합친 쿼리

/* 앞 페이지의 11번째 데이터 저장 변수 */
declare @modifieddate char(8), @salesorderid int

/* modifieddate 컬럼에는 날짜 값만 들어 있어서 char(8) 형식으로 처리함 */
set @modifieddate = '20140616'
set @salesorderid = 74677

select top 11 *
  from orders 
where productid = 922
   /* 앞 페이지의 마지막 데이터보다 크거나 같아야 한다. */
and ((ModifiedDate = convert(datetime, @modifieddate, 112) and salesorderid >= @salesorderid)
     or ModifiedDate > convert(datetime, @modifieddate, 112))
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

[실행계획]

select top 11 *   from orders   where productid = 922  and ((ModifiedDate = convert(datetime, @modifieddate, 112) and salesorderid >= @salesorderid)       or ModifiedDate > convert(datetime, @modifieddate, 112))  order by ModifiedDate, SalesOrderID, SalesOrderDetailID
  |--Top(TOP EXPRESSION:((11)))
       |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000], [Expr1006]) WITH ORDERED PREFETCH)
            |--Index Seek(OBJECT:([workload].[dbo].[orders].[orders_x01]), SEEK:([workload].[dbo].[orders].[ProductID]=(922)),  WHERE:([workload].[dbo].[orders].[ModifiedDate]=CONVERT(datetime,[@modifieddate],112) AND [workload].[dbo].[orders].[SalesOrderID]>=[@salesorderid] OR [workload].[dbo].[orders].[ModifiedDate]>CONVERT(datetime,[@modifieddate],112)) ORDERED FORWARD)
            |--RID Lookup(OBJECT:([workload].[dbo].[orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)
            
테이블 'orders'. 스캔 수 1, 논리적 읽기 24, 실제 읽기 4

 

INDEX SEEK 조건으로 ProductID만 사용되었음 = 인덱스 엑세스 조건으로 ProductID만 사용되어 인덱스 엑세스 범위를 줄이지 못함

 


인덱스를 최적으로 엑세스 하기(인덱스 엑세스 범위를 줄이기) 위해 쿼리 변경

/* 앞 페이지의 11번째 데이터 저장용 변수 */
declare @modifieddate char(8), @salesorderid int

/* modifieddate 컬럼은 날짜 값만 관리하므로 char(8) 형식으로 받아옴 */
set @modifieddate = '20140616'
set @salesorderid = 74677

select top 11 *
 from (
       select * 
	   from (
	        select top 11 * 
			from orders 
            where productid = 922
            /* 앞 페이지의 마지막 데이터와 같음 */
			 and ModifiedDate = convert(datetime, @modifieddate, 112) 
			 and salesorderid >= @salesorderid
			 order by ModifiedDate, SalesOrderID, SalesOrderDetailID
			 ) a 
	   union all 
	   select *
	    from (
	        select top 11 * 
			from orders 
            where productid = 922
            /* 앞페이지의 마지막 데이터보다 큼 */
            and ModifiedDate > convert(datetime, @modifieddate, 112)
			order by ModifiedDate, SalesOrderID, SalesOrderDetailID
			) a 
        ) a 
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go

 

[실행계획]

select top 11 *   from (         select *       from (           select top 11 *       from orders               where productid = 922      and ModifiedDate = convert(datetime, @modifieddate, 112)       and salesorderid >= @salesorderid      order by ModifiedDate, SalesOrderID, SalesOrderDetailID      ) a       union all       select *       from (          select top 11 *       from orders               where productid = 922                and ModifiedDate > convert(datetime, @modifieddate, 112)     order by ModifiedDate, SalesOrderID, SalesOrderDetailID     ) a           ) a   order by ModifiedDate, SalesOrderID, SalesOrderDetailID
  |--Top(TOP EXPRESSION:((11)))
       |--Merge Join(Concatenation)
            |--Top(TOP EXPRESSION:((11)))
            |    |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000]))
            |         |--Index Seek(OBJECT:([workload].[dbo].[orders].[orders_x01]), SEEK:([workload].[dbo].[orders].[ProductID]=(922) AND [workload].[dbo].[orders].[ModifiedDate]=CONVERT(datetime,[@modifieddate],112) AND [workload].[dbo].[orders].[SalesOrderID] >= [@salesorderid]) ORDERED FORWARD)
            |         |--RID Lookup(OBJECT:([workload].[dbo].[orders]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD)
            |--Top(TOP EXPRESSION:((11)))
                 |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1003], [Expr1020]) WITH ORDERED PREFETCH)
                      |--Index Seek(OBJECT:([workload].[dbo].[orders].[orders_x01]), SEEK:([workload].[dbo].[orders].[ProductID]=(922) AND [workload].[dbo].[orders].[ModifiedDate] > CONVERT(datetime,[@modifieddate],112)) ORDERED FORWARD)
                      |--RID Lookup(OBJECT:([workload].[dbo].[orders]), SEEK:([Bmk1003]=[Bmk1003]) LOOKUP ORDERED FORWARD)
                      
테이블 'orders'. 스캔 수 2, 논리적 읽기 17, 실제 읽기 4

 

Index Seek 오퍼레이션의 SEEK:() 부분에 모두 페이지 처리용 조건이 나타남, ModifiedDate, salesorderid가 인덱스 엑세스 조건으로 사용되어 성능이 개선되었다.

 


앞쪽 페이지로 거슬러 올라가는 쿼리

/* 뒤 페이지의 첫 번째 데이터 저장용 변수 */
declare @modifieddate char(8), @salesorderid int

/* modifieddate 컬럼은 날짜 값만 관리하므로 char(8) 형식으로 받아옴 */
set @modifieddate = '20140616'
set @salesorderid = 74677

select *
from (
select top 11 *
 from (
       select * 
	   from (
	        select top 11 * 
			from orders 
            where productid = 922
            /* 뒤 페이지의 첫 번째 데이터와 같음 */
			 and ModifiedDate = convert(datetime, @modifieddate, 112) 
			 and salesorderid <= @salesorderid
			 order by ModifiedDate desc, SalesOrderID desc, SalesOrderDetailID desc
			 ) a 
	   union all 
	   select *
	    from (
	        select top 11 * 
			from orders 
            where productid = 922
            /* 뒤 페이지의 첫 번째 데이터보다 작음 */
            and ModifiedDate < convert(datetime, @modifieddate, 112)
			order by ModifiedDate desc, SalesOrderID desc, SalesOrderDetailID desc
			) a 
        ) a 
order by ModifiedDate desc, SalesOrderID desc, SalesOrderDetailID desc
) a
order by ModifiedDate, SalesOrderID, SalesOrderDetailID
go
go

+ Recent posts