sql-optimization-patterns
npx skills add https://github.com/icartsh/icartsh_plugin --skill sql-optimization-patterns
Agent 安装分布
Skill 文档
SQL Optimization Patterns
ì²´ê³ì ì¸ ìµì í, ì¬ë°ë¥¸ ì¸ë±ì± ë° ì¿¼ë¦¬ ì¤í ê³í ë¶ìì íµí´ ë린 ë°ì´í°ë² ì´ì¤ 쿼리를 ë²ê°ì²ë¼ ë¹ ë¥¸ ìì ì¼ë¡ ë³ííì¸ì.
ì ì© ì기
- ëë¦¬ê² ì¤íëë 쿼리 ëë²ê¹
- ì±ë¥ì´ ë°ì´ë ë°ì´í°ë² ì´ì¤ ì¤í¤ë§ ì¤ê³
- ì í리ì¼ì´ì ìëµ ìê° ìµì í
- ë°ì´í°ë² ì´ì¤ ë¶í ë° ë¹ì© ì ê°
- ë°ì´í° ì¦ê°ì ë°ë¥¸ íì¥ì± ê°ì
- EXPLAIN 쿼리 ì¤í ê³í ë¶ì
- í¨ì¨ì ì¸ ì¸ë±ì¤ 구í
- N+1 쿼리 문ì í´ê²°
íµì¬ ê°ë (Core Concepts)
1. 쿼리 ì¤í ê³í (EXPLAIN)
EXPLAIN ì¶ë ¥ì ì´í´íë ê²ì ìµì íì 기본ì ëë¤.
PostgreSQL EXPLAIN:
-- 기본 ì¤í ê³í íì¸
EXPLAIN SELECT * FROM users WHERE email = 'user@example.com';
-- ì¤ì ì¤í íµê³ í¬í¨
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = 'user@example.com';
-- ë ë§ì ì¸ë¶ ì 보를 í¬í¨í ìì¸ ì¶ë ¥
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT u.*, o.order_total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > NOW() - INTERVAL '30 days';
주ì ê¹ê² ë´ì¼ í 주ì ì§í:
- Seq Scan: ì ì²´ í ì´ë¸ ì¤ìº (ëì©ë í ì´ë¸ììë ëê° ë림)
- Index Scan: ì¸ë±ì¤ ì¬ì© (ì¢ì)
- Index Only Scan: í ì´ë¸ ì ê·¼ ìì´ ì¸ë±ì¤ë§ ì¬ì© (ê°ì¥ ì¢ì)
- Nested Loop: ì¡°ì¸ ë°©ì (ìì ë°ì´í°ì ìë ê´ì°®ì)
- Hash Join: ì¡°ì¸ ë°©ì (í° ë°ì´í°ì ì ì¢ì)
- Merge Join: ì¡°ì¸ ë°©ì (ì ë ¬ë ë°ì´í°ì ì¢ì)
- Cost: ì¶ì ë 쿼리 ë¹ì© (ë®ììë¡ ì¢ì)
- Rows: ì¶ì ë ë°í í ì
- Actual Time: ì¤ì ì¤í ìê°
2. ì¸ë±ì¤ ì ëµ (Index Strategies)
ì¸ë±ì¤ë ê°ì¥ ê°ë ¥í ìµì í ë구ì ëë¤.
ì¸ë±ì¤ ì í:
- B-Tree: 기본ê°, ë±í¸(=) ë° ë²ì 쿼리ì ì¢ì
- Hash: ë±í¸(=) ë¹êµìë§ ì¬ì©
- GIN: ì ì²´ í ì¤í¸ ê²ì, ë°°ì´ ì¿¼ë¦¬, JSONB
- GiST: 기ííì ë°ì´í°, ì ì²´ í ì¤í¸ ê²ì
- BRIN: ë°ì´í° ê° ìê´ê´ê³ê° ìë ë§¤ì° í° í ì´ë¸ì ìí ë¸ë¡ ë²ì ì¸ë±ì¤
-- íì¤ B-Tree ì¸ë±ì¤
CREATE INDEX idx_users_email ON users(email);
-- ë³µí© ì¸ë±ì¤ (ììê° ì¤ìí©ëë¤!)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- ë¶ë¶ ì¸ë±ì¤ (íì ì¼ë¶ë§ ì¸ë±ì±)
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
-- ííì ì¸ë±ì¤ (í¨ì ê¸°ë° ì¸ë±ì¤)
CREATE INDEX idx_users_lower_email ON users(LOWER(email));
-- 커ë²ë§ ì¸ë±ì¤ (ì¶ê° ì»¬ë¼ í¬í¨)
CREATE INDEX idx_users_email_covering ON users(email)
INCLUDE (name, created_at);
-- ì ì²´ í
ì¤í¸ ê²ì ì¸ë±ì¤
CREATE INDEX idx_posts_search ON posts
USING GIN(to_tsvector('english', title || ' ' || body));
-- JSONB ì¸ë±ì¤
CREATE INDEX idx_metadata ON events USING GIN(metadata);
3. 쿼리 ìµì í í¨í´
SELECT * í¼í기:
-- ëì¨: ë¶íìí 모ë 컬ë¼ì ê°ì ¸ì´
SELECT * FROM users WHERE id = 123;
-- ì¢ì: íìí 컬ë¼ë§ ëª
ì
SELECT id, email, name FROM users WHERE id = 123;
WHERE ì ì í¨ì¨ì ì¬ì©:
-- ëì¨: í¨ì ì¬ì©ì¼ë¡ ì¸ë±ì¤ íì© ë¶ê°
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- ì¢ì: í¨ì ê¸°ë° ì¸ë±ì¤(functional index) ìì± ëë ì íí ì¼ì¹ ì¬ì©
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
-- ê·¸ ë¤ì:
SELECT * FROM users WHERE LOWER(email) = 'user@example.com';
-- ëë ë°ì´í°ë¥¼ ì ê·ííì¬ ì ì¥
SELECT * FROM users WHERE email = 'user@example.com';
JOIN ìµì í:
-- ëì¨: ì¹´í
ìì ê³± ìì± í íí°ë§
SELECT u.name, o.total
FROM users u, orders o
WHERE u.id = o.user_id AND u.created_at > '2024-01-01';
-- ì¢ì: ì¡°ì¸ ì íí°ë§
SELECT u.name, o.total
FROM users u
JOIN orders o ON u.id = o.user_id
WHERE u.created_at > '2024-01-01';
-- ë ì¢ì: ë í
ì´ë¸ 모ë ì¬ì íí°ë§
SELECT u.name, o.total
FROM (SELECT * FROM users WHERE created_at > '2024-01-01') u
JOIN orders o ON u.id = o.user_id;
ìµì í í¨í´ (Optimization Patterns)
í¨í´ 1: N+1 쿼리 ì ê±°
문ì : N+1 쿼리 ìí° í¨í´
# ëì¨: N+1ê°ì 쿼리를 ì¤íí¨
users = db.query("SELECT * FROM users LIMIT 10")
for user in users:
orders = db.query("SELECT * FROM orders WHERE user_id = ?", user.id)
# orders ì²ë¦¬
í´ê²°ì± : JOIN ëë ë°°ì¹ ë¡ë©(Batch Loading) ì¬ì©
-- í´ê²°ì±
1: JOIN ì¬ì©
SELECT
u.id, u.name,
o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN (1, 2, 3, 4, 5);
-- í´ê²°ì±
2: ë°°ì¹ ì¿¼ë¦¬
SELECT * FROM orders
WHERE user_id IN (1, 2, 3, 4, 5);
# ì¢ì: JOIN ëë ë°°ì¹ ë¡ë를 íµí ë¨ì¼ 쿼리 ì¤í
# JOIN ì¬ì© ì
results = db.query("""
SELECT u.id, u.name, o.id as order_id, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.id IN (1, 2, 3, 4, 5)
""")
# ëë ë°°ì¹ ë¡ë(Batch load)
users = db.query("SELECT * FROM users LIMIT 10")
user_ids = [u.id for u in users]
orders = db.query(
"SELECT * FROM orders WHERE user_id IN (?)",
user_ids
)
# user_idë³ë¡ orders 그룹í
orders_by_user = {}
for order in orders:
orders_by_user.setdefault(order.user_id, []).append(order)
í¨í´ 2: íì´ì§ë¤ì´ì (Pagination) ìµì í
ëì¨: ëì©ë í ì´ë¸ììì OFFSET ì¬ì©
-- í° offset ê°ìì ìë ì í ë°ì
SELECT * FROM users
ORDER BY created_at DESC
LIMIT 20 OFFSET 100000; -- ë§¤ì° ë림!
ì¢ì: 커ì ê¸°ë° íì´ì§ë¤ì´ì (Cursor-Based Pagination)
-- í¨ì¬ ë¹ ë¦: 커ì(ë§ì§ë§ íì¸ë ID) ì¬ì©
SELECT * FROM users
WHERE created_at < '2024-01-15 10:30:00' -- ë§ì§ë§ 커ì
ORDER BY created_at DESC
LIMIT 20;
-- ë³µí© ì ë ¬ ì
SELECT * FROM users
WHERE (created_at, id) < ('2024-01-15 10:30:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 20;
-- ì¸ë±ì¤ íì
CREATE INDEX idx_users_cursor ON users(created_at DESC, id DESC);
í¨í´ 3: í¨ì¨ì ì¸ ì§ê³ (Aggregate Efficiently)
COUNT 쿼리 ìµì í:
-- ëì¨: 모ë íì ì¹´ì´í¸í¨
SELECT COUNT(*) FROM orders; -- í° í
ì´ë¸ìì ë림
-- ì¢ì: ê·¼ì¬ì¹ë¥¼ ìí ì¶ì ì¹(estimates) ì¬ì©
SELECT reltuples::bigint AS estimate
FROM pg_class
WHERE relname = 'orders';
-- ì¢ì: ì¹´ì´í¸ ì íí°ë§ ì ì©
SELECT COUNT(*) FROM orders
WHERE created_at > NOW() - INTERVAL '7 days';
-- ë ì¢ì: ì¸ë±ì¤ ì ì© ì¤ìº(index-only scan) íì©
CREATE INDEX idx_orders_created ON orders(created_at);
SELECT COUNT(*) FROM orders
WHERE created_at > NOW() - INTERVAL '7 days';
GROUP BY ìµì í:
-- ëì¨: 그룹í í íí°ë§
SELECT user_id, COUNT(*) as order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) > 10;
-- ì¢ì: ê°ë¥í ê²½ì° ë¨¼ì íí°ë§ í 그룹í
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE status = 'completed'
GROUP BY user_id
HAVING COUNT(*) > 10;
-- ê°ì¥ ì¢ì: 커ë²ë§ ì¸ë±ì¤ íì©
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
í¨í´ 4: ìë¸ì¿¼ë¦¬ ìµì í
ìê´ ìë¸ì¿¼ë¦¬(Correlated Subqueries) ë³í:
-- ëì¨: ìê´ ìë¸ì¿¼ë¦¬ (ê° íë§ë¤ ì¤íë¨)
SELECT u.name, u.email,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) as order_count
FROM users u;
-- ì¢ì: ì§ê³ê° í¬í¨ë JOIN
SELECT u.name, u.email, COUNT(o.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name, u.email;
-- ë ì¢ì: ìëì° í¨ì ì¬ì©
SELECT DISTINCT ON (u.id)
u.name, u.email,
COUNT(o.id) OVER (PARTITION BY u.id) as order_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
ê°ë ì±ì ìí CTE ì¬ì©:
-- ê³µíµ í
ì´ë¸ ìë³ì(CTE) íì©
WITH recent_users AS (
SELECT id, name, email
FROM users
WHERE created_at > NOW() - INTERVAL '30 days'
),
user_order_counts AS (
SELECT user_id, COUNT(*) as order_count
FROM orders
WHERE created_at > NOW() - INTERVAL '30 days'
GROUP BY user_id
)
SELECT ru.name, ru.email, COALESCE(uoc.order_count, 0) as orders
FROM recent_users ru
LEFT JOIN user_order_counts uoc ON ru.id = uoc.user_id;
í¨í´ 5: ë°°ì¹ ìì (Batch Operations)
ë°°ì¹ INSERT:
-- ëì¨: ë¤ìì ê°ë³ insert ìí
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
INSERT INTO users (name, email) VALUES ('Bob', 'bob@example.com');
INSERT INTO users (name, email) VALUES ('Carol', 'carol@example.com');
-- ì¢ì: ë°°ì¹ insert
INSERT INTO users (name, email) VALUES
('Alice', 'alice@example.com'),
('Bob', 'bob@example.com'),
('Carol', 'carol@example.com');
-- ë ì¢ì: ëë insert ì COPY íì© (PostgreSQL)
COPY users (name, email) FROM '/tmp/users.csv' CSV HEADER;
ë°°ì¹ UPDATE:
-- ëì¨: ë°ë³µë¬¸ ë´ ì
ë°ì´í¸
UPDATE users SET status = 'active' WHERE id = 1;
UPDATE users SET status = 'active' WHERE id = 2;
-- ... ë§ì ID를 ë°ë³µ
-- ì¢ì: IN ì ì íì©í ë¨ì¼ UPDATE
UPDATE users
SET status = 'active'
WHERE id IN (1, 2, 3, 4, 5, ...);
-- ë ì¢ì: ëë ë°°ì¹ ì ìì í
ì´ë¸ íì©
CREATE TEMP TABLE temp_user_updates (id INT, new_status VARCHAR);
INSERT INTO temp_user_updates VALUES (1, 'active'), (2, 'active'), ...;
UPDATE users u
SET status = t.new_status
FROM temp_user_updates t
WHERE u.id = t.id;
ê³ ê¸ ê¸°ë² (Advanced Techniques)
구체íë ë·° (Materialized Views)
ë¹ì©ì´ ë§ì´ ëë 쿼리를 미리 ê³ì°í´ ë¡ëë¤.
-- 구체íë ë·° ìì±
CREATE MATERIALIZED VIEW user_order_summary AS
SELECT
u.id,
u.name,
COUNT(o.id) as total_orders,
SUM(o.total) as total_spent,
MAX(o.created_at) as last_order_date
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;
-- 구체íë ë·°ì ì¸ë±ì¤ ì¶ê°
CREATE INDEX idx_user_summary_spent ON user_order_summary(total_spent DESC);
-- 구체íë ë·° ê°±ì
REFRESH MATERIALIZED VIEW user_order_summary;
-- ëì ê°±ì (PostgreSQL, ë½ ìµìí)
REFRESH MATERIALIZED VIEW CONCURRENTLY user_order_summary;
-- 구체íë ë·° 쿼리 (ë§¤ì° ë¹ ë¦)
SELECT * FROM user_order_summary
WHERE total_spent > 1000
ORDER BY total_spent DESC;
íí°ì ë (Partitioning)
ì±ë¥ í¥ìì ìí´ ëí í ì´ë¸ì ëëëë¤.
-- ë ì§ë³ ë²ì íí°ì
ë (PostgreSQL)
CREATE TABLE orders (
id SERIAL,
user_id INT,
total DECIMAL,
created_at TIMESTAMP
) PARTITION BY RANGE (created_at);
-- íí°ì
ìì±
CREATE TABLE orders_2024_q1 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE orders_2024_q2 PARTITION OF orders
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');
-- 쿼리ë ìëì¼ë¡ ì ì í íí°ì
ì ì¬ì©í¨
SELECT * FROM orders
WHERE created_at BETWEEN '2024-02-01' AND '2024-02-28';
-- orders_2024_q1 íí°ì
ë§ ì¤ìºí¨
쿼리 íí¸ ë° ìµì í
-- ì¸ë±ì¤ ì¬ì© ê°ì (MySQL)
SELECT * FROM users
USE INDEX (idx_users_email)
WHERE email = 'user@example.com';
-- ë³ë ¬ 쿼리 (PostgreSQL)
SET max_parallel_workers_per_gather = 4;
SELECT * FROM large_table WHERE condition;
-- ì¡°ì¸ íí¸ (PostgreSQL)
SET enable_nestloop = OFF; -- hash join ëë merge join ê°ì
ëª¨ë² ì¬ë¡ (Best Practices)
- ì ë³ì ì¸ ì¸ë±ì±: ì¸ë±ì¤ê° ë무 ë§ì¼ë©´ ì°ê¸° ìì ì´ ëë ¤ì§ëë¤.
- 쿼리 ì±ë¥ 모ëí°ë§: ë린 쿼리 ë¡ê·¸(slow query logs)를 íì©íì¸ì.
- íµê³ ì ë³´ ì ë°ì´í¸ ì ì§: ì 기ì ì¼ë¡ ANALYZE를 ì¤ííì¸ì.
- ì ì í ë°ì´í° íì ì¬ì©: ìì íì ì¼ìë¡ ì±ë¥ì´ ì¢ìµëë¤.
- ì¬ë ¤ ê¹ì ì ê·í: ì ê·íì ì±ë¥ ì¬ì´ì ê· íì ë§ì¶ì¸ì.
- ì주 ì ê·¼íë ë°ì´í° ìºì±: ì í리ì¼ì´ì ë 벨 ìºì±ì íì©íì¸ì.
- 커ë¥ì íë§ (Connection Pooling): ë°ì´í°ë² ì´ì¤ ì°ê²°ì ì¬ì¬ì©íì¸ì.
- ì 기ì ì¸ ì ì§ë³´ì: VACUUM, ANALYZE, ì¸ë±ì¤ ì¬ë¹ë ë±ì ìííì¸ì.
-- íµê³ ì
ë°ì´í¸
ANALYZE users;
ANALYZE VERBOSE orders;
-- Vacuum (PostgreSQL)
VACUUM ANALYZE users;
VACUUM FULL users; -- ê³µê° íì (í
ì´ë¸ ë½ ë°ì)
-- ì¸ë±ì¤ ì¬êµ¬ì±
REINDEX INDEX idx_users_email;
REINDEX TABLE users;
ì주 ë°ìíë 문ì (Common Pitfalls)
- ê³¼ëí ì¸ë±ì±: ê° ì¸ë±ì¤ë INSERT/UPDATE/DELETE ìë를 ë¦ì¶¥ëë¤.
- ì¬ì©ëì§ ìë ì¸ë±ì¤: ê³µê°ì ëë¹íê³ ì°ê¸° ì±ë¥ì ì íìíµëë¤.
- ì¸ë±ì¤ ëë½: 쿼리 ìë ì í, ì ì²´ í ì´ë¸ ì¤ìº ì ë°.
- ììì íì ë³í: ì¸ë±ì¤ ì¬ì©ì ë°©í´í©ëë¤.
- OR ì¡°ê±´: ì¸ë±ì¤ë¥¼ í¨ì¨ì ì¼ë¡ ì¬ì©í기 ì´ë µê² ë§ë¤ ì ììµëë¤.
- ìì¼ëì¹´ëê° ìì ë¶ì LIKE:
LIKE '%abc'ë ì¸ë±ì¤ë¥¼ í ì ììµëë¤. - WHERE ì ì í¨ì: ê¸°ë¥ ê¸°ë° ì¸ë±ì¤ê° ìë¤ë©´ ì¸ë±ì¤ ì¬ì©ì ë°©í´í©ëë¤.
쿼리 모ëí°ë§
-- ë린 쿼리 찾기 (PostgreSQL)
SELECT query, calls, total_time, mean_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- ëë½ë ì¸ë±ì¤ 찾기 (PostgreSQL)
SELECT
schemaname,
tablename,
seq_scan,
seq_tup_read,
idx_scan,
seq_tup_read / seq_scan AS avg_seq_tup_read
FROM pg_stat_user_tables
WHERE seq_scan > 0
ORDER BY seq_tup_read DESC
LIMIT 10;
-- ì¬ì©ëì§ ìë ì¸ë±ì¤ 찾기 (PostgreSQL)
SELECT
schemaname,
tablename,
indexname,
idx_scan,
idx_tup_read,
idx_tup_fetch
FROM pg_stat_user_indexes
WHERE idx_scan = 0
ORDER BY pg_relation_size(indexrelid) DESC;
리ìì¤
- references/postgres-optimization-guide.md: PostgreSQL ì ì© ìµì í
- references/mysql-optimization-guide.md: MySQL/MariaDB ìµì í
- references/query-plan-analysis.md: EXPLAIN ì¤í ê³í ì¬ì¸µ ë¶ì
- assets/index-strategy-checklist.md: ì¸ë±ì¤ ìì± ìì ë° ë°©ë²
- assets/query-optimization-checklist.md: ë¨ê³ë³ ìµì í ê°ì´ë
- scripts/analyze-slow-queries.sql: ë°ì´í°ë² ì´ì¤ ë´ ë린 쿼리 ìë³
- scripts/index-recommendations.sql: ì¸ë±ì¤ ê¶ì¥ ì¬í ìì±