AD JB Toolbox 01

MariaDB 강좌 / EXPLAIN으로 실행 계획 읽고 인덱스 전후 비교하기

EXPLAIN은 쿼리를 어떻게 실행할지 최적화기가 선택한 계획을 보여 줍니다. 인덱스 전후에 같은 쿼리와 결과를 비교하면서 접근 방식·선택 키·예상 행 수를 읽되, 예상치와 실제 실행 시간은 구분해야 합니다.

실행 계획에서 먼저 볼 항목 정하기

어떤 테이블부터 읽고 어떤 키로 범위를 좁힐지 이해하는 것이 출발점입니다. type이 ALL이라고 무조건 나쁘거나 Using filesort가 나오면 디스크 파일 정렬이 확정되는 것은 아닙니다. 데이터량과 선택 비율, 실제 지연을 함께 봅니다. 후보 키를 정하는 근거는 인덱스 만들기: 단일·복합 인덱스와 열 순서이며, 실제로 느린 쿼리를 찾을 때는 서버 설정·SQL 모드·시간대·로그 확인하기의 로그 기록 조건도 함께 확인합니다.

열 확인할 내용 주의
table 현재 단계의 테이블·별칭 한 쿼리에 여러 단계 가능
type 접근 방식 작은 테이블의 ALL은 합리적일 수 있음
possible_keys / key 후보 / 실제 선택 키 인덱스 존재와 선택은 별개
rows 예상 읽을 행 수 정확한 실행 결과 개수 아님
Extra 필터·정렬·커버링 등 문구 하나만으로 성능 판정 금지

같은 분포를 재현하는 1000행 준비하기

shopcourse에 접속해 digits_40_lab·explain_40_lab이 없는지 확인합니다. 아래 코드는 숫자 0~9의 세 조합으로 1000행을 만들며 실제 주문 자료를 읽거나 변경하지 않습니다. 고객 번호는 1~100에 나누어 고객 한 명당 10행이 생깁니다.

USE shopcourse;
CREATE TABLE digits_40_lab(n TINYINT PRIMARY KEY) ENGINE=InnoDB;
INSERT INTO digits_40_lab VALUES(0),(1),(2),(3),(4),(5),(6),(7),(8),(9);
CREATE TABLE explain_40_lab(
 order_id INT PRIMARY KEY,customer_id INT NOT NULL,
 ordered_at DATETIME NOT NULL,amount DECIMAL(10,2) NOT NULL
) ENGINE=InnoDB;
INSERT INTO explain_40_lab
SELECT 1+a.n+10*b.n+100*d.n,
 MOD(a.n+10*b.n+100*d.n,100)+1,
 DATE_ADD('2026-01-01',INTERVAL (a.n+10*b.n+100*d.n) MINUTE),1000
FROM digits_40_lab a CROSS JOIN digits_40_lab b CROSS JOIN digits_40_lab d;

0~999의 번호 계산과 입력 행 수를 확인하려면 SELECT COUNT(*) FROM explain_40_lab;를 실행합니다. 이 인공 분포는 계획 읽기 연습용이며 운영 고객의 편중이나 데이터 크기를 대신하지 않습니다.

인덱스 전후에 같은 EXPLAIN 실행하기

EXPLAIN SELECT order_id,ordered_at FROM explain_40_lab
WHERE customer_id=47 AND ordered_at>='2026-01-01'
ORDER BY ordered_at,order_id;
CREATE INDEX idx_e40_customer_date
ON explain_40_lab(customer_id,ordered_at,order_id);
EXPLAIN SELECT order_id,ordered_at FROM explain_40_lab
WHERE customer_id=47 AND ordered_at>='2026-01-01'
ORDER BY ordered_at,order_id;

첫 계획에서는 보조 키 없이 읽고, 이후에는 고객·시간 키를 사용할 후보가 생깁니다. 실제 key가 무엇인지와 rows가 어떻게 바뀌었는지 자신의 출력으로 비교합니다. 버전·통계에 따라 출력 세부 값은 다를 수 있으므로 특정 숫자를 그대로 외우지 않습니다. EXPLAIN 공식 항목 설명을 참고하세요.

이 글의 1000행 생성 코드를 MariaDB 12.3.3에서 실행했을 때 관찰한 한 번의 계획은 다음과 같습니다. 자신의 출력이 같은 숫자여야 한다는 뜻은 아니며 데이터·통계·버전에 따라 달라질 수 있습니다.

시점 type key rows Extra
인덱스 전 ALL NULL 1000 Using where; Using filesort
인덱스 후 range idx_e40_customer_date 10 Using where; Using index

이 관찰에서는 전체 스캔 후보가 고객·시각 범위 접근으로 바뀌고 예상 읽기 규모가 1000에서 10으로 줄었습니다. 이것은 실행 시간이 100배 빨라졌다는 측정이 아닙니다. 결과 주문 열 개가 보존되는지와 실제 실행 정보를 별도로 확인합니다.

계획의 예상과 실제 반환 행을 나누어 검증하기

SELECT order_id,ordered_at FROM explain_40_lab
WHERE customer_id=47 AND ordered_at>='2026-01-01'
ORDER BY ordered_at,order_id;

반환 주문 번호는 47부터 시작해 100씩 증가한 10개입니다. 같은 SELECT가 인덱스 추가 전후에 같은 행을 같은 순서로 반환해야 합니다. EXPLAIN의 rows는 반환 행 수가 아니라 해당 단계의 예상 읽기 규모이므로 COUNT(*)와 다른 값을 보일 수 있습니다.

계획만으로 지연 시간을 보장하지 않습니다. 실제 비교에서는 동일 조건·데이터·동시 부하와 캐시 상태를 통제하고 여러 번 측정합니다. 작은 실습의 시간 차이를 실제 서비스의 개선 비율로 발표하지 않습니다.

ANALYZE와 통계 갱신의 의미 구분하기

MariaDB의 ANALYZE 문장 계열은 쿼리를 실제로 실행해 실행 정보를 얻습니다. EXPLAIN과 같은 읽기 전용 계획 보기라고 생각하고 UPDATE·DELETE 앞에 붙이면 안 됩니다. 필요한 경우 별도 환경에서 SELECT만 대상으로 적용 범위와 권한을 확인합니다.

ANALYZE TABLE은 테이블 통계를 갱신하는 다른 기능입니다. 통계가 오래되면 최적화기의 추정이 부정확해질 수 있지만, 통계 갱신도 서버 부하·잠금과 버전 차이를 검토해야 합니다. FORCE INDEX로 원하는 계획을 강제로 만들기 전에 데이터 분포와 쿼리 형태를 먼저 조사합니다.

실제 실행 정보가 필요하면 이 학습용 SELECT만 대상으로 다음처럼 확인할 수 있습니다. ANALYZE FORMAT=JSON은 쿼리를 실행하며 r_rows·r_loops 등 실제 측정 필드와 rows 추정값을 구분해서 읽습니다. 실행 시간은 환경마다 달라 고정된 정답 숫자를 두지 않습니다.

ANALYZE FORMAT=JSON
SELECT order_id,ordered_at FROM explain_40_lab
WHERE customer_id=47 AND ordered_at>='2026-01-01'
ORDER BY ordered_at,order_id;

ANALYZE FORMAT=JSON의 실행 필드

검증 기록과 실습 객체 정리하기

서버 버전, CREATE TABLE·인덱스 정의, 데이터 생성 방법, 비교한 SELECT와 전후 계획을 함께 보관하면 결과를 재현하기 쉽습니다. 이 글의 실습 테이블 두 개만 보관 필요성을 확인하고 삭제합니다. 두 객체에는 외래키가 없으며 기존 주문 테이블은 정리 대상이 아닙니다.

DROP TABLE shopcourse.explain_40_lab;
DROP TABLE shopcourse.digits_40_lab;
같은 카테고리의 다른 글
MariaDB 강좌 / EXPLAIN으로 실행 계획 읽고 인덱스 전후 비교하기

MariaDB 강좌 / EXPLAIN으로 실행 계획 읽고 인덱스 전후 비교하기

1000행 실습으로 인덱스 전후 EXPLAIN과 실제 반환 행을 비교합니다. ANALYZE FORMAT=JSON의 실행 정보와 통계 갱신을 구분하고 결과·환경을 기록합니다.

MariaDB 강좌 / VIEW로 자주 쓰는 조회를 정의하기

MariaDB 강좌 / VIEW로 자주 쓰는 조회를 정의하기

CREATE VIEW로 결제 주문 합계를 정의하고 원본 변경·취소가 조회 결과에 반영되는지 확인합니다. CTE와의 차이, INVOKER·DEFINER 권한, 집계 뷰의 수정 제한과 정리 순서를 설명합니다.

MariaDB 강좌 / COUNT·SUM·AVG·MIN·MAX로 집계하기

MariaDB 강좌 / COUNT·SUM·AVG·MIN·MAX로 집계하기

COUNT·SUM·AVG·MIN·MAX의 집계 대상과 단위를 상품 재고로 비교합니다. NULL·0·빈 결과의 차이, 행별 계산 후 합계, 최솟값과 해당 행을 혼동하지 않는 기준을 설명합니다.

MariaDB 강좌 / CREATE TABLE로 테이블 구조 정의하기

MariaDB 강좌 / CREATE TABLE로 테이블 구조 정의하기

상품 테이블을 예로 열·타입·기본값·기본키·저장 엔진을 정의하고 실제 생성 결과를 확인합니다. 기존 테이블 처리, IF NOT EXISTS의 한계와 안전한 반복 실습 방법도 설명합니다.

MariaDB 강좌 / 문자열 타입 선택하기: CHAR·VARCHAR·TEXT

MariaDB 강좌 / 문자열 타입 선택하기: CHAR·VARCHAR·TEXT

CHAR·VARCHAR·TEXT를 저장 목적에 따라 선택하고 문자 수·바이트 수·끝 공백을 비교합니다. 한글과 이모지 입력, 길이 초과 오류, 정렬 규칙과 긴 본문 저장 시 주의점을 설명합니다.

MariaDB 강좌 / Windows에 MariaDB 설치하고 서버 실행하기

MariaDB 강좌 / Windows에 MariaDB 설치하고 서버 실행하기

Windows MSI 설치에서 서버·클라이언트, 관리자 인증, 서비스와 포트를 설정합니다. SQL 실행을 확인하고 이후 강좌에서 사용할 학습용 계정을 준비하며 시작·접속 오류를 점검합니다.

MariaDB 강좌 / 서브쿼리·EXISTS·NOT EXISTS로 조건 표현하기

MariaDB 강좌 / 서브쿼리·EXISTS·NOT EXISTS로 조건 표현하기

평균값 서브쿼리와 EXISTS·NOT EXISTS를 상품·고객·주문으로 실습합니다. 한 값의 반환 조건, 상관 키 누락, paid 주문 부재의 정의와 NOT IN의 NULL 함정을 설명합니다.

MariaDB 강좌 / ALTER TABLE로 구조 변경하기: 열·제약 조건·변경 전 점검

MariaDB 강좌 / ALTER TABLE로 구조 변경하기: 열·제약 조건·변경 전 점검

ALTER TABLE을 새 코드 열 추가·기존 행 채우기·제약 적용으로 나누어 실습합니다. NULL·중복 사전 검사, MODIFY 속성 보존, 실제 실패 입력과 잠금·복구 계획을 설명합니다.

MariaDB 강좌 / Docker로 MariaDB 실습 환경 만들기

MariaDB 강좌 / Docker로 MariaDB 실습 환경 만들기

Compose 파일로 MariaDB 포트·암호 파일·데이터 볼륨·헬스체크를 구성합니다. 연결 확인과 컨테이너 교체 실습을 통해 데이터 유지, 초기화와 접속 오류의 차이를 이해합니다.

MariaDB 강좌 / 데이터베이스 만들기: CREATE DATABASE·USE·문자 집합

MariaDB 강좌 / 데이터베이스 만들기: CREATE DATABASE·USE·문자 집합

shopcourse 데이터베이스를 만들고 현재 연결의 작업 대상으로 선택합니다. utf8mb4와 collation, 기존 DB에서 IF NOT EXISTS의 한계, 권한·문자 설정 확인을 예제로 설명합니다.