실제 MySQL 8.4.10 실행 계획과 131,072행 fixture로 복합 인덱스 column order, range boundary, ICP, covering lookup을 단계별로 확인합니다.
Misordered
- Selected key
idx_orders_status_date_customer
- Access type
range
- used_key_parts
status, ordered_at
SQL과 인덱스 정의를 함께 읽습니다
customer_id=412, status='PAID', ordered_at 하한, 최신순 정렬, LIMIT 20이 하나의 access path에서 어떤 역할을 갖는지 확인합니다.
MySQL이 선택한 range access를 확인합니다
선택된 key, access type, key length, estimated rows, filtered, cost를 같은 native range-scan operator에서 읽습니다.
status만 leftmost equality prefix가 됩니다
인덱스 첫 column인 status='PAID'는 equality prefix를 만들지만 customer_id=412는 ordered_at range 뒤에 있어 prefix를 늘리지 못합니다.
status와 ordered_at이 기본 range를 만듭니다
used_key_parts와 일반 range 대안은 status, ordered_at입니다. 이 두 key part가 전체 날짜 span의 기본 후보 범위를 결정합니다.
customer_id는 boundary suffix와 ICP에서 작동합니다
low-LIMIT 재검토의 chosen tuple에는 customer_id<=412가 suffix로 나타나고, scan 중에는 ICP가 customer_id=412를 평가합니다. id는 결과 순서를 유지합니다.
status/date range에는 31,348행이 들어옵니다
M_RANGE는 status='PAID'와 ordered_at 하한을 만족하는 fixture row 31,348개입니다. 이 집합은 엔진 방문 순서가 아니라 정확한 predicate 결과입니다.
customer_id=412가 31행만 통과시킵니다
31,348개 candidate 중 31개가 customer_id=412이고 31,317개는 ICP predicate에서 제외되는 fixture 관계입니다.
total_cents를 위해 base row가 필요합니다
선택된 secondary index에 total_cents가 없으므로 index entry의 primary key로 clustered row를 찾는 논리 단계를 표시합니다.
인덱스 순서가 ORDER BY를 충족합니다
ordered_at DESC, id DESC 순서가 선택된 index order와 일치해 별도 filesort operator 없이 결과 순서를 만듭니다.
20번째 tuple에서 결과를 끊습니다
order 83356이 마지막 반환 row이고 order 79260이 다음 제외 row입니다. 두 tuple의 ordered_at과 id를 직접 비교합니다.
캡처된 20행을 그대로 반환합니다
세 variant가 같은 20행과 checksum f3b6a116…01a8b를 반환하는지 ID, 시각, 금액까지 대조합니다.
plan, trace, counters, result를 한 번에 대조합니다
used_key_parts와 range 문자열은 plan/trace에서, candidate 관계는 fixture에서, rows·cost·Handler·result는 각 캡처 locator에서 확인합니다.
캡처 결과
MySQL 8.4.10 · 20 rows · f3b6a116a0d644412cd60167a96e4e8d628adbe75106f53d78c3893fef401a8b
Aligned
- Selected key
idx_orders_customer_status_ordered_id
- Access type
range
- used_key_parts
customer_id, status, ordered_at
SQL과 인덱스 정의를 함께 읽습니다
customer_id=412, status='PAID', ordered_at 하한, 최신순 정렬, LIMIT 20이 하나의 access path에서 어떤 역할을 갖는지 확인합니다.
MySQL이 선택한 range access를 확인합니다
선택된 key, access type, key length, estimated rows, filtered, cost를 같은 native range-scan operator에서 읽습니다.
customer_id와 status가 equality prefix를 만듭니다
customer_id=412와 status='PAID'가 인덱스 앞에서 연속 equality 조건으로 고정됩니다.
ordered_at이 31행 range의 첫 경계입니다
used_key_parts는 customer_id, status, ordered_at이며 앞의 두 equality 뒤에서 ordered_at 하한이 첫 range boundary가 됩니다.
id는 정렬 suffix로 남습니다
id DESC는 range를 더 줄이는 predicate가 아니라 ordered_at이 같은 row의 ORDER BY 순서를 결정합니다.
정렬된 prefix가 31행만 range로 만듭니다
customer_id=412, status='PAID', ordered_at 하한을 모두 만족하는 A_RANGE는 정확히 31행이며 ELIGIBLE과 같습니다.
range candidate 31행이 모두 통과합니다
customer_id와 status가 이미 range prefix에 포함되어 별도의 fixture reject row 없이 31행이 결과 후보가 됩니다.
range는 좁아졌지만 total_cents는 base row에 있습니다
이 인덱스에도 total_cents가 없으므로 31개 candidate의 projection에는 clustered-row materialization 모델이 필요합니다.
인덱스 순서가 ORDER BY를 충족합니다
ordered_at DESC, id DESC 순서가 선택된 index order와 일치해 별도 filesort operator 없이 결과 순서를 만듭니다.
20번째 tuple에서 결과를 끊습니다
order 83356이 마지막 반환 row이고 order 79260이 다음 제외 row입니다. 두 tuple의 ordered_at과 id를 직접 비교합니다.
캡처된 20행을 그대로 반환합니다
세 variant가 같은 20행과 checksum f3b6a116…01a8b를 반환하는지 ID, 시각, 금액까지 대조합니다.
plan, trace, counters, result를 한 번에 대조합니다
used_key_parts와 range 문자열은 plan/trace에서, candidate 관계는 fixture에서, rows·cost·Handler·result는 각 캡처 locator에서 확인합니다.
캡처 결과
MySQL 8.4.10 · 20 rows · f3b6a116a0d644412cd60167a96e4e8d628adbe75106f53d78c3893fef401a8b
Covering
- Selected key
idx_orders_customer_status_ordered_id_total
- Access type
range
- used_key_parts
customer_id, status, ordered_at
SQL과 인덱스 정의를 함께 읽습니다
customer_id=412, status='PAID', ordered_at 하한, 최신순 정렬, LIMIT 20이 하나의 access path에서 어떤 역할을 갖는지 확인합니다.
MySQL이 선택한 range access를 확인합니다
선택된 key, access type, key length, estimated rows, filtered, cost를 같은 native range-scan operator에서 읽습니다.
customer_id와 status가 같은 equality prefix를 만듭니다
noncovering variant와 동일하게 customer_id=412와 status='PAID'가 leading equality prefix를 고정합니다.
ordered_at이 같은 31행 range를 만듭니다
covering suffix를 추가해도 used_key_parts와 first range boundary는 customer_id, status, ordered_at으로 동일합니다.
id는 정렬하고 total_cents는 projection을 완성합니다
id DESC는 ordering suffix이고 total_cents는 SELECT projection을 index entry 안에서 충족하는 suffix입니다.
covering 여부와 관계없이 range candidate는 31행입니다
total_cents suffix는 range를 좁히지 않으므로 A_RANGE는 noncovering variant와 같은 31행입니다.
31행이 그대로 result candidate가 됩니다
leading equality와 range가 모든 WHERE predicate를 포함하므로 fixture pass 31, reject 0입니다.
index entry만으로 SELECT 값을 반환합니다
total_cents까지 secondary index에 있어 base-row materialization 모델 없이 id, customer_id, status, ordered_at, total_cents를 투영합니다.
인덱스 순서가 ORDER BY를 충족합니다
ordered_at DESC, id DESC 순서가 선택된 index order와 일치해 별도 filesort operator 없이 결과 순서를 만듭니다.
20번째 tuple에서 결과를 끊습니다
order 83356이 마지막 반환 row이고 order 79260이 다음 제외 row입니다. 두 tuple의 ordered_at과 id를 직접 비교합니다.
캡처된 20행을 그대로 반환합니다
세 variant가 같은 20행과 checksum f3b6a116…01a8b를 반환하는지 ID, 시각, 금액까지 대조합니다.
plan, trace, counters, result를 한 번에 대조합니다
used_key_parts와 range 문자열은 plan/trace에서, candidate 관계는 fixture에서, rows·cost·Handler·result는 각 캡처 locator에서 확인합니다.
캡처 결과
MySQL 8.4.10 · 20 rows · f3b6a116a0d644412cd60167a96e4e8d628adbe75106f53d78c3893fef401a8b