본문 바로가기

use-the-index-luke

7.2 Fetching The Next Page

use-the-index-luke 저자 Markus Winand

---------------------------------------------

Markus Winand는 SQL에 대한 통찰력을 제공하고 다양한 시스템이 SQL을 지원하는 방법을 modern-sql.com 에서 보여줍니다. 이전에 그는 use-the-index-luke.com 을 만들었는데, 지금도 활발하게 유지되고 있습니다. Markus는 winand.at 를 통해 강사, 연사 및 컨설턴트로 고용될 수 있습니다.

---------------------------------------------

You can upload a Korean translation of use-the-index-luke.com on your blog

Thank you from the bottom of my heart to author Makus Winand for allowing me.

 

These are translations that I use for studying by using a papago (google translate)

The translations may not be correct or there may be a typo.

I'd appreciate it if you could point it out in the comments.

---------------------------------------------

---------------------------------------------

use-the-index-luke.com 의 한글번역본을 블로그에 업로드 해도 된다고

허락해주신 Makus Winand 저자님께 진심으로 감사합니다.

 

이 번역본들은 제가 공부용도로 번역기(papago, google transrate)를 돌려서 

번역한 내용들이라 맞지 않거나, 오타가 있을수 있습니다.

댓글로 지적해주시면 감사하겠습니다.

---------------------------------------------

 

Paging Through Results

https://use-the-index-luke.com/sql/partial-results/fetch-next-page

페이지를 효율적으로 검색하기 위해 파이프라인 top-N 쿼리를 구현한 다음 페이지를 가져오려면 다른 쿼리가 필요한 경우가 많습니다. 결과 이전 페이지의 행을 건너뛸 있습니다. 문제를 해결하는 데는 가지 다른 방법이 있습니다. 첫번째 오프셋 방법은 처음부터 번호를 지정하고 번호의 필터를 사용하여 요청한 페이지 앞에 있는 행을 삭제하는 방법입니다. 번째 방법을 검색 방법이라고 하는데, 이전 페이지의 마지막 항목을 검색하고 다음 행만 가져옵니다.

 

다음 예제에서는 널리 사용되는 오프셋 방법을 보여줍니다. 특이 전용 키워드(오프셋) 있는 데이터베이스에서는 매우 쉽게 처리할 있다는 것이 주요 장점입니다.

키워드는 fetch first 확장의 일부로 SQL 표준에도 포함되었습니다.

 

----------------------------------------------------------

Oracle

Oracle 데이터베이스는 릴리스 12c 이후 오프셋을 지원합니다. 이전 릴리스에서는 결과 집합의 번호를 자동으로 매기는 유사 ROWNUM 제공합니다. 그러나 유사열에는 (>=)보다 크거나 같은 필터를 적용할 없습니다.

작업을 수행하려면 먼저 이름을 별칭으로 변경하여 번호를 "구체화"해야 합니다.

 

SELECT *
  FROM ( SELECT tmp.*, rownum rn
           FROM ( SELECT *
                    FROM sales
                   ORDER BY sale_date DESC
                ) tmp
          WHERE rownum <= 20
       )
 WHERE rn > 10

 

하한에는 별칭 RN 사용하고 상한에는 ROWNUM 유사 자체를 사용합니다(Tom Kyte 덕분에).

----------------------------------------------------------

단순성 외에도 방법의 다른 장점은 임의 페이지를 가져오기 위해 row offset 있으면 된다는 것입니다. 그러나 데이터베이스는 처음부터 요청된 페이지에 도달할 때까지 모든 행을 계산해야 합니다.

그림 7.2 많은 페이지를 가져올때 스캔한 인덱스 범위가 커진다는 것을 보여줍니다.

Figure 7.2 Access Using the Offset Method

----------------------------------------------------------

----------------------------------------------------------

이것은 가지 단점을 가지고 있습니다.

(1) 번호 매기기가 항상 처음부터 수행되기 때문에 신규 판매를 삽입할 페이지가 이동합니다. (2) 뒤로 탐색할 응답시간이 증가합니다.

 

검색 방법은 이전 페이지의 값을 구분 기호로 사용하므로 가지 문제를 모두 방지합니다. , 이전 페이지의 마지막 항목뒤에 와야 하는 값을 검색합니다. 이는 단순한 where 절로 표현할 있습니다. 반대로 말하면, 검색 방법은 단순히 이미 표시된 값을 선택하지 않습니다.

 

다음 예에서는 검색방법을 보여 줍니다.

시연을 위해, 우리는 하루에 번만 판매한다는 가정하에 시작할 것입니다. 이렇게 하면 SALE_DATE 고유한 키가 됩니다. 특정 날짜 보다 뒤에 와야 하는 판매를 선택하려면 내립차순 정렬 순서로 인해 보다 작은 조건(<) 사용해야 합니다. 오름차순의 경우 보다 (>) 조건을 사용해야 합니다. fetch first 절은 결과를 10 행으로 제한하는 사용됩니다.

----------------------------------------------------------

SELECT *
  FROM sales
 
WHERE sale_date < ?
 ORDER BY sale_date DESC
 FETCH FIRST 10 ROWS ONLY

----------------------------------------------------------

번호 대신 이전 페이지의 마지막 값을 사용하여 하한을 지정합니다. 데이터베이스가 Index access SALE_DATE < ? 조건을 사용할 있기 때문에 성능 측면에서 이점이 있습니다. , 데이터베이스가 이전 페이지의 행을 건너뛸 있습니다. 또한 행을 삽입하면 안정적인 결과를 얻을 있습니다.

 

그럼에도 불구하고 방법은 그림 7.2 같이 하루에 이상의 판매가 있는 경우에는 작동하지 않습니다.

페이지의 마지막 날짜("어제") 사용하면 페이지에 이미 표시된 날짜뿐만 아니라 어제의 모든 결과가 건너뛰기 때문입니다.

문제는 order by 순서가 결정론적 순서를 설정하지 않는다는 것입니다. 그러나 이는 페이지 구분에 단순 범위 조건을 사용하기 위한 전제 조건입니다.

 

결정론적 order by 순서가 없으면 데이터베이스는 정의상  결정론적 순서를 전달하지 않습니다.

일반적으로 일관된 순서가 나타나는 유일한 이유는 데이터베이스가 일반적으로 동일한 방법으로 쿼리를 실행하기 때문입니다.

그럼에도 불구하고 데이터베이스는 실제로 동일한 SALE_DATE 가진 행을 섞을 있으며 여전히 order by 조건을 충족할 있습니다.

최근 릴리스에서는 데이터베이스가 결과를 의도적으로 석기 때문이 아니라 데이터베이스가 병렬 쿼리 실행을 사용할 있기  때문에 쿼리를 실행할 때마다 결과가 다른 순서로 나타날 있습니다.

, 실행 스레드가 비결정론적 순서로 끝나기 때문에 동일한 실행 계획이 다른 시퀀스를 생성할 있습니다.

----------------------------------------------------------

Important

페이징에는 결정론적 정렬 순서가 필요합니다.

----------------------------------------------------------

기능 사양이 "날짜별, 최신 우선" 정렬만 요구하더라도 개발자로서 우리는 order by 모델이 결정론적인 시퀀스를 생성하는지 확인해야 합니다. 이를 위해 결정론적 시퀀스를 얻기 위해 임의 열로 order by 순서를 확장해야 있습니다. 파이프라인 order by 사용되는 인덱스에 열이 추가되면 파이프라인 order by 대해 인덱스를 계속 사용할 있도록 order by 테이블에 추가하는 것이 좋습니다. 그래도 결정론적 정렬 순서가 나타나지 않으면 고유한 열을 추가하고 그에 따라 인덱스를 확장하십시오.

 

다음 예제에서는 결정론적 시퀀스를 얻기 위해 기본 SALE_ID 사용하여 order by 키와 인덱스를 확장합니다. 또한 원하는 결과를 얻으려면 모두에 "다음 이후에 온다" 논리를 적용해야 합니다:

----------------------------------------------------------

CREATE INDEX sl_dtid ON sales (sale_date, sale_id)

----------------------------------------------------------

 

----------------------------------------------------------

SELECT *
  FROM sales
 WHERE (sale_date, sale_id) < (?, ?)
 ORDER BY sale_date DESC,
sale_id DESC
 FETCH FIRST 10 ROWS ONLY

----------------------------------------------------------

 

where 절은 알려지지 않은 "Row Values" 구문을 사용합니다("SQL Row Values" 상자 참조).

여러 값을 정규 비교 연산자에 적용할 있는 논리 단위로 결합합니다. 스칼라 값과 마찬가지로, 내림차순으로 정렬할 보다 작은 조건은 "다음에 옵니다" 해당합니다. , 쿼리는 지정된 SALE_DATE, SALE_ID 이후에 발생하는 판매만 고려합니다.

----------------------------------------------------------

 

----------------------------------------------------------

SQL Row Values

 정규 스칼라 외에도 SQL 표준은 소위 생성자를 정의합니다. 또는 부분행으로 구성할 순서가 지정된 집합 지정

[SQL:92, §7.1: <row value constructor>]

구문적으로 값은 대괄호 안에 있는 목록 입니다.

 

구문은 삽입 문에서 사용되는 것으로 가장 알려져 있습니다.

 

그러나 where 절에서 생성자를 사용하는 것은 알려져 있지만 여전히 완벽하게 유효합니다. SQL 표준은 실제로 생성자에 대한 모든 비교 연산자를 정의합니다. less thin 연산의 정의는 예를 들어 다음과 같습니다:

 

"Rx < Ry"는 모든 i< n 대해 RXi=RYi 이고 일부 n 대해 RXn < RYn 경우에만 참입니다.

 

— SQL:92, §8.2.7.2

 

여기서 I in 목록의 위치 인덱스를 반영합니다.

, RXn 값이 해당 RYn보다 작고 모든 이전 쌍이 동일한 경우 RX RY보다 작다는 것을 의미합니다.

(RXi = RYi; for i<n)

 

정의는 식을 RX < RY 만듭니다

"RX sort before RY" 동의어로, 이는 정확히 검색 방법에 필요한 논리입니다.

----------------------------------------------------------

구문은 SQL 표준의 일부이지만 일부 데이터베이스만 이를 지원합니다. SQL Server 2017에서는 값을 전혀 지원하지 않습니다. Oracle 데이터베이스는 원칙적으로 값을 지원하지만 범위 연산자를 적용할 없습니다.(ORA-01796)

MySQL 표현식을 올바르게 평가하지만 Index access 중에는 access predicate 사용할 없습니다.

그러나 DB2(LUW, 10.1 이후) PostgreSQL(8.4 이후)

row value predicate 적절하게 지원하며 사용가능한 인데긋가 있는 경우 이를 사용하여 인덱스에 access 합니다.

 

그럼에도 불구하고 Postgre SQL 값만큼 우아하고 효율적이지는 않지만 값을 제대로 지원하지 않는 데이터베이스에는 탐색 방법의 대략적인 변형을 사용할 있습니다.  근사치를 위해 다음 Oracle 예제와 같이 "정규" 비교를 사용하여 필요한 논리를 표현해야 합니다:

----------------------------------------------------------

SELECT *
  FROM ( SELECT *
           FROM sales
          WHERE
sale_date <= ?
            AND NOT (sale_date = ? AND sale_id >= ?)

          ORDER BY sale_date DESC, sale_id DESC
       )
 WHERE rownum <= 10

----------------------------------------------------------

where 절은 부분으로 구성됩니다. 첫번째 파트에서는 SQLE_DATE 고려하고 (<=)보다 작서나 같은 조건을 사용합니다. 필요에 따라 많은 행을 선택합니다.

where 절의 부분은 모든 데이터베이스가 Index 접근하는 사용할 있을 정도로 간단합니다. where 절의 번째 부분은 이전 페잊에 이미 표시된 초과 행을 제거합니다. "Indexxing Equivalent Logic" 상자는 where 절이 이렇게 표현되는 이유를 설명합니다.

----------------------------------------------------------

Indexing Equivalent Logic

 

논리적 조건은 항상 다른 방식으로 표현될 있습니다.

예를 들어 위에 표시된 건너 뛰기 논리를 다음과 같이 구현할 수도 있습니다:

----------------------------------------

WHERE (
         (sale_date < ?)
       OR
         (sale_date = ? AND sale_id < ?)
      )

----------------------------------------

변형은 조건을 포함하여 사용할 뿐이며 적어도 인간에게는 이해하기가 쉬울 것입니다. 데이터베이스는 다른 관점을 가집니다.

이들은 SALE_DATE 분기에 대해 동일한 경우 where 절이 해당 SALE_DATE/SALE_ID 쌍으로 시작하는 모든 행을 선택한다는 것을 인식하지 못합니다. 대신 데이터베이스는 전체 where 절을 firter predicate 사용합니다. 적어도 옵티마이저가 또는 여러 개의 데이터베이스 "SALE_DATE <= ? 조건을 제거" 것으로 예상할 있지만, 이서비스를 제공하는 데이터베이스는 없습니다.

 

그럼에도 불구하고 가독성을 높이지 않더라도 수동으로 중복 조건을 추가할 있습니다:

 

----------------------------------------

WHERE sale_date <= ?
  AND
(
         (sale_date < ?)
       OR
         (sale_date = ? AND sale_id < ?)
      )

----------------------------------------

다행히 모든 데이터베이스는 where 절의 부분을 access prdicate 사용할 있습니다. 그러나 조항은 위에서 보여준 근사 논리로 이해하기가 횔씬 어렵습니다. 또한 원래의 논리는 나중에 "불필요한" (중복) 부분이 where 절에서 실수로 제거될 위험을 피합니다.

----------------------------------------------------------

 

실행 계획은 데이터베이스가 where절의 번째 부분을 access predicate 사용한다는 것을 보여줍니다.

----------------------------------------------------------

---------------------------------------------------------------
|Id | Operation                      | Name    |  Rows | Cost |
---------------------------------------------------------------
| 0 | SELECT STATEMENT               |         |    10 |    4 |
|*1 |  COUNT STOPKEY                 |         |       |      |
| 2 |   VIEW                         |         |    10 |    4 |
| 3 |    TABLE ACCESS BY INDEX ROWID | SALES   | 50218 |    4 |
|*4 |     INDEX RANGE SCAN DESCENDING| SL_DTIT |     2 |    3 |
---------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------
   1 - filter(ROWNUM<=10)
  
4 - access("SALE_DATE"<=:SALE_DATE)
      
filter("SALE_DATE"<>:SALE_DATE
           OR "SALE_ID"<TO_NUMBER(:SALE_ID))

----------------------------------------------------------

SALE_DATE access predicate 사용하면 데이터베이스가 이전 페이지에 완전히 표시된 날짜를 건너뛸 있습니다. where절의 번째 부분은 필터 서술어입니다.

, 데이터베이스는 이전 페이지의 일부 항목을 다시 검사하지만 즉시 삭제합니다.

그림 7.3 각각의 접근 경로를 보여줍니다.

 

Figure 7.3 Access Using the Seek Method

----------------------------------------------------------

----------------------------------------------------------

 

그림 7.4 오프셋과 탐색 방법의 성능 특성을 비교합니다. 측정의 정확도는 차트의 왼쪽에 있는 차이를 확인하기에는 충분하지 않지만 20페이지 이후부터는 차이를 명확하게 있습니다.

Figure 7.4 Scalability when Fetching the Next Page

----------------------------------------------------------

----------------------------------------------------------

물론 탐색 방법에도 단점이 있는데, 가장 중요한 것은 그것을 다루는 어려움이 있다는 것입니다.

where 절을 매우 주의 깊게 구해야 뿐만 아니라 임의 페이지를 가져올 수도 없습니다. 또한 검색 방향을 변경하려면 모든 비교 정렬 작업을 되돌려야합니다.

사용자 인터페이스에 무한 스크롤 메커니즘을 사용할 때는 정확히 페이지 건너뛰기와 뒤로 탐색이라는 두가지 기능이 필요하지 않습니다.

 

 

Figure 7.5 Database/Feature Matrix

----------------------------------------------------------

----------------------------------------------------------

 

 

 

 

----------------------------------------------------------

'use-the-index-luke' 카테고리의 다른 글

8장 Insert, Delete and Update  (0) 2023.11.10
7.3 Window-Functions  (0) 2023.11.10
7.1 Selecting Top-N Rows  (0) 2023.11.03
7장 Partial Results  (1) 2023.10.31
6.3 Index-Oraganized Table  (1) 2023.10.31