주문 조회 두 개 튜닝하기

MySQLBeginner
지금 연습하기

소개

성장하는 매장에는 유용한 주문 조회 두 가지가 필요합니다. 한 매장의 준비된 주문을 오래된 순서로 보는 조회와, 한 고객의 주문 이력을 최신순으로 보는 조회입니다. 첫 페이지에는 주문 일곱 건이 들어갑니다. 타임스탬프와 가격이 같아도 주문 ID가 다르면 별개로 유지해야 합니다. 0센트 프로모션 주문도 픽업 준비 상태일 수 있습니다.

재사용 가능한 MySQL 쿼리 두 개와 인덱스 스크립트를 저장하고 유용한 인덱스를 적용하며, 두 첫 페이지가 보조 인덱스 두 개의 공통 한도와 페이지당 MySQL 자체 진단 행 요청 64회의 한도 안에서 동작하게 합니다. 원래 합성 스냅샷에는 주문이 96,000건 넘게 있습니다. 이 단원에서는 첫 페이지를 튜닝하고, 다음 독립 단원에서는 완전한 커서 페이지 나누기를 배웁니다.

기본적인 터미널 탐색, MySQL 연결, SELECT/WHERE 지식이 필요합니다. 매개변수, 반개구간 시간 범위, 정렬, 인덱스, 실제 실행 계획 관찰은 사용 전에 설명합니다. 전용 MySQL 서버, 원본 데이터, Terminal은 준비되어 있으며 Python이나 프런트엔드 개발은 필요하지 않습니다.

/home/labex/project/order-desk에서 작업하세요. queue.sql, history.sql, indexes.sql을 만들거나 편집하고 보조 인덱스를 최대 두 개 관리하세요. 원래 주문, 열, 기본 키, 식별 정보, 상태, 정확한 애플리케이션 타임스탬프, 센트, 메모, 실행 도구를 보존하세요. 원본 행을 삭제하거나 다시 쓰거나, 진단 설정을 변경하거나, 옵티마이저 힌트를 추가하거나, 외부 캐시를 사용하지 마세요. 인덱스 이름과 동등한 SQL은 달라도 됩니다. 실행 환경의 표시 이름과 출력은 영어로 유지됩니다.

준비된 진단 연결에서는 스토리지 엔진 내부로 필터링을 숨길 수 있는 엔진 최적화인 인덱스 조건 푸시다운을 비활성화합니다. 이렇게 하면 MySQL 자체 행 요청 비교를 해석하기 쉬워집니다. 이 설정은 해당 연결에만 적용됩니다. 실습은 물리적 디스크 I/O를 측정하거나 운영 환경에서 일정 배수의 속도 향상을 약속하지 않습니다. 밀리초나 하나의 정확한 실행 계획 문자열은 완료 기준이 아닙니다.

작업 부하를 읽고 픽업 대기열 튜닝하기

이 단계에서는 유용한 인덱스를 고르기 전에 유효한 주문을 살펴봅니다. 첫 페이지가 빨라져도 필요한 주문 사실은 동일하게 들어 있어야 합니다.

일반 셸의 Terminal에서 시작하세요. cd는 작업 공간으로 이동하고 ./는 현재 디렉터리의 프로그램을 실행합니다. db는 주문을 읽고 과정용 인덱스를 관리하는 준비된 MySQL 클라이언트를 엽니다. here-document는 따옴표로 지정한 SQL 표시와 닫는 SQL 사이의 줄들을 해당 클라이언트로 보냅니다. 닫는 표시는 단독으로 있어야 합니다. SQL 문은 세미콜론으로 끝나며 블록이 끝나면 이 클라이언트 연결도 닫힙니다. 경로나 명령 이름을 잘못 입력했다면 ls로 실제 이름을 확인하고 수정한 명령을 다시 시도하세요. 원본 데이터는 그대로 유지하세요.

5월 10일 매장 3의 준비된 주문을 확인하세요. 비교 >=는 시작을 포함하고 <는 다음 자정을 제외합니다. ORDER BY placed_at,order_id는 시간 다음 ID로 오름차순 정렬합니다. ID는 같은 타임스탬프를 구분합니다. total_cents는 정수 센트를 저장하고, placed_at은 마이크로초 여섯 자리를 유지하는 애플리케이션 타임스탬프인 DATETIME(6)입니다. 제공된 이 타임스탬프는 시간대 변환이 필요하지 않습니다.

cd /home/labex/project/order-desk
./db <<'SQL'
SELECT order_id,shop_id,customer_id,state,placed_at,total_cents
FROM orders
WHERE shop_id=3 AND state='ready'
  AND placed_at>='2025-05-10' AND placed_at<'2025-05-11'
ORDER BY placed_at,order_id;
SQL

해당하는 주문은 열 건입니다. 서로 다른 ID 아홉 개가 09:15를 공유하고 주문 900021은 23:59:59.999999에 발생합니다. 주문 900004는 0센트이며 여전히 준비 상태입니다. 같은 시각의 주문 대부분은 1250센트이지만 같은 가격이 식별 정보는 아닙니다. 정확히 다음 자정인 주문은 다음 날에 속합니다. 접근 경로를 바꾸면서도 이 사실을 유지하세요.

인덱스는 MySQL이 행을 찾도록 돕는 추가 정렬 구조입니다. 저장 공간과 쓰기 유지 비용이 들므로 가능한 모든 인덱스를 추가하는 것이 목표는 아닙니다. 복합 인덱스는 여러 열로 항목을 정렬합니다. 기본 키가 이미 order_id를 식별합니다. 이 데스크는 두 작업 부하 전체에 추가 보조 인덱스를 최대 두 개 허용합니다.

실행 계획은 MySQL이 결과에 도달하는 방법을 설명합니다. 일반 EXPLAIN에는 추정치가 들어 있습니다. EXPLAIN ANALYZE는 SELECT를 실제로 실행하고 actual ... rows=... loops=... 같은 반복자 관찰값을 보고합니다. FORMAT=TREE는 반복자라고 하는 실행 단계를 들여쓴 부모/자식 텍스트로 표시합니다. LIMIT 7은 정렬된 행을 최대 일곱 개 요청합니다. 맨 위 반복자가 일곱 행을 반환해도 아래의 테이블 스캔 반복자는 훨씬 큰 집합을 읽을 수 있습니다. 경과 시간은 달라지며 채점하지 않습니다. 아래의 DATE(placed_at)은 달력 날짜를 추출하므로 이 특정 기준 요청은 여전히 올바른 하루를 선택합니다.

./db <<'SQL'
EXPLAIN ANALYZE FORMAT=TREE
SELECT order_id,shop_id,customer_id,state,placed_at,total_cents
FROM orders
WHERE shop_id=3 AND state='ready' AND DATE(placed_at)='2025-05-10'
ORDER BY placed_at,order_id
LIMIT 7;
SQL

맨 위의 일곱 행만 보지 말고 하위 접근 반복자를 읽으세요. 적절한 인덱스가 없으면 약 96,000개 주문을 스캔합니다. 진단 연결은 필터링이 보이도록 인덱스 조건 푸시다운을 꺼 둡니다. 준비된 이 비교 설정을 유지하세요. 반환 페이지가 작다는 사실만으로 데이터베이스 작업량이 작다고 입증할 수는 없습니다.

다른 매장/상태 요청과 임의의 반개구간 시간 범위를 처리할 수 있는 쿼리를 저장하세요. 이름 앞에 @를 붙이면 MySQL 자체의 MySQL 세션 변수가 됩니다. 데스크는 각 요청에서 해당 쿼리 연결에 @group_id, @state, @from_at, @until_at을 제공합니다. 수동 db 연결 하나에서 설정한 변수는 다른 연결에 유지되지 않습니다. SET 문은 저장된 쿼리 파일 밖에 두세요.

두 쿼리 인터페이스는 순서대로 여섯 열을 반환합니다: order_id, group_id, actor_id, state, event_at, amount_cents. AS는 선택한 열에 인터페이스 이름을 붙입니다. 매장은 group이고 고객은 actor입니다. 요약으로 대체하지 말고 실제 값을 보존하세요. LIMIT 7은 데스크의 주문 일곱 건 페이지 계약입니다. 줄이면 페이지를 최적화하는 대신 주문을 숨기게 됩니다. ASC는 오름차순을 명시합니다.

cat > queue.sql은 다음 전체 텍스트를 해당 파일에 쓰며 이전 내용을 덮어씁니다. 따옴표로 지정한 here-document 표시는 셸 확장을 막습니다. 닫는 표시를 단독으로 유지하며 전체 블록을 붙여 넣으세요:

cat > queue.sql <<'SQL'
SELECT order_id,shop_id AS group_id,customer_id AS actor_id,
       state,placed_at AS event_at,total_cents AS amount_cents
FROM orders
WHERE shop_id=@group_id AND state=@state
  AND placed_at>=@from_at AND placed_at<@until_at
ORDER BY placed_at ASC,order_id ASC
LIMIT 7;
SQL
./desk page queue

page는 저장한 SQL을 변경 없이 실행하고 실제 행을 표시합니다. 첫 페이지에는 0센트 주문을 포함해 ID 900001부터 900007까지 들어 있습니다. MySQL 자체 handler_requests 값은 이 진단 연결에서 서버/스토리지 엔진 경계의 요청을 세며 물리적 I/O 횟수가 아닙니다. 인덱스 적용 전에는 여전히 큽니다. 표시된 다음 요청 커서는 실제 마지막 반환 행에서만 가져옵니다. 이 첫 페이지 쿼리는 아직 사용하지 않습니다. null로 표시된 커서 입력은 첫 페이지 요청에 이전 행이 없다는 뜻입니다.

유용한 접근 접두부는 먼저 매장과 상태를 고정하고, 이어서 요청 시간 범위와 (time,ID) 순서를 따릅니다. 이 쿼리들에서는 동등 조건의 두 열 위치를 서로 바꿔도 됩니다. WHERE의 텍스트 순서가 인덱스 순서를 정하지는 않습니다. 시간만의 인덱스는 다른 요청에서 무관한 매장이나 상태를 여전히 걸러내야 할 수 있습니다. 새 복사본에도 적용할 수 있도록 선택한 후보를 indexes.sql에 저장하세요.

cat > indexes.sql <<'SQL'
CREATE INDEX queue_time ON orders(shop_id,state,placed_at,order_id);
SQL
./db < indexes.sql

<는 파일을 클라이언트의 SQL 입력으로 제공합니다. 새 주문이 아니라 접근 구조를 만듭니다. 나중에 같은 CREATE INDEX를 다시 실행하면 인덱스가 이미 존재하므로 MySQL이 중복 키 이름을 보고합니다. 과정에서 만든 후보를 유지할지 교체할지 결정하기 전에 기존 인덱스를 살펴보세요. 원본 주문을 삭제하거나 이 스크립트가 반복 실행해도 같은 결과를 유지하는 마이그레이션이라고 기대하지 마세요.

여기서 -e는 따옴표 안의 전체 SQL 문자열을 실행하고 종료합니다. 데스크의 plan 동작은 실제 저장 쿼리에 EXPLAIN ANALYZE를 실행합니다. --from과 --until은 이번 요청의 다른 시작과 제외할 끝점을 선택합니다. 날짜만 있는 값은 자정을 뜻합니다.

./db -e "SHOW INDEX FROM orders;"
./desk page queue
./desk plan queue
./desk page queue --from 2025-01-05 --until 2025-01-12

이제 접근 경로는 좁은 인덱스 범위를 사용해야 하며, 원래 5월 10일 페이지에는 같은 주문 ID 일곱 개와 정확한 필드가 계속 들어 있어야 합니다. 두 요청 모두 64회 요청 한도에 맞는지 확인하세요. SHOW INDEX는 존재를 입증합니다. 실제 계획, 반환 행, 요청 횟수가 유용한 동작을 입증합니다. 정확한 인덱스 이름, 비용, 밀리초는 달라도 됩니다.

명령이나 파일 이름을 잘못 입력했다면 오류를 읽고 ls로 작업 공간을 확인한 뒤 수정한 이름으로 다시 시도하세요. Order desk error는 파일, SQL, 연결 오류를 뜻하며 성공한 빈 결과가 아닙니다. 저장된 SQL이나 후보 인덱스를 수정하고 실제 요청을 다시 실행하세요. 정확성과 관찰된 작업량을 모두 확인한 뒤 단계 검사를 실행하세요.

고객 이력과 인덱스 한도 공유하기

이 단계에서는 픽업 대기열을 잃거나 보조 인덱스 두 개를 넘지 않으면서 두 번째 업무 요청을 지원합니다.

계속 Terminal과 /home/labex/project/order-desk를 사용하세요. 원래 테이블과 queue.sql을 유지하세요. 이력 조회는 픽업 상태나 매장과 관계없이 고객 한 명을 선택하고 최신 주문부터 보여 줍니다. DESC는 정렬을 뒤집습니다. 같은 시각의 주문도 일관된 최신순을 갖도록 타임스탬프와 ID 모두에 사용하세요. MySQL 자체 매개변수 @actor_id가 고객을 식별합니다. 고정 고객으로 대체하거나 대기열의 상태 제한을 추가하지 마세요.

완전한 첫 페이지 이력 쿼리를 쓰세요. 픽업 쿼리와 같은 여섯 열 인터페이스와 반개구간을 사용합니다:

cat > history.sql <<'SQL'
SELECT order_id,shop_id AS group_id,customer_id AS actor_id,
       state,placed_at AS event_at,total_cents AS amount_cents
FROM orders
WHERE customer_id=@actor_id
  AND placed_at>=@from_at AND placed_at<@until_at
ORDER BY placed_at DESC,order_id DESC
LIMIT 7;
SQL
./desk page history
./desk plan history

5월 10일 이력은 마지막 마이크로초의 주문 900021로 시작하고, 같은 시각의 ID들이 내림차순으로 이어집니다. 실제 센트와 상태는 계속 보입니다. 이 올바른 첫 페이지도 작업량이 너무 클 수 있습니다. 기존 대기열 인덱스는 이력이 제한하지 않는 매장/상태로 시작하기 때문입니다.

두 번째 유용한 접두부는 고객으로 시작하고 시간/ID가 뒤따릅니다. 오름차순 인덱스를 역방향으로 스캔해 이력의 내림차순에 사용할 수 있으며, 관찰한 계획이 실제 동작을 보여 줍니다. 이 작업은 추가 커버링 열을 요구하지 않습니다. 보조 인덱스를 두 개 이하로 유지하고 두 작업 부하 모두에 유용한 접근을 유지하세요.

cat >> indexes.sql은 기존 원본 파일에 새 문장을 추가합니다. >는 덮어씁니다. 이 VM에는 대기열 인덱스가 이미 있으므로 추가한 뒤 아래 새 CREATE 문만 실행하세요. 새 데이터베이스에서는 두 문장으로 된 저장 파일이 완전한 인덱스 원본입니다.

cat >> indexes.sql <<'SQL'
CREATE INDEX actor_time ON orders(customer_id,placed_at,order_id);
SQL
./db -e "CREATE INDEX actor_time ON orders(customer_id,placed_at,order_id);"
./db -e "SHOW INDEX FROM orders;"
./desk page history
./desk plan history

더 넓은 고객 이력, 다른 식별자, 빈 날짜 범위, 원래 대기열을 확인하세요. --actor는 요청 고객 ID를 바꾸며 원본 데이터를 바꾸지 않습니다. 유효한 쿼리도 행을 반환하지 않을 수 있으며 이는 실행 오류와 다릅니다.

./desk page history --from 2025-01-01 --until 2025-05-01
./desk page history --actor 78
./desk page history --from 2027-01-01 --until 2027-01-02
./desk page queue

두 작업 부하 모두 진단 한도 안에서 올바른 첫 페이지를 유지해야 합니다. 빈 요청은 꾸며낸 주문 대신 실제 빈 행 리스트를 반환합니다. 저장된 indexes.sql과 실제 정의는 독립적으로 검사합니다. 올바른 파일이 적용을 입증하지 않으며, 동작하는 실제 인덱스가 저장된 원본을 대신하지도 않습니다.

후보를 바꾼다면 SHOW INDEX를 확인하고 교체할 과정용 인덱스만 삭제한 다음 indexes.sql을 갱신하고 새 정의를 적용하세요. 원래 기본 키, 열, 행은 절대 삭제하지 마세요. CREATE 반복 시 중복 이름 오류가 나면 현재 정의를 확인해야 하며 VM을 초기화할 이유가 아닙니다.

이 단원의 완료 목표는 유용한 첫 페이지 조회 두 개입니다. 반환된 일곱 행이 더 긴 대기열 전체는 아닙니다. 다음 독립 단원은 이 첫 페이지 쿼리와 인덱스가 이미 준비된 상태에서 시작하고, 모든 페이지에서 주문을 유지하는 커서 조건을 가르칩니다. 저장된 쿼리 두 개, 전체 인덱스 스크립트, 실제로 적용된 보조 인덱스 최대 두 개로 최종 검사를 실행하세요.

요약

실제 주문 사실과 실제 실행 관찰을 살펴보고, 반개구간을 사용하는 재사용 가능한 픽업 및 고객 이력 쿼리를 저장하며, 제한된 수의 유용한 인덱스로 두 첫 페이지를 지원했습니다. 원래 식별 정보, 타임스탬프, 센트, 메모는 그대로 유지됩니다. MySQL 자체 진단 행 요청을 반환 행 수, 추정치, 물리적 I/O와 구분했습니다. 여러 페이지의 완전한 전달은 다음 단원의 목표입니다.