6부 · 안전하고 빠르게 (2) 인덱스와 뷰
← 트랜잭션과 무결성 · 목차 · 다음: 7부 파이썬과 SQL →
운영의 나머지 절반, 속도와 재사용입니다. 데이터가 수천·수만 행으로 늘면 어떤 쿼리는 눈에 띄게 느려집니다. 인덱스(index) 가 이를 극적으로 빠르게 만듭니다. 그리고 복잡한 쿼리를 매번 다시 쓰는 대신 뷰(view) 로 이름 붙여 재사용합니다.
flowchart LR
E[묻기] -.튜닝.-> J["빠른 조회<br/>인덱스 · 뷰"]
classDef here fill:#fdf0d5,stroke:#e67e22,color:#000,stroke-width:3px
classDef dim fill:#eee,stroke:#bbb,color:#888
class J here
class E dim
6.7 인덱스: 책 뒤의 '찾아보기'
SELECT * FROM books WHERE title = '모비딕'을 실행하면, 데이터베이스는 기본적으로 모든 행을 처음부터 끝까지 훑으며 제목이 맞는지 비교합니다. 이를 전체 스캔(full scan) 이라고 합니다. 6권이면 순식간이지만, 600만 권이면 600만 번 비교해야 합니다.
인덱스는 책 맨 뒤의 "찾아보기(색인)"와 같습니다. 책 전체를 넘기지 않고 색인에서 단어를 찾아 페이지로 바로 가듯, 인덱스는 특정 칸의 값을 미리 정렬해 둔 별도 구조라서 원하는 값을 빠르게 찾아갑니다.
flowchart LR
subgraph without["인덱스 없음"]
w["WHERE title='모비딕'"] --> ws["모든 행을 하나씩 비교<br/>(SCAN)"]
end
subgraph with["인덱스 있음"]
i["WHERE title='모비딕'"] --> is["색인에서 바로 위치 찾기<br/>(SEARCH USING INDEX)"]
end
classDef slow fill:#f5c6cb,stroke:#c0392b,color:#000
classDef fast fill:#d5f2e0,stroke:#27ae60,color:#000
class w,ws slow
class i,is fast
인덱스 만들기
CREATE INDEX idx_books_title ON books(title);
"books의 title 칸에 idx_books_title이라는 인덱스를 만들어라." 이제 title로 검색하면 데이터베이스가 이 인덱스를 활용합니다.
정말 빨라지는지 확인 — EXPLAIN QUERY PLAN
SQLite는 "이 쿼리를 어떻게 실행할 계획인지" 보여 주는 EXPLAIN QUERY PLAN을 제공합니다. 인덱스 전후를 비교하면:
EXPLAIN QUERY PLAN SELECT * FROM books WHERE title = '모비딕';
인덱스가 없을 때:
SCAN books
인덱스를 만든 후:
SEARCH books USING INDEX idx_books_title (title=?)
SCAN(전부 훑기)이 SEARCH ... USING INDEX(색인으로 바로 찾기)로 바뀌었습니다. 데이터가 클수록 이 차이가 체감 속도를 가릅니다. 쿼리가 느릴 때 EXPLAIN QUERY PLAN으로 "지금 스캔하고 있나?"를 확인하는 것이 성능 진단의 첫걸음입니다.
인덱스의 비용 (공짜가 아니다)
인덱스는 만능 버튼이 아닙니다. 대가가 있습니다.
- 저장 공간을 더 쓴다. 인덱스는 별도 구조라 디스크를 추가로 차지합니다.
- 쓰기가 느려진다. 행을
INSERT/UPDATE/DELETE할 때마다 인덱스도 함께 갱신해야 합니다. 인덱스가 많으면 쓰기마다 부담이 늘죠. - 모든 쿼리를 빠르게 하진 않는다.
WHERE·JOIN·ORDER BY에 자주 쓰는 칸엔 효과적이지만, 거의 안 쓰는 칸에 건 인덱스는 비용만 늘립니다.
실천 지침. "느린 쿼리가 실제로 생긴 다음", 그 쿼리가 거르거나 잇는 칸에 인덱스를 거세요. 미리 모든 칸에 인덱스를 까는 건 낭비입니다. 다행히 기본키와UNIQUE칸엔 인덱스가 자동으로 생깁니다 — 그래서id나title,order_date, 외래키customer_id)을 대상으로 합니다.
-- 자주 쓰는 외래키·날짜에 인덱스 (예시) CREATE INDEX idx_orders_customer ON orders(customer_id); CREATE INDEX idx_orders_date ON orders(order_date); DROP INDEX idx_books_title; -- 필요 없어지면 제거
6.8 뷰: 복잡한 쿼리에 이름 붙이기
5부에서 만든 "주문 상세"(고객·책·소계를 합친) 쿼리는 꽤 길었습니다. 이걸 자주 쓴다면 매번 다시 쓰는 대신, 뷰(view) 로 이름을 붙여 둘 수 있습니다. 뷰는 "저장된 쿼리"이자 "가상의 테이블"입니다.
CREATE VIEW order_details AS SELECT o.id AS order_id, c.name AS customer, b.title AS book, b.price, o.quantity, b.price * o.quantity AS subtotal, o.order_date FROM orders o JOIN customers c ON c.id = o.customer_id JOIN books b ON b.id = o.book_id;
이제 order_details를 진짜 테이블처럼 조회할 수 있습니다.
SELECT customer, book, subtotal FROM order_details ORDER BY subtotal DESC LIMIT 4;
출력:
customer book subtotal -------- ---------- -------- 이두리 데미안 36000 김하나 데미안 24000 최넷별 노인과 바다 18000 김하나 모비딕 15000
뷰 위에 또 집계를 얹을 수도 있습니다. "고객별 총 구매액"이 한결 짧아집니다.
SELECT customer, SUM(subtotal) AS total FROM order_details GROUP BY customer ORDER BY total DESC;
출력:
customer total -------- ----- 이두리 51000 김하나 49000 박세찬 24000 최넷별 18000
세 테이블 조인이 order_details라는 이름 뒤로 숨어, 분석 쿼리가 읽기 쉬워졌습니다.
뷰의 성질과 한계
- 데이터를 복제하지 않는다. 뷰는 결과를 따로 저장하지 않고, 조회할 때마다 원본 쿼리를 다시 실행합니다. 그래서 원본 테이블이 바뀌면 뷰도 자동으로 최신입니다(별도 갱신 불필요).
- 그래서 빠르게 만들어 주진 않는다. 뷰는 "정리·재사용" 도구이지 "가속" 도구가 아닙니다. 느린 쿼리를 뷰로 감싼다고 빨라지진 않습니다(속도는 인덱스의 몫).
- 보통 읽기 전용처럼 쓴다. 여러 테이블을 조인한 뷰에 직접
INSERT/UPDATE하는 건 제약이 많습니다. 뷰는 조회용으로 쓰고, 변경은 원본 테이블에 하는 게 깔끔합니다.
DROP VIEW order_details; -- 뷰 제거 (원본 테이블엔 영향 없음)
언제 쓰나. 같은 조인·계산을 여러 쿼리에서 반복할 때, 또는 복잡한 로직을 가리고 동료에게 단순한 인터페이스를 주고 싶을 때 뷰가 빛납니다. "이 보고서들이 다 같은 조인으로 시작하네" 싶으면 그 조인을 뷰로 빼세요.
6.9 성능, 균형 있게 보기
속도 이야기를 정리합니다. 초보가 빠지기 쉬운 함정은 "느려지지도 않았는데 미리 최적화"하는 것입니다. 실용적인 순서는 이렇습니다.
- 먼저 올바르게. 정확한 결과를 내는 쿼리를 먼저 만듭니다. 빠르지만 틀린 쿼리는 쓸모없습니다.
- 느려지면 측정. 체감될 만큼 느려지면
EXPLAIN QUERY PLAN으로 원인을 봅니다(전체 스캔인지). - 병목에 인덱스. 자주 거르고 잇는 칸에 인덱스를 겁니다. 그리고 다시 측정해 효과를 확인합니다.
- 반복을 뷰로 정리. 같은 복잡 쿼리가 반복되면 뷰로 묶어 가독성을 올립니다.
대부분의 입문 프로젝트는 데이터가 작아 성능을 거의 신경 쓸 필요가 없습니다. "이런 도구가 있고, 필요해지면 이 순서로 접근한다"만 알아 두면 충분합니다.
6.10 정리
- 인덱스는 칸 값을 미리 정렬해 둔 색인.
WHERE·JOIN·ORDER BY로 자주 쓰는 칸의 조회를 전체 스캔(SCAN)에서 색인 검색(SEARCH USING INDEX)으로 바꿔 빠르게 한다. - 인덱스는 공짜가 아니다 — 저장 공간을 쓰고 쓰기를 느리게 한다. 기본키·
UNIQUE엔 자동 생성되며, 직접 인덱스는 "실제로 느려진 뒤" 병목 칸에 건다. EXPLAIN QUERY PLAN으로 쿼리 실행 계획(스캔 여부)을 진단한다.- 뷰는 저장된 쿼리이자 가상 테이블. 복잡한 조인·계산에 이름을 붙여 재사용하고 가독성을 높인다. 데이터를 복제하지 않아 항상 최신이지만, 속도를 높이진 않는다(그건 인덱스의 일).
- 성능은 "먼저 올바르게 → 느려지면 측정 → 병목에 인덱스 → 반복은 뷰" 순서로 접근한다.
이제 데이터베이스를 안전하고 빠르게 다루는 기초를 갖췄습니다. 마지막 부에서 SQL을 코드와 실전으로 연결합니다. 파이썬에서 데이터베이스를 다루고(우리 서점을 프로그램으로 움직이고), 보안 함정을 피하고, pandas로 잇고, SQLite 너머의 세계로 시야를 넓힙니다.
직접 해 보기
orders의order_date에 인덱스를 만들고,EXPLAIN QUERY PLAN SELECT * FROM orders WHERE order_date = '2026-01-05';이 인덱스 전후로 어떻게 바뀌는지 비교하세요.- 6.8의
order_details뷰를 만든 뒤, 이를 이용해 "책별 총 매출"을 구해 보세요. (뷰에서GROUP BY book) - (생각해 보기) 모든 칸에 인덱스를 거는 게 왜 나쁜 생각일까요? 두 가지 비용을 적어 보세요.
- (생각해 보기) 뷰로 감싸도 느린 쿼리가 빨라지지 않는 이유는 무엇인가요?
← 트랜잭션과 무결성 · 목차 · 다음: 7부 파이썬과 SQL →