์๋ฒฝ 2์, ์ฌ๋ ์๋ฆผ์์ด ์๋ํ๊ฒ ์ธ๋ฆฝ๋๋ค. "์ ๋ฐฐ๋, DB CPU ์ฌ์ฉ๋ฅ ์ด 99%๋ฅผ ์ฐ๊ณ API ์๋ต์ด ํฐ์ก์ด์!" ์์๋์ ํ๋ฆฌ๋ฉฐ ์กธ๋ฆฐ ๋์ ๋น๋น๋ ์ฃผ๋์ด ์์ , ๋๊ตฌ๋ ํ ๋ฒ์ฏค ๊ฒช์ด๋ณธ ์ ๋ชฝ์ด์ฃ . ๋ฐ์ดํฐ๋ฒ ์ด์ค๊ฐ ๋ป์์ ๋ ๋ฌด์์ ์ธ๋ฑ์ค๋ง ๋์ง๋์ง ๋ถ์ด๋ค๊ฐ ์คํ๋ ค ์ฐ๊ธฐ(INSERT/UPDATE) ์ฑ๋ฅ๊น์ง ๋ง์๋จน์ ์ ์์ผ์ ๊ฐ์? ์ค๋์ ์๋ง์ ํ๋ก๋์ ์ฅ์ ๋ฅผ ์์ตํ๋ฉฐ ๋ผ์ ์๊ธด PostgreSQL ์ธ๋ฑ์ค ํ๋ ๋ฐ ์คํ๊ณํ(Execution Plan) ์๋ฒฝ ํด๋ถ ํ์ ์๋์์ด ๋ฐฉ์ถํด ๋๋ฆฝ๋๋ค.
1. ๋งน๋ชฉ์ ์ธ EXPLAIN์ ๋ฒ๋ ค๋ผ: `EXPLAIN (ANALYZE, BUFFERS)`๊ฐ ์ง์ง์ธ ์ด์
๋ง์ ๋ถ๋ค์ด ์ฟผ๋ฆฌ ์ต์ ํ๋ฅผ ํ ๋ ๋จ์ํ EXPLAIN๋ง ์น๊ณ ๋๋
๋๋ค. ํ์ง๋ง EXPLAIN๋ง ์ฐ๋ฉด ์ตํฐ๋ง์ด์ ๊ฐ ์ถ์ ํ '์์ ๋น์ฉ(Cost)'๋ง ๋ณด์ฌ์ค ๋ฟ, ์ค์ ์คํ๋ ๊ฒฐ๊ณผ๊ฐ ์๋๋๋ค. ์ฐ๋ฆฌ๊ฐ ํ๋ก๋์
ํ๋์์ ๋ฐ๋์ ์จ์ผ ํ๋ ๋ฌด๊ธฐ๋ ๋ฐ๋ก EXPLAIN (ANALYZE, BUFFERS)์
๋๋ค.
-- ๋จ์ ์ถ์ ์น๊ฐ ์๋ ์ค์ ์คํ ์๊ฐ๊ณผ I/O ๋ฉ๋ชจ๋ฆฌ ๋ฒํผ ์ํ๊น์ง ํ์ธ
EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS)
SELECT user_id, order_date, total_amount
FROM orders
WHERE created_at >= '2026-01-01'
AND status = 'COMPLETED';
์ฌ๊ธฐ์ ์ฐ๋ฆฌ๊ฐ ์ง์คํด์ ๋ฏ์ด๋ด์ผ ํ ํต์ฌ ์งํ๋ ์ธ ๊ฐ์ง์ ๋๋ค.
- Actual Time:
(actual time=0.045..1.230 rows=150 loops=1)ํํ๋ก ์ถ๋ ฅ๋ฉ๋๋ค. ์ ์ซ์๋ ์ฒซ ํ์ ๊ฐ์ ธ์ค๋ ๋ฐ ๊ฑธ๋ฆฐ ์๊ฐ(ms), ๋ค ์ซ์๋ ๋ชจ๋ ๊ฒฐ๊ณผ๋ฅผ ๋ฐํํ๋ ๋ฐ ๊ฑธ๋ฆฐ ์๊ฐ์ ๋๋ค. - Rows Removed by Filter: ์ธ๋ฑ์ค๋ฅผ ํ๋๋ผ๋ ํํฐ๋ง ๊ณผ์ ์์ ๋ฒ๋ ค์ง ํ์ด ์์ญ๋ง ๊ฑด์ด๋ผ๋ฉด, ๋ณตํฉ ์ธ๋ฑ์ค(Composite Index) ์ปฌ๋ผ ์์๊ฐ ์๋ชป๋์๊ฑฐ๋ ์ธ๋ฑ์ค ์กฐ๊ฑด์ด ์์ฑํ๋ค๋ ๊ฐ๋ ฅํ ์ ํธ์ ๋๋ค!
- Buffers (Shared Hit vs Read):
Buffers: shared hit=420 read=15์์ hit์ ๊ณต์ ๋ฉ๋ชจ๋ฆฌ(์บ์)์์ ๊ฐ์ ธ์จ ๋ธ๋ก ์์ด๊ณ , read๋ ๋์คํฌ I/O๋ฅผ ์ง์ ๋ฐ์์ํจ ๋ธ๋ก ์์ ๋๋ค. ๋์คํฌ Read ๋น์จ์ด ๋์์๋ก I/O ๋ณ๋ชฉ์ด ๋ฐ์ํฉ๋๋ค.
์๋์ด์ ์ค๋ฌด ๊ฟํ: Costs ์์น์ ํํน๋์ง ๋ง์ธ์!
Cost๋ PostgreSQL ์ตํฐ๋ง์ด์ ๋ด๋ถ์ '์๋์ ์ฐ์ฐ ์ ์'์ผ ๋ฟ ์ค์ ๊ฑธ๋ฆฐ ๋ฐ๋ฆฌ์ด(ms) ๋จ์ ์๊ฐ์ด ์๋๋๋ค. Cost๊ฐ ๋ฎ์๋ ๋์คํฌ I/O๊ฐ ํญ๋ฐํ๋ฉด ์ฟผ๋ฆฌ๋ ๊ธฐ์ด๊ฐ๋๋ค. ์ธ์ ๋ Actual Total Time๊ณผ Buffers: shared read ์์น๋ฅผ 0์ ๊ฐ๊น๊ฒ ๋ง๋๋ ๊ฒ์ 1์์ ๋ชฉํ๋ก ์ผ์ผ์ธ์.
2. ์ํฉ๋ณ๋ก ๊ณจ๋ผ ์ฐ๋ PostgreSQL ์ธ๋ฑ์ค ๋ฌด๊ธฐ์ฐฝ๊ณ
ํํ ์ธ๋ฑ์ค๋ผ๊ณ ํ๋ฉด B-Tree๋ง ๋ ์ฌ๋ฆฌ์์ฃ ? ํ์ง๋ง PostgreSQL์ ์์ ์ด์์ผ๋ก ๋๋ํ๊ณ ๋ค์ํ ์ธ๋ฑ์ค ์์ง์ ์ ๊ณตํฉ๋๋ค. ๋ฐ์ดํฐ ํน์ฑ์ ๋ง์ถฐ ์ ๋๋ก ๋ ์ธ๋ฑ์ค๋ฅผ ๊ฝ์์ฃผ๋ฉด ๋๋ผ๋งํฑํ ์ฑ๋ฅ ํฅ์์ ๊ฒฝํํ ์ ์์ต๋๋ค.
โ ๋ถ๋ถ ์ธ๋ฑ์ค (Partial Index) โ ์ฉ๋์ ์ค์ด๊ณ ์๋๋ ๊ทน๋ํ!
์ํํธ ๋๋ฆฌํธ(Soft Delete)๋ ํน์ ์ํ๊ฐ(์: status = 'PENDING')์ฒ๋ผ ์ ์ฒด ๋ฐ์ดํฐ ์ค 5~10% ๋ฏธ๋ง์ ํน์ ๋ ์ฝ๋๋ง ์์ฃผ ์กฐํํ๋ค๋ฉด ์ ์ฒด ํ์ ์ธ๋ฑ์ฑํ ์ด์ ๊ฐ ์ ํ ์์ต๋๋ค.
-- [AS-IS] 1,000๋ง ๊ฑด ์ ์ฒด์ ์ธ๋ฑ์ค ์์ฑ (์ฉ๋ ์๋ฐฑ MB ๋ญ๋น, CUD ์ฑ๋ฅ ์ ํ)
CREATE INDEX idx_orders_unprocessed_bad ON orders (created_at);
-- [TO-BE] ๋ฏธ์ฒ๋ฆฌ ์ฃผ๋ฌธ 5๋ง ๊ฑด๋ง ์ ๊ณจ๋ผ ์ธ๋ฑ์ฑ (์ฉ๋ 95% ์ ๊ฐ, ์๋์ ์๋)
CREATE INDEX idx_orders_unprocessed_good
ON orders (created_at)
WHERE status IN ('PENDING', 'PROCESSING');
โก ์ปค๋ฒ๋ง ์ธ๋ฑ์ค (Covering Index with INCLUDE) โ Table Random Access ์ ๊ฑฐ
์ธ๋ฑ์ค๋ฅผ ํตํด ํ์ ์์น๋ฅผ ์ฐพ์ ๋ค ์ค์ ์ปฌ๋ผ ๊ฐ์ ์ฝ์ผ๋ฌ ํ
์ด๋ธ ํ(Heap) ์์ญ์ผ๋ก ๊ฑด๋๊ฐ๋ Table Access ๊ณผ์ ์ ์์ ์ฃผ๋ ๊ธฐ์ ์
๋๋ค. PostgreSQL 11๋ถํฐ ์ง์๋๋ INCLUDE ์ ์ ์ฐ๋ฉด B-Tree ํธ๋ฆฌ ํ์ ํค๊ฐ ์๋ ํ์ด๋ก๋ ๋ฐ์ดํฐ๋ก๋ง ์ปฌ๋ผ์ ํฌํจ์์ผ ์ธ๋ฑ์ค ํฌ๊ธฐ๋ฅผ ์ฌ๋ฆผํ๊ฒ ์ ์งํ ์ ์์ต๋๋ค.
-- user_id๋ก ํ์ํ๊ณ total_price๋ฅผ ์ธ๋ฑ์ค ํ์ด๋ก๋์ ํฌํจ
CREATE INDEX idx_orders_user_covering
ON orders (user_id)
INCLUDE (total_price, order_date);
-- ์คํ๊ณํ์์ 'Index Only Scan'์ด ๋จ๋ฉฐ ํ
์ด๋ธ I/O๊ฐ 0์ผ๋ก ์๋ ด!
SELECT user_id, total_price, order_date
FROM orders
WHERE user_id = 95210;
โข BRIN (Block Range Index) โ ๋์ฉ๋ ์๊ณ์ด/๋ก๊ทธ ๋ฐ์ดํฐ์ ๊ตฌ์ํฌ์
IoT ์ผ์ ๋ฐ์ดํฐ๋ ๋ก๊ทธ ํ ์ด๋ธ์ฒ๋ผ ์์ต ๊ฑด์ด ๋งค์ผ ์๊ฐ ์์๋๋ก ์์ด๋ ํ ์ด๋ธ์ ์ผ๋ฐ B-Tree ์ธ๋ฑ์ค๋ฅผ ๊ฑธ๋ฉด ์ธ๋ฑ์ค ํฌ๊ธฐ๋ง ์์ญ GB์ ๋ฌํด ๋ฉ๋ชจ๋ฆฌ๋ฅผ ์ง์ด์ผํต๋๋ค. ์ด๋ BRIN์ ์ฌ์ฉํ๋ฉด ๋ฌผ๋ฆฌ์ ๋ธ๋ก ๋ฒ์์ ์ต์/์ต๋๊ฐ๋ง ์ ์ฅํ๋ฏ๋ก B-Tree ๋๋น 1/100 ๋ฏธ๋ง์ ์ฉ๋์ผ๋ก ์ด๊ณ ์ ํ์์ด ๊ฐ๋ฅํฉ๋๋ค.
| ์ธ๋ฑ์ค ์ข ๋ฅ | ์ ํฉํ ์ฌ์ฉ ์๋๋ฆฌ์ค | ์ ์ฅ ๊ณต๊ฐ / ์ฐ๊ธฐ ์ค๋ฒํค๋ | ํต์ฌ ์ฅ์ |
|---|---|---|---|
| B-Tree | ๋ฒ์ฉ์ ์ธ ๊ณ ์ ํค, ์ ๋ ฌ, ๋ฒ์ ๊ฒ์ | ๋ณดํต / ๋ณดํต | ๊ฐ์ฅ ์์ ์ ์ด๊ณ ๋ฒ์ฉ์ |
| Partial Index | ํน์ ์กฐ๊ฑด(์: ํ์ฑ ์ฌ์ฉ์, ๋ฏธ์ฒ๋ฆฌ ์ฃผ๋ฌธ) | ๋งค์ฐ ์ ์ / ๋งค์ฐ ๋ฎ์ | ๋ถํ์ํ ์ฐ๊ธฐ ๋ถํ ๋ฐ ์ฉ๋ 90% ์ ๊ฐ |
| Covering (INCLUDE) | ํน์ SELECT ์ปฌ๋ผ ์กฐํ๊ฐ ๋น๋ฒํ ํจํด | ๋ณดํต / ์ฝ๊ฐ ์ฆ๊ฐ | Index Only Scan์ผ๋ก ๋์คํฌ I/O ์ ๊ฑฐ |
| BRIN | ์์ต ๊ฑด์ ์๊ณ์ด ๋ก๊ทธ, Append-Only ํ ์ด๋ธ | ๊ทน์ (์ MB ์์ค) / ๊ทน์ | ์ด๋์ฉ๋ ๋ฐ์ดํฐ์ ์๋์ ๊ฐ์ฑ๋น ์ต์ ํ |
| GIN | JSONB ๋ฐ์ดํฐ ๊ฒ์, ์ ๋ฌธ ๊ฒ์(Full-Text), ๋ฐฐ์ด | ํผ / ํผ | ๋น์ ํ ๋ฌธ์ ๋ฐ ๋ค์ค ์์ ๊ฒ์ ์ต๊ฐ์ |
3. ์ค๋ฌด์์ ๋ฐฅ๋จน๋ฏ ์ ์ง๋ฅด๋ ์ธ๋ฑ์ค ํ๋ ์ต์ ์ ํจ์ 3๊ฐ์ง
์๋ฌด๋ฆฌ ์ข์ ์ธ๋ฑ์ค๋ฅผ ๋ง๋ค์ด ๋๋ ์ฟผ๋ฆฌ ์์ฑ ์ต๊ด์ด ์๋ง์ด๋ฉด ์ตํฐ๋ง์ด์ ๋ ๊ฐ์ฐจ ์์ด Seq Scan(Full Table Scan)์ ๋๋ ค๋ฒ๋ฆฝ๋๋ค. ์ฃผ๋์ด๋ค์ด ๊ฐ์ฅ ๋ง์ด ์ค์ํ๋ ํจํด์ ์ง์ด๋๋ฆฝ๋๋ค.
ํจ์ 1: ์ธ๋ฑ์ค ์ปฌ๋ผ์ ํจ์๋ ์ฐ์ฐ์ผ๋ก ๊ฐ์ธ๊ธฐ (SARGable ์๋ฐ)
-- [BAD] created_at ์ปฌ๋ผ์ ํจ์๋ก ๊ฐ์ธ๋ฉด ์ธ๋ฑ์ค๋ฅผ ํ์ง ๋ชปํ๊ณ ์ ์ฒด ํ
์ด๋ธ ์ค์บ!
SELECT * FROM orders WHERE DATE(created_at) = '2026-08-19';
-- [GOOD] ์ธ๋ฑ์ค ์ปฌ๋ผ์ ์์ํ๊ฒ ๋๊ณ ๋ฒ์๋ฅผ ์ง์ ํด์ผ Index Scan ์๋
SELECT * FROM orders
WHERE created_at >= '2026-08-19 00:00:00'
AND created_at < '2026-08-20 00:00:00';
ํจ์ 2: ์๋ฌต์ ํ๋ณํ(Implicit Type Cast)์ ํจ์
์ปฌ๋ผ ํ์
์ VARCHAR์ธ๋ฐ WHERE ์กฐ๊ฑด์ ์ ์ซ์ ๋ฆฌํฐ๋ด WHERE phone_number = 01012345678 ํํ๋ก ๋๊ธฐ๋ฉด, DB๋ ๋ชจ๋ ํ์ ๋ฌธ์์ด์ ์ซ์๋ก ๋ณํํ๋๋ผ ์ธ๋ฑ์ค๋ฅผ ํฌ๊ธฐํฉ๋๋ค. ๋ฐ๋์ ํ์
์ ์ผ์น์ํค์ธ์!
ํจ์ 3: ํต๊ณ ์ ๋ณด์ ๋ ธํํ (Stats Out of Date)
ํ
์ด๋ธ์ ๋๊ท๋ชจ INSERT/DELETE๊ฐ ๋ฐ์ํ๋๋ฐ ํต๊ณ ์ ๋ณด๊ฐ ๊ฐฑ์ ๋์ง ์์ผ๋ฉด, ์ตํฐ๋ง์ด์ ๊ฐ ๋ฉ์ฒญํ ํ๋จ์ ๋ด๋ ค ์ข์ ์ธ๋ฑ์ค๋ฅผ ๋๊ณ ๋ Seq Scan์ ๋๋๋ค. ์ ๊ธฐ์ ์ธ ANALYZE์ ์ ์ ํ Autovacuum ํ๋์ ์ ํ์ด ์๋ ํ์์
๋๋ค.
์๋์ด์ ์ฟผ๋ฆฌ ํ๋ ์ค์ ์ฒดํฌ๋ฆฌ์คํธ
- 1. EXPLAIN (ANALYZE, BUFFERS)๋ก Actual Time๊ณผ Shared Read ๋ฒํผ๋ฅผ ํ์ธํ๋๊ฐ?
- 2. ๋ณตํฉ ์ธ๋ฑ์ค ์์ฑ ์ ์นด๋๋๋ฆฌํฐ(์ ํ๋)๊ฐ ๋์ ์ปฌ๋ผ์ ์์ ๋ฐฐ์นํ๋๊ฐ?
- 3. ํน์ ์ํ๊ฐ ์กฐํ์ฉ์ด๋ผ๋ฉด Partial Index๋ก ํฌ๊ธฐ๋ฅผ ๋ค์ด์ดํธํ๋๊ฐ?
- 4. WHERE ์ ์ข๋ณ์ ํจ์ ๋ํ์ด๋ ์๋ฌต์ ํ๋ณํ์ด ๋ฐ์ํ์ง ์๋๊ฐ?
- 5. ๋ฏธ์ฌ์ฉ ์ธ๋ฑ์ค๊ฐ ์์ฌ INSERT/UPDATE ๋ณ๋ชฉ์ ์ ๋ฐํ๊ณ ์์ง ์์๊ฐ?
๋ฐ์ดํฐ๋ฒ ์ด์ค ํ๋์ ๋ง๋ฒ์ด ์๋๋๋ค. ์ตํฐ๋ง์ด์ ๊ฐ ๋ฐ์ดํฐ๋ฅผ ์ฐพ์๊ฐ๋ ๊ธธ์ ์ ํํ ์ดํดํ๊ณ , ๊ทธ ๊ธธ๋ชฉ์ ์๋ง์ ํ์งํ(์ธ๋ฑ์ค)์ ๊ฝ์์ฃผ๋ ์์ง๋์ด๋ง ์์ ์ด์ฃ . ์ค๋ ๋ฐฐ์ด EXPLAIN ๋ฒํผ ๋ถ์๊ณผ ์ํฉ๋ณ ํน์ ์ธ๋ฑ์ค๋ฅผ ์ฌ๋ฌ๋ถ์ ํ๋ก์ ํธ์ ์ง์ ์ ์ฉํด ๋ณด์ธ์. ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ ๋ก๊ทธ๊ฐ ๊ธฐ์ ์ฒ๋ผ ์ฌ๋ผ์ง๋ ์ง๋ฆฟํจ์ ๋ง๋ณด์๊ฒ ๋ ๊ฒ๋๋ค! ๐
ps. ์ฌ์ค ์์ฆ์ AIํํ ์ฟผ๋ฆฌ์ ์คํ๊ณํ์ ๋์์ ๋ณด์ฌ์ฃผ๋ฉด ์์์ ์ ํ๋ ํด์ค๋๋ค. ใ ใ
์์ง ๋๊ธ์ด ์์ต๋๋ค. ์ฒซ ๋๊ธ์ ๋จ๊ฒจ๋ณด์ธ์!