| name | database-expert-advisor |
| description | Database design, optimization, and operations expert |
Database Expert Advisor
Overview
DB Expert Advisor๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ, ์ต์ ํ, ์ด์ ์ ๋ฐ์ ์ง์ํ๋ ์ข
ํฉ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ปจ์คํ
์คํฌ์
๋๋ค. PostgreSQL, MySQL, MongoDB, Redis ๋ฑ ์ฃผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์์คํ
์ ๋ํ ์ ๋ฌธ ์ง์๊ณผ 45๊ฐ ์ด์์ ํ์ ๋
ผ๋ฌธ, ๊ณต์ ๋ฌธ์, ์ฐ์
๋ฒ ์คํธ ํ๋ํฐ์ค๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ๊ตฌ์ถ๋์์ต๋๋ค.
ํต์ฌ ์ญ๋
- ์ฟผ๋ฆฌ ์ต์ ํ: EXPLAIN ๋ถ์, ์ธ๋ฑ์ค ์ ๋ต, ์คํ ๊ณํ ๊ฐ์
- ์คํค๋ง ์ค๊ณ: ER ๋ชจ๋ธ๋ง, ์ ๊ทํ(1NF-BCNF), ํํฐ์
๋ ์ ๋ต
- ์ฑ๋ฅ ํ๋: ๋ณ๋ชฉ ๊ตฌ๊ฐ ๋ถ์, ํธ๋์ญ์
์ต์ ํ, ์บ์ฑ ์ ๋ต
- ๋ณด์ ๊ด๋ฆฌ: ์ ๊ทผ ์ ์ด, ์ํธํ, SQL Injection ๋ฐฉ์ง, ํ๊ตญ ๋ฒ๊ท ์ค์
- ๋ง์ด๊ทธ๋ ์ด์
: ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ํ, ์ค๋ฉ, ๋ณต์ ์ค๊ณ
- ํธ๋ฌ๋ธ์ํ
: ๋ฐ๋๋ฝ ํด๊ฒฐ, ๋ฉ๋ชจ๋ฆฌ ๋์, ์ฑ๋ฅ ์ ํ ์ง๋จ
์ง์ ๋ฐ์ดํฐ๋ฒ ์ด์ค
| ์นดํ
๊ณ ๋ฆฌ | ๋ฐ์ดํฐ๋ฒ ์ด์ค | ์ฃผ์ ์ฉ๋ |
|---|
| ๊ด๊ณํ | PostgreSQL, MySQL, MariaDB | OLTP, ํธ๋์ญ์
์ฒ๋ฆฌ |
| NoSQL | MongoDB, Redis, Cassandra, DynamoDB | ๋์ฉ๋ ๋ฐ์ดํฐ, ์บ์ฑ, ์๊ณ์ด |
| NewSQL | CockroachDB, TiDB, YugabyteDB | ๋ถ์ฐ SQL, ๊ธ๋ก๋ฒ ํ์ฅ |
| ์๊ณ์ด | TimescaleDB, InfluxDB | ๋ชจ๋ํฐ๋ง, IoT ๋ฐ์ดํฐ |
| ๊ทธ๋ํ | Neo4j | ๊ด๊ณ ๋ถ์, ์ถ์ฒ ์์คํ
|
When to Use
โ
์ด ์คํฌ์ ์ฌ์ฉํด์ผ ํ๋ ๊ฒฝ์ฐ
1. ์ฑ๋ฅ ๋ฌธ์ ํด๊ฒฐ
- ๋๋ฆฐ ์ฟผ๋ฆฌ ๋ถ์ ๋ฐ ์ต์ ํ (์๋ต ์๊ฐ > 1์ด)
- ๋์ CPU/๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ๋ฅ ์์ธ ์ง๋จ
- ๋์ ์ ์ ์ฆ๊ฐ๋ก ์ธํ ๋ณ๋ชฉ ํ์
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ๋ฝ(Lock) ๋ฐ ๋ฐ๋๋ฝ(Deadlock) ๋ฌธ์
์์ ์ง๋ฌธ:
"PostgreSQL์์ ํน์ ์ฟผ๋ฆฌ๊ฐ 10์ด ์ด์ ๊ฑธ๋ฆฝ๋๋ค.
EXPLAIN ANALYZE ๊ฒฐ๊ณผ๋ฅผ ๋ถ์ํด์ฃผ์ธ์."
2. ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ
- ์ ๊ท ์ ํ๋ฆฌ์ผ์ด์
์ ์คํค๋ง ์ค๊ณ
- ๊ธฐ์กด ์คํค๋ง์ ์ ๊ทํ/๋น์ ๊ทํ ๊ฒํ
- ์ธ๋ฑ์ค ์ ๋ต ์๋ฆฝ (B-tree, Hash, GIN, GiST)
- ํํฐ์
๋ ๋ฐ ์ค๋ฉ ์ค๊ณ
์์ ์ง๋ฌธ:
"์ ์์๊ฑฐ๋ ์ฃผ๋ฌธ ์์คํ
์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ค๊ณํด์ฃผ์ธ์.
ํ๋ฃจ 10๋ง ๊ฑด ์ด์์ ์ฃผ๋ฌธ์ ์ฒ๋ฆฌํด์ผ ํฉ๋๋ค."
3. ๋ง์ด๊ทธ๋ ์ด์
๋ฐ ์ ํ
- MySQL โ PostgreSQL ์ ํ ๊ณํ
- ๋ชจ๋๋ฆฌ์ โ ๋ง์ดํฌ๋ก์๋น์ค DB ๋ถ๋ฆฌ
- ์จํ๋ ๋ฏธ์ค โ ํด๋ผ์ฐ๋ ์ด์
- ์ค๋ฉ ๋๋ ๋ณต์ ๊ตฌ์ฑ
์์ ์ง๋ฌธ:
"MySQL 5.7์์ PostgreSQL 16์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ๋ ค๊ณ ํฉ๋๋ค.
์ฃผ์์ฌํญ๊ณผ ๋จ๊ณ๋ณ ๊ณํ์ ์๋ ค์ฃผ์ธ์."
4. ๋ณด์ ๋ฐ ๊ท์ ์ค์
- ํ๊ตญ ๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ ์ค์ ๋ฐฉ์
- ์ ์๊ธ์ต๊ฑฐ๋๋ฒ ์๊ตฌ์ฌํญ ์ถฉ์กฑ
- SQL Injection ๋ฐฉ์ง
- ์ํธํ ๋ฐ ์ ๊ทผ ์ ์ด ์ค์
์์ ์ง๋ฌธ:
"๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ์ ๋ฐ๋ผ ๊ณ ์ ์๋ณ์ ๋ณด๋ฅผ ์ํธํํด์ผ ํฉ๋๋ค.
PostgreSQL์์ ์ด๋ป๊ฒ ๊ตฌํํ๋์?"
5. ๋์ฉ๋ ๋ฐ์ดํฐ ์ฒ๋ฆฌ
- ์์ต ๊ฑด ์ด์์ ๋ฐ์ดํฐ ๊ด๋ฆฌ
- ์ค์๊ฐ ๋ถ์ ์ฟผ๋ฆฌ ์ต์ ํ
- ๋ฐฐ์น ์ฒ๋ฆฌ ์ฑ๋ฅ ๊ฐ์
- ์๊ณ์ด ๋ฐ์ดํฐ ์ํคํ
์ฒ
์์ ์ง๋ฌธ:
"1์ต ๊ฑด ์ด์์ ๋ก๊ทธ ๋ฐ์ดํฐ๋ฅผ ์ ์ฅํ๊ณ ์ค์๊ฐ ๋ถ์ํด์ผ ํฉ๋๋ค.
TimescaleDB vs InfluxDB ์ค ์ด๋ค ๊ฒ์ด ์ ํฉํ๊ฐ์?"
โ ์ด ์คํฌ์ด ์ ํฉํ์ง ์์ ๊ฒฝ์ฐ
- ๋จ์ SQL ๋ฌธ๋ฒ ์ง๋ฌธ (๊ณต์ ๋ฌธ์ ์ฐธ์กฐ)
- ํ๋ก๊ทธ๋๋ฐ ์ธ์ด๋ณ ORM ์ฌ์ฉ๋ฒ (๋ณ๋ ์คํฌ ์ฌ์ฉ)
- ํด๋ผ์ฐ๋ ์๋น์ค๋ณ UI ์กฐ์ (๊ณต์ ๊ฐ์ด๋ ์ฐธ์กฐ)
- ํน์ DB ๋ฒค๋์ ์ต์ ์
๋ฐ์ดํธ ์ ๋ณด (์น ๊ฒ์ ๊ถ์ฅ)
Core Capabilities
1. ์ฟผ๋ฆฌ ์ต์ ํ ์์ง
EXPLAIN ๋ถ์ ๋ฐ ํด์
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT o.order_id, c.name, SUM(oi.quantity * oi.price) AS total
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.order_id = oi.order_id
WHERE o.created_at >= '2025-01-01'
GROUP BY o.order_id, c.name
HAVING SUM(oi.quantity * oi.price) > 1000000;
๋ถ์ ํญ๋ชฉ:
- Seq Scan vs Index Scan: ์ ์ฒด ํ
์ด๋ธ ์ค์บ์ธ์ง ์ธ๋ฑ์ค ์ฌ์ฉ์ธ์ง
- Cost: ์ถ์ ๋น์ฉ (startup cost, total cost)
- Rows: ์์ ํ ์ vs ์ค์ ํ ์
- Buffers: ๊ณต์ ๋ฒํผ ์ฌ์ฉ๋ (hit, read, written)
- Planning Time vs Execution Time: ๊ณํ ์๊ฐ vs ์คํ ์๊ฐ
์ต์ ํ ์ ๋ต:
- ์ธ๋ฑ์ค ์ถ๊ฐ (๋ณตํฉ ์ธ๋ฑ์ค, ๋ถ๋ถ ์ธ๋ฑ์ค)
- JOIN ์์ ๋ณ๊ฒฝ
- ์๋ธ์ฟผ๋ฆฌ โ CTE ๋๋ JOIN์ผ๋ก ๋ณ๊ฒฝ
- ํต๊ณ ์ ๋ณด ์
๋ฐ์ดํธ (
ANALYZE)
์ธ๋ฑ์ค ์ ๋ต
| ์ธ๋ฑ์ค ์ ํ | ์ฌ์ฉ ์ฌ๋ก | ๋ฐ์ดํฐ๋ฒ ์ด์ค | ์ฑ๋ฅ ํน์ฑ |
|---|
| B-tree | ๋ฒ์ ๊ฒ์, ์ ๋ ฌ | PostgreSQL, MySQL | ๊ท ํ ์กํ ์ฑ๋ฅ |
| Hash | ๋๋ฑ ๋น๊ต (=) | PostgreSQL, Redis | ๋น ๋ฅธ ์กฐํ, ๋ฒ์ ๋ถ๊ฐ |
| GIN | ์ ๋ฌธ ๊ฒ์, ๋ฐฐ์ด, JSONB | PostgreSQL | ๋ณต์กํ ๊ฒ์, ๋๋ฆฐ ์ฝ์
|
| GiST | ์ง๋ฆฌ ์ ๋ณด, ๋ฒ์ ํ์
| PostgreSQL | ๋ฒ์ฉ ์ธ๋ฑ์ค ํ๋ ์์ํฌ |
| BRIN | ์๊ณ์ด, ์์ฐจ ๋ฐ์ดํฐ | PostgreSQL | ์ ์ ๊ณต๊ฐ, ๋์ฉ๋ |
| Full-Text | ์์ฐ์ด ๊ฒ์ | MySQL, PostgreSQL | ํ
์คํธ ๊ฒ์ ์ต์ ํ |
์ธ๋ฑ์ค ์ค๊ณ ์์น:
โ
DO:
- WHERE ์ ์ ์์ฃผ ์ฌ์ฉ๋๋ ์ปฌ๋ผ์ ์ธ๋ฑ์ค
- JOIN ํค ์ปฌ๋ผ์ ์ธ๋ฑ์ค
- ORDER BY, GROUP BY ์ปฌ๋ผ์ ์ธ๋ฑ์ค
- ๋ณตํฉ ์ธ๋ฑ์ค๋ ์ ํ๋ ๋์ ์ปฌ๋ผ์ ์์ ๋ฐฐ์น
โ DON'T:
- ๋ชจ๋ ์ปฌ๋ผ์ ๋ฌด๋ถ๋ณํ ์ธ๋ฑ์ค (์ฐ๊ธฐ ์ฑ๋ฅ ์ ํ)
- ์ ํ๋ ๋ฎ์ ์ปฌ๋ผ ์ธ๋ฑ์ค (Boolean ๋ฑ)
- ์์ฃผ ๋ณ๊ฒฝ๋๋ ์ปฌ๋ผ์ ๊ณผ๋ํ ์ธ๋ฑ์ค
- ์ฌ์ฉํ์ง ์๋ ์ธ๋ฑ์ค ์ ์ง (๊ณต๊ฐ ๋ญ๋น)
2. ์คํค๋ง ์ค๊ณ ๋ฐ ์ ๊ทํ
์ ๊ทํ ๋จ๊ณ๋ณ ๊ฐ์ด๋
์ 1์ ๊ทํ (1NF): ์์์ฑ
CREATE TABLE orders (
order_id INT,
product_names TEXT
);
CREATE TABLE orders (
order_id INT PRIMARY KEY
);
CREATE TABLE order_items (
order_id INT REFERENCES orders(order_id),
product_name VARCHAR(100),
PRIMARY KEY (order_id, product_name)
);
์ 2์ ๊ทํ (2NF): ๋ถ๋ถ ์ข
์ ์ ๊ฑฐ
CREATE TABLE order_items (
order_id INT,
product_id INT,
product_name VARCHAR(100),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100)
);
CREATE TABLE order_items (
order_id INT,
product_id INT REFERENCES products(product_id),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
์ 3์ ๊ทํ (3NF): ์ดํ ์ข
์ ์ ๊ฑฐ
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
address VARCHAR(200),
city VARCHAR(50)
);
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
address VARCHAR(200),
city VARCHAR(50)
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT REFERENCES customers(customer_id)
);
BCNF (Boyce-Codd ์ ๊ทํ): ๋ชจ๋ ๊ฒฐ์ ์๊ฐ ํ๋ณดํค
CREATE TABLE courses (
student_id INT,
course_name VARCHAR(100),
instructor VARCHAR(100),
PRIMARY KEY (student_id, course_name)
);
CREATE TABLE instructors (
course_name VARCHAR(100) PRIMARY KEY,
instructor VARCHAR(100)
);
CREATE TABLE enrollments (
student_id INT,
course_name VARCHAR(100) REFERENCES instructors(course_name),
PRIMARY KEY (student_id, course_name)
);
๋น์ ๊ทํ ์ ๋ต (์ฑ๋ฅ ์ต์ ํ)
์ธ์ ๋น์ ๊ทํํ๋๊ฐ:
- ์ฝ๊ธฐ ์ฑ๋ฅ์ด ๋งค์ฐ ์ค์ํ ๊ฒฝ์ฐ
- JOIN ๋น์ฉ์ด ๊ณผ๋ํ ๊ฒฝ์ฐ
- ์ง๊ณ ์ฟผ๋ฆฌ๊ฐ ๋น๋ฒํ ๊ฒฝ์ฐ
- ๋ฐ์ดํฐ ์ผ๊ด์ฑ๋ณด๋ค ์ฑ๋ฅ ์ฐ์ ์ธ ๊ฒฝ์ฐ
๋น์ ๊ทํ ๊ธฐ๋ฒ:
CREATE TABLE orders (
order_id INT PRIMARY KEY,
total_amount DECIMAL(10,2),
item_count INT
);
CREATE MATERIALIZED VIEW daily_sales_summary AS
SELECT
DATE(created_at) AS sale_date,
SUM(total_amount) AS total_sales,
COUNT(*) AS order_count
FROM orders
GROUP BY DATE(created_at);
REFRESH MATERIALIZED VIEW daily_sales_summary;
CREATE TABLE logs (
log_id BIGSERIAL,
created_at TIMESTAMP NOT NULL,
message TEXT
) PARTITION BY RANGE (created_at);
CREATE TABLE logs_2025_01 PARTITION OF logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
CREATE TABLE logs_2025_02 PARTITION OF logs
FOR VALUES FROM () ();
3. ํธ๋์ญ์
๋ฐ ๋์์ฑ ์ ์ด
Isolation Level ๋น๊ต
| Isolation Level | Dirty Read | Non-Repeatable Read | Phantom Read | ์ฑ๋ฅ | ์ฌ์ฉ ์ฌ๋ก |
|---|
| Read Uncommitted | ๋ฐ์ | ๋ฐ์ | ๋ฐ์ | ์ต๊ณ | ๋ก๊ทธ ์์ง (์ ํ๋ ๋ ์ค์) |
| Read Committed | ๋ฐฉ์ง | ๋ฐ์ | ๋ฐ์ | ๋์ | ๋๋ถ๋ถ์ ์ ํ๋ฆฌ์ผ์ด์
(๊ธฐ๋ณธ๊ฐ) |
| Repeatable Read | ๋ฐฉ์ง | ๋ฐฉ์ง | ๋ฐ์ | ์ค๊ฐ | ๋ฆฌํฌํธ ์์ฑ, ์ผ๊ด๋ ์ฝ๊ธฐ ํ์ |
| Serializable | ๋ฐฉ์ง | ๋ฐฉ์ง | ๋ฐฉ์ง | ๋ฎ์ | ๊ธ์ต ๊ฑฐ๋, ์ฌ๊ณ ๊ด๋ฆฌ |
๋ฐ๋๋ฝ ํด๊ฒฐ ์ ๋ต
๋ฐ๋๋ฝ ์์:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 2;
UPDATE accounts SET balance = balance + 50 WHERE id = 1;
COMMIT;
ํด๊ฒฐ ๋ฐฉ๋ฒ:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
SET lock_timeout = '5s';
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
UPDATE accounts
SET balance balance , version version
id version ;
MVCC (Multi-Version Concurrency Control)
PostgreSQL MVCC ๋์:
BEGIN;
SELECT * FROM products WHERE id = 1;
BEGIN;
UPDATE products SET price = 12000 WHERE id = 1;
COMMIT;
SELECT * FROM products WHERE id = 1;
COMMIT;
VACUUM ํ์์ฑ:
VACUUM ANALYZE products;
SELECT * FROM pg_settings WHERE name LIKE 'autovacuum%';
SELECT relname, last_vacuum, last_autovacuum, n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
4. NoSQL ๋ฐ์ดํฐ๋ฒ ์ด์ค ๊ฐ์ด๋
MongoDB ์คํค๋ง ์ค๊ณ
Embedded vs Referenced:
{
"_id": ObjectId("..."),
"title": "MongoDB Best Practices",
"author": {
"name": "John Doe",
"email": "john@example.com"
},
"comments": [
{ "user": "Alice", "text": "Great post!" },
{ "user": "Bob", "text": "Very helpful." }
]
}
{
"_id": ObjectId("..."),
"title": "MongoDB Best Practices",
"author_id": ObjectId("...")
}
{
"_id": ObjectId("..."),
"name": "John Doe",
"email": "john@example.com"
}
{
"_id": ObjectId("..."),
"post_id": ObjectId("..."),
"user": "Alice",
"text": "Great post!"
}
์ธ๋ฑ์ฑ ์ ๋ต:
db.users.createIndex({ email: 1 })
db.orders.createIndex({ customer_id: 1, created_at: -1 })
db.articles.createIndex({ title: "text", content: "text" })
db.stores.createIndex({ location: "2dsphere" })
db.orders.createIndex(
{ created_at: 1 },
{ partialFilterExpression: { status: "completed" } }
)
Aggregation Pipeline:
db.orders.aggregate([
{ $match: { created_at: { $gte: ISODate("2025-01-01") } } },
{ $lookup: {
from: "customers",
localField: "customer_id",
foreignField: "_id",
as: "customer"
}},
{ $unwind: "$customer" },
{ $group: {
_id: "$customer.city",
total_sales: { $sum: "$total_amount" },
order_count: { $sum: 1 }
}},
{ $sort: { total_sales: -1 } },
{ $limit: 10 }
])
Redis ์ฌ์ฉ ํจํด
1. ์บ์ฑ:
import redis
import json
r = redis.Redis(host='localhost', port=6379, db=0)
def get_user(user_id):
cached = r.get(f"user:{user_id}")
if cached:
return json.loads(cached)
user = db.query("SELECT * FROM users WHERE id = ?", user_id)
r.setex(f"user:{user_id}", 3600, json.dumps(user))
return user
2. ์ธ์
์ ์ฅ:
session_id = "sess_abc123"
session_data = {
"user_id": 42,
"username": "john",
"login_time": "2025-01-15T10:30:00"
}
r.setex(f"session:{session_id}", 1800, json.dumps(session_data))
session = r.get(f"session:{session_id}")
r.expire(f"session:{session_id}", 1800)
3. Rate Limiting:
def is_rate_limited(user_id, limit=100, window=60):
"""1๋ถ ๋์ ์ต๋ 100ํ ์์ฒญ ํ์ฉ"""
key = f"rate:{user_id}"
count = r.incr(key)
if count == 1:
r.expire(key, window)
return count > limit
4. ๋ฆฌ๋๋ณด๋ (Sorted Set):
r.zadd("leaderboard", {"player1": 1000, "player2": 1500})
top_players = r.zrevrange("leaderboard", 0, 9, withscores=True)
rank = r.zrevrank("leaderboard", "player1")
5. ๋ณด์ ๋ฐ ๊ท์ ์ค์
SQL Injection ๋ฐฉ์ง
user_input = request.GET['username']
query = f"SELECT * FROM users WHERE username = '{user_input}'"
cursor.execute(query)
user_input = request.GET['username']
query = "SELECT * FROM users WHERE username = %s"
cursor.execute(query, (user_input,))
from sqlalchemy import select
stmt = select(User).where(User.username == user_input)
result = session.execute(stmt)
ํ๊ตญ ๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ ์ค์
๊ณ ์ ์๋ณ์ ๋ณด ์ํธํ (ํ์):
CREATE EXTENSION IF NOT EXISTS pgcrypto;
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(100),
resident_number BYTEA
);
INSERT INTO users (name, resident_number)
VALUES ('ํ๊ธธ๋', pgp_sym_encrypt('901231-1234567', 'encryption_key'));
SELECT
name,
pgp_sym_decrypt(resident_number, 'encryption_key') AS resident_number
FROM users;
์ ๊ทผ ๋ก๊ทธ ๋ณด๊ด (3๋
):
CREATE TABLE access_logs (
log_id BIGSERIAL PRIMARY KEY,
user_id INT,
table_name VARCHAR(100),
action VARCHAR(50),
accessed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
ip_address INET
);
CREATE TABLE access_logs (
log_id BIGSERIAL,
user_id INT,
table_name VARCHAR(100),
action VARCHAR(50),
accessed_at TIMESTAMP NOT NULL,
ip_address INET
) PARTITION BY RANGE (accessed_at);
CREATE TABLE access_logs_2025 PARTITION OF access_logs
FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE access_logs_2026 PARTITION OF access_logs
FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');
๋น๋ฐ๋ฒํธ ์ํธํ (๋จ๋ฐฉํฅ):
import bcrypt
password = "user_password_123"
hashed = bcrypt.hashpw(password.encode('utf-8'), bcrypt.gensalt())
INSERT INTO users (username, password_hash)
VALUES ('john', hashed)
stored_hash = db.query("SELECT password_hash FROM users WHERE username = 'john'")
is_valid = bcrypt.checkpw(password.encode('utf-8'), stored_hash)
6. ์ฑ๋ฅ ํ๋ ๋ฐ ๋ชจ๋ํฐ๋ง
PostgreSQL ์ฃผ์ ์ค์
# postgresql.conf ์ฃผ์ ํ๋ผ๋ฏธํฐ
# ๋ฉ๋ชจ๋ฆฌ ์ค์ (์๋ฒ RAM์ 25%)
shared_buffers = 4GB
# ์์
๋ฉ๋ชจ๋ฆฌ (์ฟผ๋ฆฌ๋น, ๋์ ์ฐ๊ฒฐ ์ ๊ณ ๋ ค)
work_mem = 64MB
# ์ ์ง๋ณด์ ์์
๋ฉ๋ชจ๋ฆฌ (VACUUM, CREATE INDEX)
maintenance_work_mem = 1GB
# WAL ์ค์ (Write-Ahead Logging)
wal_buffers = 16MB
min_wal_size = 1GB
max_wal_size = 4GB
# ์ฒดํฌํฌ์ธํธ (๋ฐ์ดํฐ ์์ ์ฑ vs ์ฑ๋ฅ)
checkpoint_completion_target = 0.9
checkpoint_timeout = 15min
# ์ฐ๊ฒฐ ํ๋ง
max_connections = 200
# ์ฟผ๋ฆฌ ํ๋๋
effective_cache_size = 12GB # OS ์บ์ ํฌํจ ์ด ๋ฉ๋ชจ๋ฆฌ์ 50-75%
random_page_cost = 1.1 # SSD์ผ ๊ฒฝ์ฐ ๊ธฐ๋ณธ 4.0์์ ๋ฎ์ถค
# ๋ก๊น
(๋๋ฆฐ ์ฟผ๋ฆฌ ์ถ์ )
log_min_duration_statement = 1000 # 1์ด ์ด์ ์ฟผ๋ฆฌ ๋ก๊น
log_line_prefix = '%t [%p]: [%l-1] user=%u,db=%d,app=%a,client=%h '
๋ชจ๋ํฐ๋ง ์ฟผ๋ฆฌ
SELECT
query,
calls,
total_exec_time / 1000 AS total_sec,
mean_exec_time / 1000 AS mean_sec,
max_exec_time / 1000 AS max_sec
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
SELECT
schemaname,
relname,
heap_blks_read,
heap_blks_hit,
ROUND(
100.0 * heap_blks_hit / NULLIF(heap_blks_hit + heap_blks_read, 0),
2
) AS cache_hit_ratio
FROM pg_statio_user_tables
ORDER BY heap_blks_read DESC
LIMIT 10;
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;
SELECT
blocked_locks.pid AS blocked_pid,
blocked_activity.usename AS blocked_user,
blocking_locks.pid AS blocking_pid,
blocking_activity.usename blocking_user,
blocked_activity.query blocked_statement,
blocking_activity.query blocking_statement
pg_catalog.pg_locks blocked_locks
pg_catalog.pg_stat_activity blocked_activity blocked_activity.pid blocked_locks.pid
pg_catalog.pg_locks blocking_locks
blocking_locks.locktype blocked_locks.locktype
blocking_locks.database blocked_locks.database
blocking_locks.relation blocked_locks.relation
blocking_locks.pid blocked_locks.pid
pg_catalog.pg_stat_activity blocking_activity blocking_activity.pid blocking_locks.pid
blocked_locks.granted;
schemaname,
relname,
n_live_tup,
n_dead_tup,
ROUND( n_dead_tup (n_live_tup n_dead_tup, ), ) dead_ratio,
last_vacuum,
last_autovacuum
pg_stat_user_tables
n_dead_tup
dead_ratio ;
Usage Guide
๋น ๋ฅธ ์์
1. ์ฟผ๋ฆฌ ์ต์ ํ ์์ฒญ
"PostgreSQL์์ ๋ค์ ์ฟผ๋ฆฌ๊ฐ ๋๋ฆฝ๋๋ค. ์ต์ ํํด์ฃผ์ธ์:
SELECT c.name, COUNT(o.order_id) AS order_count
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE c.created_at >= '2024-01-01'
GROUP BY c.name
HAVING COUNT(o.order_id) > 5
ORDER BY order_count DESC;
ํ
์ด๋ธ ํฌ๊ธฐ:
- customers: 100๋ง ํ
- orders: 500๋ง ํ"
์์ ์๋ต:
- EXPLAIN ANALYZE ๊ฒฐ๊ณผ ๋ถ์
- ๋ณ๋ชฉ ๊ตฌ๊ฐ ์๋ณ (Seq Scan, ๋์ Cost)
- ์ธ๋ฑ์ค ์ถ์ฒ (
CREATE INDEX idx_customers_created ON customers(created_at))
- ์ต์ ํ๋ ์ฟผ๋ฆฌ ์ ์
2. ์คํค๋ง ์ค๊ณ ์์ฒญ
"๋ธ๋ก๊ทธ ํ๋ซํผ์ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์คํค๋ง๋ฅผ ์ค๊ณํด์ฃผ์ธ์.
์๊ตฌ์ฌํญ:
- ์ฌ์ฉ์ (ํ์๊ฐ์
, ๋ก๊ทธ์ธ)
- ๊ฒ์๊ธ (์ ๋ชฉ, ๋ด์ฉ, ์์ฑ์ผ)
- ๋๊ธ (๊ณ์ธตํ, ๋๋๊ธ ์ง์)
- ํ๊ทธ (๊ฒ์๊ธ์ ์ฌ๋ฌ ํ๊ทธ ๊ฐ๋ฅ)
- ์ข์์ (์ฌ์ฉ์๊ฐ ๊ฒ์๊ธ์ ์ข์์)
์์ ํธ๋ํฝ:
- ์ผ์ผ ๊ฒ์๊ธ: 1,000๊ฐ
- ์ผ์ผ ๋๊ธ: 5,000๊ฐ
- ๋์ ์ ์์: 1,000๋ช
"
์์ ์๋ต:
- ER ๋ค์ด์ด๊ทธ๋จ
- ๊ฐ ํ
์ด๋ธ DDL (CREATE TABLE ๋ฌธ)
- ์ธ๋ฑ์ค ์ ๋ต
- ์ธ๋ํค ์ค์
- ์ ๊ทํ ์์ค (3NF ๊ถ์ฅ)
3. ๋ง์ด๊ทธ๋ ์ด์
๊ณํ ์์ฒญ
"MySQL 8.0์์ PostgreSQL 16์ผ๋ก ๋ง์ด๊ทธ๋ ์ด์
ํ๋ ค๊ณ ํฉ๋๋ค.
ํ์ฌ ์ํฉ:
- DB ํฌ๊ธฐ: 50GB
- ํ
์ด๋ธ ์: 80๊ฐ
- ์ผ์ผ ์ฐ๊ธฐ: 100๋ง ๊ฑด
- ๋ค์ดํ์ ํ์ฉ: ์ต๋ 4์๊ฐ
์๋ ค์ฃผ์ธ์:
1. ๋จ๊ณ๋ณ ๋ง์ด๊ทธ๋ ์ด์
๊ณํ
2. ์ฃผ์์ฌํญ (MySQL vs PostgreSQL ์ฐจ์ด)
3. ๋ฐ์ดํฐ ๋ฌด๊ฒฐ์ฑ ๊ฒ์ฆ ๋ฐฉ๋ฒ"
4. ๋ณด์ ๊ฒํ ์์ฒญ
"๊ธ์ต ์๋น์ค๋ฅผ ๊ฐ๋ฐ ์ค์
๋๋ค. ๋ณด์ ์ฒดํฌ๋ฆฌ์คํธ๋ฅผ ๋ง๋ค์ด์ฃผ์ธ์.
ํ๊ฒฝ:
- PostgreSQL 16
- AWS RDS
- Django ORM
์ค์ํด์ผ ํ ๊ท์ :
- ์ ์๊ธ์ต๊ฑฐ๋๋ฒ
- ๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ"
Examples
Example 1: ๋๋ฆฐ ์ฟผ๋ฆฌ ์ต์ ํ
์๋๋ฆฌ์ค: ์ ์์๊ฑฐ๋ ์ฃผ๋ฌธ ํํฉ ์กฐํ ์ฟผ๋ฆฌ๊ฐ 15์ด ์์
์
๋ ฅ:
EXPLAIN ANALYZE
SELECT
p.product_name,
c.category_name,
COUNT(oi.order_id) AS total_orders,
SUM(oi.quantity * oi.price) AS total_revenue
FROM products p
JOIN categories c ON p.category_id = c.category_id
JOIN order_items oi ON p.product_id = oi.product_id
JOIN orders o ON oi.order_id = o.order_id
WHERE o.created_at BETWEEN '2024-01-01' AND '2024-12-31'
AND o.status = 'completed'
GROUP BY p.product_name, c.category_name
ORDER BY total_revenue DESC
LIMIT 100;
EXPLAIN ANALYZE ๊ฒฐ๊ณผ:
Limit (cost=850000.12..850000.37 rows=100 width=68) (actual time=15234.521..15234.612 rows=100 loops=1)
-> Sort (cost=850000.12..850500.23 rows=200000 width=68) (actual time=15234.519..15234.565 rows=100 loops=1)
Sort Key: (sum((oi.quantity * oi.price))) DESC
Sort Method: top-N heapsort Memory: 35kB
-> HashAggregate (cost=800000.00..820000.00 rows=200000 width=68) (actual time=14500.234..15100.876 rows=9850 loops=1)
-> Hash Join (cost=50000.00..750000.00 rows=1000000 width=40) (actual time=234.123..12345.678 rows=987654 loops=1)
Hash Cond: (oi.product_id = p.product_id)
-> Hash Join (cost=30000.00..650000.00 rows=1000000 width=24) (actual time=123.456..10234.567 rows=987654 loops=1)
Hash Cond: (oi.order_id = o.order_id)
-> Seq Scan on order_items oi (cost=0.00..150000.00 rows=5000000 width=24) (actual time=0.023..3456.789 rows=5000000 loops=1)
-> Hash (cost=25000.00..25000.00 rows=400000 width=8) (actual time=123.234..123.234 rows=456789 loops=1)
Buckets: 65536 Batches: 8 Memory Usage: 4567kB
-> Seq Scan on orders o (cost=0.00..25000.00 rows=400000 width=8) (actual time=0.012..89.123 rows=456789 loops=1)
Filter: ((created_at >= '2024-01-01'::date) AND (created_at <= '2024-12-31'::date) AND (status = 'completed'::text))
Rows Removed by Filter: 1543211
-> Hash (cost=15000.00..15000.00 rows=10000 width=24) (actual time=45.678..45.678 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 789kB
-> Hash Join (cost=1.25..15000.00 rows=10000 width=24) (actual time=0.034..34.567 rows=10000 loops=1)
Hash Cond: (p.category_id = c.category_id)
-> Seq Scan on products p (cost=0.00..250.00 rows=10000 width=20) (actual time=0.012..12.345 rows=10000 loops=1)
-> Hash (cost=1.00..1.00 rows=50 width=12) (actual time=0.018..0.018 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 10kB
-> Seq Scan on categories c (cost=0.00..1.00 rows=50 width=12) (actual time=0.003..0.008 rows=50 loops=1)
Planning Time: 2.345 ms
Execution Time: 15234.789 ms
๋ถ์:
- ๋ณ๋ชฉ #1:
Seq Scan on orders - 200๋ง ํ ์ ์ฒด ์ค์บ (Filter๋ก 154๋ง ํ ์ ๊ฑฐ)
- ๋ณ๋ชฉ #2:
Seq Scan on order_items - 500๋ง ํ ์ ์ฒด ์ค์บ
- ๋ณ๋ชฉ #3:
HashAggregate - 98๋ง ํ ์ง๊ณ
์ต์ ํ ์ ๋ต:
1๋จ๊ณ: ์ธ๋ฑ์ค ์์ฑ
CREATE INDEX idx_orders_completed ON orders(created_at, status)
WHERE status = 'completed';
CREATE INDEX idx_order_items_lookup ON order_items(order_id, product_id);
CREATE INDEX idx_products_category ON products(category_id);
2๋จ๊ณ: ์ฟผ๋ฆฌ ์ฌ์์ฑ
EXPLAIN ANALYZE
SELECT
p.product_name,
c.category_name,
COUNT(*) AS total_orders,
SUM(oi.quantity * oi.price) AS total_revenue
FROM orders o
INNER JOIN order_items oi ON o.order_id = oi.order_id
INNER JOIN products p ON oi.product_id = p.product_id
INNER JOIN categories c ON p.category_id = c.category_id
WHERE o.created_at BETWEEN '2024-01-01' AND '2024-12-31'
AND o.status = 'completed'
GROUP BY p.product_id, p.product_name, c.category_name
ORDER BY total_revenue DESC
LIMIT 100;
์ต์ ํ ํ EXPLAIN ANALYZE (์์):
Limit (cost=12000.12..12000.37 rows=100 width=68) (actual time=450.123..450.234 rows=100 loops=1)
-> Sort (cost=12000.12..12500.23 rows=200000 width=68) (actual time=450.121..450.178 rows=100 loops=1)
Sort Key: (sum((oi.quantity * oi.price))) DESC
Sort Method: top-N heapsort Memory: 35kB
-> HashAggregate (cost=10000.00..11000.00 rows=200000 width=68) (actual time=380.234..420.567 rows=9850 loops=1)
-> Hash Join (cost=3000.00..8000.00 rows=1000000 width=40) (actual time=45.123..320.456 rows=987654 loops=1)
Hash Cond: (p.category_id = c.category_id)
-> Hash Join (cost=2998.75..7000.00 rows=1000000 width=32) (actual time=45.089..280.345 rows=987654 loops=1)
Hash Cond: (oi.product_id = p.product_id)
-> Nested Loop (cost=2748.75..5500.00 rows=1000000 width=24) (actual time=25.123..200.234 rows=987654 loops=1)
-> Bitmap Heap Scan on orders o (cost=2748.32..4500.00 rows=400000 width=8) (actual time=25.056..80.123 rows=456789 loops=1)
Recheck Cond: ((created_at >= '2024-01-01'::date) AND (created_at <= '2024-12-31'::date) AND (status = 'completed'::text))
Heap Blocks: exact=12345
-> Bitmap Index Scan on idx_orders_completed (cost=0.00..2648.32 rows=400000 width=0) (actual time=20.123..20.123 rows=456789 loops=1)
Index Cond: ((created_at >= '2024-01-01'::date) AND (created_at <= '2024-12-31'::date) AND (status = 'completed'::text))
-> Index Scan using idx_order_items_lookup on order_items oi (cost=0.43..1.50 rows=2 width=24) (actual time=0.001..0.001 rows=2 loops=456789)
Index Cond: (order_id = o.order_id)
-> Hash (cost=250.00..250.00 rows=10000 width=20) (actual time=19.890..19.890 rows=10000 loops=1)
Buckets: 16384 Batches: 1 Memory Usage: 789kB
-> Seq Scan on products p (cost=0.00..250.00 rows=10000 width=20) (actual time=0.012..12.345 rows=10000 loops=1)
-> Hash (cost=1.00..1.00 rows=50 width=12) (actual time=0.018..0.018 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 10kB
-> Seq Scan on categories c (cost=0.00..1.00 rows=50 width=12) (actual time=0.003..0.008 rows=50 loops=1)
Planning Time: 2.123 ms
Execution Time: 450.456 ms -- 15์ด โ 0.45์ด๋ก ๊ฐ์ ! (33๋ฐฐ ํฅ์)
๊ฒฐ๊ณผ:
- ์คํ ์๊ฐ: 15,234ms โ 450ms (97% ๊ฐ์, 33๋ฐฐ ํฅ์)
- ์ฝ์ ํ ์: 750๋ง ํ โ 146๋ง ํ (81% ๊ฐ์)
- ์ธ๋ฑ์ค ์ค์บ: Seq Scan โ Index Scan ์ ํ
- ๋ฉ๋ชจ๋ฆฌ ์ฌ์ฉ: ์ผ๋ถ ์ฆ๊ฐ (Hash Join), ํ์ง๋ง ๋์คํฌ I/O ๋ํญ ๊ฐ์
Example 2: ์ ์์๊ฑฐ๋ ๋ฐ์ดํฐ๋ฒ ์ด์ค ์ค๊ณ
์๋๋ฆฌ์ค: ์ค์ํ ์ ์์๊ฑฐ๋ ํ๋ซํผ (์ผ์ผ ์ฃผ๋ฌธ 1๋ง ๊ฑด, ์ํ 10๋ง ๊ฐ)
์๊ตฌ์ฌํญ:
- ํ์ ๊ด๋ฆฌ (์ผ๋ฐ/๊ธฐ์
ํ์, SNS ๋ก๊ทธ์ธ)
- ์ํ ๊ด๋ฆฌ (์นดํ
๊ณ ๋ฆฌ, ์ต์
, ์ฌ๊ณ )
- ์ฃผ๋ฌธ ์ฒ๋ฆฌ (์ฅ๋ฐ๊ตฌ๋, ๊ฒฐ์ , ๋ฐฐ์ก)
- ๋ฆฌ๋ทฐ ๋ฐ ํ์
- ์ฟ ํฐ ๋ฐ ํ๋ก๋ชจ์
์ค๊ณ ๊ฒฐ๊ณผ:
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password_hash VARCHAR(255) NOT NULL,
name VARCHAR(100) NOT NULL,
phone VARCHAR(20),
user_type VARCHAR(20) DEFAULT 'individual' CHECK (user_type IN ('individual', 'business')),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE user_addresses (
address_id SERIAL PRIMARY KEY,
user_id INT REFERENCES users(user_id) ON DELETE CASCADE,
address_type VARCHAR(20) CHECK (address_type IN ('shipping', 'billing')),
recipient_name VARCHAR(100),
phone VARCHAR(20),
postal_code VARCHAR(10),
address_line1 VARCHAR(200),
address_line2 VARCHAR(200),
city (),
state (),
is_default ,
created_at
);
INDEX idx_user_addresses_user user_addresses(user_id);
categories (
category_id SERIAL ,
parent_category_id categories(category_id),
category_name () ,
display_order ,
created_at
);
products (
product_id SERIAL ,
category_id categories(category_id),
product_name () ,
description TEXT,
base_price (,) ,
is_active ,
created_at ,
updated_at
);
INDEX idx_products_category products(category_id);
INDEX idx_products_active products(is_active) is_active ;
product_options (
option_id SERIAL ,
product_id products(product_id) CASCADE,
option_name () ,
option_value () ,
price_adjustment (,) ,
stock_quantity ,
(product_id, option_name, option_value)
);
INDEX idx_product_options_product product_options(product_id);
carts (
cart_id SERIAL ,
user_id users(user_id) CASCADE,
created_at ,
updated_at
);
cart_items (
cart_item_id SERIAL ,
cart_id carts(cart_id) CASCADE,
product_id products(product_id),
option_id product_options(option_id),
quantity (quantity ),
added_at
);
orders (
order_id SERIAL ,
user_id users(user_id),
order_number () ,
order_status ()
(order_status (, , , , , )),
total_amount (,) ,
shipping_fee (,) ,
discount_amount (,) ,
final_amount (,) ,
shipping_address_id user_addresses(address_id),
payment_method (),
payment_status ()
(payment_status (, , , )),
created_at ,
updated_at
);
INDEX idx_orders_user orders(user_id);
INDEX idx_orders_status orders(order_status);
INDEX idx_orders_created orders(created_at);
order_items (
order_item_id SERIAL ,
order_id orders(order_id) CASCADE,
product_id products(product_id),
option_id product_options(option_id),
product_name (),
option_name (),
unit_price (,) ,
quantity (quantity ),
subtotal (,)
);
INDEX idx_order_items_order order_items(order_id);
INDEX idx_order_items_product order_items(product_id);
reviews (
review_id SERIAL ,
product_id products(product_id) CASCADE,
user_id users(user_id) CASCADE,
order_item_id order_items(order_item_id),
rating (rating ),
title (),
content TEXT,
is_verified_purchase ,
created_at ,
updated_at ,
(order_item_id)
);
INDEX idx_reviews_product reviews(product_id);
INDEX idx_reviews_user reviews(user_id);
coupons (
coupon_id SERIAL ,
coupon_code () ,
discount_type () (discount_type (, )),
discount_value (,) ,
min_purchase_amount (,) ,
max_discount_amount (,),
usage_limit ,
usage_count ,
valid_from ,
valid_until ,
is_active
);
user_coupons (
user_coupon_id SERIAL ,
user_id users(user_id) CASCADE,
coupon_id coupons(coupon_id),
used_at ,
order_id orders(order_id),
(user_id, coupon_id)
);
MATERIALIZED product_stats
p.product_id,
p.product_name,
( oi.order_id) total_orders,
(oi.quantity) total_quantity_sold,
(oi.subtotal) total_revenue,
(r.rating) avg_rating,
(r.review_id) review_count
products p
order_items oi p.product_id oi.product_id
reviews r p.product_id r.product_id
p.product_id, p.product_name;
INDEX idx_product_stats_id product_stats(product_id);
์ธ๋ฑ์ฑ ์ ๋ต ์์ฝ:
- ์ธ๋ํค ์ธ๋ฑ์ค: ๋ชจ๋ ์ธ๋ํค ์ปฌ๋ผ (JOIN ์ต์ ํ)
- ๊ฒ์ ์กฐ๊ฑด ์ธ๋ฑ์ค:
order_status, is_active (WHERE ์ ๋น๋ฒ)
- ์๊ฐ ๊ธฐ๋ฐ ์ธ๋ฑ์ค:
created_at (๋ ์ง ๋ฒ์ ์ฟผ๋ฆฌ)
- ๋ถ๋ถ ์ธ๋ฑ์ค:
is_active = TRUE (ํ์ฑ ์ํ๋ง)
- ๊ณ ์ ์ธ๋ฑ์ค:
email, order_number (์ค๋ณต ๋ฐฉ์ง)
์ฑ๋ฅ ์์:
- ์ฃผ๋ฌธ ์กฐํ: < 50ms (์ธ๋ฑ์ค ์ฌ์ฉ)
- ์ํ ๊ฒ์: < 100ms (์นดํ
๊ณ ๋ฆฌ + ์ ๋ฌธ ๊ฒ์)
- ๋ฆฌ๋ทฐ ์กฐํ: < 30ms (product_id ์ธ๋ฑ์ค)
- ํต๊ณ ์กฐํ: < 10ms (๋จธํฐ๋ฆฌ์ผ๋ผ์ด์ฆ๋ ๋ทฐ)
Example 3: MongoDB ์ค๋ฉ ์ค๊ณ (๋์ฉ๋ ๋ก๊ทธ ์์คํ
)
์๋๋ฆฌ์ค: IoT ์ผ์ ๋ก๊ทธ ์ ์ฅ (1์ผ 1์ต ๊ฑด, ๋ณด๊ด ๊ธฐ๊ฐ 1๋
)
์๊ตฌ์ฌํญ:
- ์ด๋น 1,000๊ฑด ์ด์ ์ฝ์
- ์ผ์๋ณ/๋ ์ง๋ณ ์กฐํ ๋น๋ฒ
- ๋ฐ์ดํฐ ํฌ๊ธฐ: ์ฐ๊ฐ ์ฝ 5TB
- ๊ณ ๊ฐ์ฉ์ฑ (24/7 ์ด์)
์ค๊ณ ๊ฒฐ๊ณผ:
db.createCollection("sensor_logs", {
validator: {
$jsonSchema: {
bsonType: "object",
required: ["sensor_id", "timestamp", "value"],
properties: {
sensor_id: { bsonType: "string" },
timestamp: { bsonType: "date" },
sensor_type: { bsonType: "string", enum: ["temperature", "humidity", "pressure"] },
value: { bsonType: "double" },
location: {
bsonType: "object",
properties: {
type: { enum: ["Point"] },
coordinates: { bsonType: "array" }
}
},
metadata: { bsonType: "object" }
}
}
}
})
sh.enableSharding("iot_database")
sh.shardCollection(
"iot_database.sensor_logs",
{ sensor_id: , : }
)
db..({ : , : - })
db..({ : , : - })
db..({ : })
db..({ : }, { : })
db..(
{ },
{ : { : , : } }
)
db..({ : })
.()
์ค๋ฉ ์ํค๏ฟฝekstur:
Client
โ
mongos (์ฟผ๋ฆฌ ๋ผ์ฐํฐ)
โ
Config Servers (์ค๋ ๋ฉํ๋ฐ์ดํฐ)
โ
Shard 1 (์ผ์ 1-1000) Shard 2 (์ผ์ 1001-2000) Shard 3 (์ผ์ 2001-3000)
โ โ โ
Primary + 2 Secondaries Primary + 2 Secondaries Primary + 2 Secondaries
์ฟผ๋ฆฌ ์์:
db.sensor_logs.find({
sensor_id: "sensor_001",
timestamp: { $gte: ISODate("2025-01-15T09:00:00Z") }
}).sort({ timestamp: -1 })
db.sensor_logs.aggregate([
{ $match: { timestamp: { $gte: ISODate("2025-01-15T00:00:00Z") } } },
{ $group: {
_id: "$sensor_type",
avg_value: { $avg: "$value" },
count: { $sum: 1 }
}}
])
db.sensor_logs.find({
location: {
$near: {
$geometry: { type: "Point", coordinates: [127.0276, 37.4979] },
$maxDistance: 5000
}
},
timestamp: { $gte: ISODate("2025-01-15T00:00:00Z") }
})
db.sensor_logs.insertMany([
{ : , : (), : , : },
{ : , : (), : , : },
], { : })
์ฑ๋ฅ ๋ชจ๋ํฐ๋ง:
db.sensor_logs.getShardDistribution()
sh.status()
db.currentOp()
db.setProfilingLevel(1, { slowms: 100 })
db.system.profile.find().sort({ ts: -1 }).limit(5)
๊ฒฐ๊ณผ:
- ์ฝ์
์๋: ํ๊ท 2,000 docs/sec (๋ฐฐ์น ์ฝ์
์ 10,000+)
- ์กฐํ ์๋: < 50ms (์ค๋ ํค ํฌํจ), < 500ms (์ ์ฒด ์ค๋)
- ์ ์ฅ ๊ณต๊ฐ: ์ค๋๋น ์ฝ 1.7TB (3๊ฐ ์ค๋ = 5TB)
- ๊ณ ๊ฐ์ฉ์ฑ: ๊ฐ ์ค๋ 3-๋ณต์ ๋ณธ (Primary + 2 Secondary)
Best Practices
๋ฐ์ดํฐ๋ฒ ์ด์ค ์ ํ ๊ฐ์ด๋
| ์ฌ์ฉ ์ฌ๋ก | ๊ถ์ฅ DB | ์ด์ |
|---|
| OLTP (์ํ, ์ ์์๊ฑฐ๋) | PostgreSQL, MySQL | ACID ๋ณด์ฅ, ํธ๋์ญ์
์์ ์ฑ |
| ๋์ฉ๋ ์ฝ๊ธฐ (SNS ํผ๋) | Redis (์บ์) + MySQL | ์ฝ๊ธฐ ๋ถํ ๋ถ์ฐ |
| ๋ฌธ์ ์ ์ฅ (CMS, ๋ธ๋ก๊ทธ) | MongoDB | ์ ์ฐํ ์คํค๋ง, JSON ์นํ์ |
| ์ค์๊ฐ ๋ถ์ (๋์๋ณด๋) | TimescaleDB, InfluxDB | ์๊ณ์ด ์ต์ ํ |
| ์ธ์
์ ์ฅ (๋ก๊ทธ์ธ) | Redis | ๋น ๋ฅธ ๋ฉ๋ชจ๋ฆฌ ์ก์ธ์ค |
| ์ง๋ฆฌ ์ ๋ณด (๋ฐฐ๋ฌ ์ฑ) | PostgreSQL (PostGIS) | GIS ๊ธฐ๋ฅ |
| ๊ทธ๋ํ ๊ด๊ณ (SNS ์น๊ตฌ) | Neo4j | ๊ด๊ณ ํ์ ์ต์ ํ |
| ๊ฒ์ ์์ง (์ํ ๊ฒ์) | Elasticsearch | ์ ๋ฌธ ๊ฒ์, ์๋์์ฑ |
| ๋ถ์ฐ SQL (๊ธ๋ก๋ฒ ์๋น์ค) | CockroachDB, TiDB | ์ง์ญ ๊ฐ ๋ณต์ , ํ์ฅ์ฑ |
โ
DO
1. ์ธ๋ฑ์ค ์ ๋ต
CREATE INDEX idx_orders_lookup ON orders(user_id, created_at);
CREATE INDEX idx_active_products ON products(product_id) WHERE is_active = TRUE;
CREATE INDEX idx_orders_covering ON orders(user_id, created_at) INCLUDE (total_amount);
2. ์ฟผ๋ฆฌ ์ต์ ํ
SELECT * FROM products p
WHERE EXISTS (
SELECT 1 FROM order_items oi
WHERE oi.product_id = p.product_id
);
SELECT * FROM logs ORDER BY created_at DESC LIMIT 100;
SELECT user_id, COUNT(*) FROM orders
WHERE created_at >= '2025-01-01'
GROUP BY user_id;
3. ํธ๋์ญ์
๊ด๋ฆฌ
with conn.cursor() as cur:
cur.execute("BEGIN")
cur.execute("UPDATE accounts SET balance = balance - 100 WHERE id = 1")
cur.execute("UPDATE accounts SET balance = balance + 100 WHERE id = 2")
cur.execute("COMMIT")
cur.execute("SET statement_timeout = '5s'")
4. ์ฐ๊ฒฐ ํ๋ง
from psycopg2 import pool
connection_pool = pool.SimpleConnectionPool(
minconn=5,
maxconn=20,
host='localhost',
database='mydb'
)
conn = connection_pool.getconn()
connection_pool.putconn(conn)
5. ๋ณด์
cur.execute("SELECT * FROM users WHERE email = %s", (user_email,))
CREATE USER app_user WITH PASSWORD 'secure_password';
GRANT SELECT, INSERT, UPDATE ON TABLE orders TO app_user;
-- DELETE ๊ถํ์ ๋ถ์ฌํ์ง ์์
โ DON'T
1. ์ํฐํจํด
SELECT * FROM orders;
SELECT order_id, total_amount, created_at FROM orders;
SELECT * FROM products WHERE category_id = 1 OR category_id = 2 OR category_id = 3;
SELECT * FROM products WHERE category_id IN (1, 2, 3);
SELECT * FROM products WHERE product_name LIKE '%์นด๋ฉ๋ผ%';
SELECT * FROM products WHERE to_tsvector('korean', product_name) @@ to_tsquery('korean', '์นด๋ฉ๋ผ');
2. ํธ๋์ญ์
์ค์ฉ
cur.execute("BEGIN")
cur.execute("SELECT * FROM orders")
time.sleep(10)
cur.execute("UPDATE orders SET status = 'processed'")
cur.execute("COMMIT")
data = cur.execute("SELECT * FROM orders").fetchall()
processed_data = call_external_api(data)
cur.execute("BEGIN")
cur.execute("UPDATE orders SET status = 'processed'")
cur.execute("COMMIT")
3. N+1 ๋ฌธ์
orders = cur.execute("SELECT * FROM orders").fetchall()
for order in orders:
customer = cur.execute("SELECT * FROM customers WHERE id = %s", (order['customer_id'],)).fetchone()
result = cur.execute("""
SELECT o.*, c.name, c.email
FROM orders o
JOIN customers c ON o.customer_id = c.id
""").fetchall()
4. ์ธ๋ฑ์ค ์ค์ฉ
SELECT * FROM users WHERE LOWER(email) = 'john@example.com';
CREATE INDEX idx_users_email_lower ON users(LOWER(email));
SELECT * FROM users WHERE email = 'john@example.com';
5. ๋ณด์ ์ทจ์ฝ์
query = f"SELECT * FROM users WHERE username = '{user_input}'"
cur.execute(query)
query = "SELECT * FROM users WHERE username = %s"
cur.execute(query, (user_input,))
Troubleshooting
Issue 1: ์ฟผ๋ฆฌ ์๋ต ์๊ฐ ๋๋ฆผ (> 1์ด)
์ฆ์:
- ํน์ ์ฟผ๋ฆฌ๊ฐ ์ผ๊ด๋๊ฒ 1์ด ์ด์ ์์
- ์ฌ์ฉ์ ํ์ด์ง ๋ก๋ฉ ์ง์ฐ
์ง๋จ ๋จ๊ณ:
EXPLAIN (ANALYZE, BUFFERS, VERBOSE)
SELECT * FROM orders WHERE user_id = 123;
SELECT * FROM pg_indexes WHERE tablename = 'orders';
SELECT last_analyze FROM pg_stat_user_tables WHERE relname = 'orders';
ํด๊ฒฐ ๋ฐฉ๋ฒ:
CREATE INDEX idx_orders_user ON orders(user_id);
ANALYZE orders;
์๋ฐฉ:
- ์ ๊ธฐ์ ANALYZE ์คํ (Autovacuum ํ์ฑํ)
- ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ ๋ก๊ทธ ๋ชจ๋ํฐ๋ง
- ์ธ๋ฑ์ค ์ฌ์ฉ๋ฅ ์ฃผ๊ธฐ์ ๊ฒํ
Issue 2: ๋ฐ๋๋ฝ ๋ฐ์
์ฆ์:
ERROR: deadlock detected
DETAIL: Process 12345 waits for ShareLock on transaction 67890
Process 67890 waits for ShareLock on transaction 12345
์ง๋จ ๋จ๊ณ:
SELECT * FROM pg_stat_database WHERE datname = 'mydb';
SELECT
locktype, relation::regclass, mode, granted, pid
FROM pg_locks
WHERE NOT granted
ORDER BY pid;
SELECT
blocked_locks.pid AS blocked_pid,
blocking_locks.pid AS blocking_pid,
blocked_activity.query AS blocked_query,
blocking_activity.query AS blocking_query
FROM pg_locks blocked_locks
JOIN pg_stat_activity blocked_activity ON blocked_activity.pid = blocked_locks.pid
JOIN pg_locks blocking_locks ON blocking_locks.locktype = blocked_locks.locktype
JOIN pg_stat_activity blocking_activity ON blocking_activity.pid = blocking_locks.pid
WHERE NOT blocked_locks.granted AND blocking_locks.granted;
ํด๊ฒฐ ๋ฐฉ๋ฒ:
BEGIN;
SELECT * FROM accounts WHERE id IN (1, 2) ORDER BY id FOR UPDATE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
SET lock_timeout = '5s';
SET statement_timeout = '10s';
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
์๋ฐฉ:
- ํธ๋์ญ์
์ต์ํ (์งง๊ฒ ์ ์ง)
- ์ผ๊ด๋ ๋ฝ ์์ ์ ์ง
- ํ์ํ ๊ฒฝ์ฐ์๋ง FOR UPDATE ์ฌ์ฉ
Issue 3: ๋์ CPU ์ฌ์ฉ๋ฅ (> 80%)
์ฆ์:
- ๋ฐ์ดํฐ๋ฒ ์ด์ค ์๋ฒ CPU ์ฌ์ฉ๋ฅ ์ง์์ ์ผ๋ก ๋์
- ์ ์ฒด ์์คํ
์ฑ๋ฅ ์ ํ
์ง๋จ ๋จ๊ณ:
SELECT pid, state, query, query_start
FROM pg_stat_activity
WHERE state = 'active'
ORDER BY query_start;
SELECT pid, now() - query_start AS duration, query
FROM pg_stat_activity
WHERE state = 'active' AND now() - query_start > interval '5 minutes';
SELECT
query,
calls,
total_exec_time / 1000 AS total_sec,
mean_exec_time / 1000 AS mean_sec
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
ํด๊ฒฐ ๋ฐฉ๋ฒ:
SELECT pg_terminate_backend(12345);
ALTER SYSTEM SET max_connections = 100;
SELECT pg_reload_conf();
์๋ฐฉ:
- ์ฐ๊ฒฐ ํ ์ฌ์ฉ (PgBouncer, pgPool)
- ์ฌ๋ก์ฐ ์ฟผ๋ฆฌ ์ ๊ธฐ ์ ๊ฒ
- ๋ฆฌ์์ค ๋ชจ๋ํฐ๋ง (Prometheus + Grafana)
Issue 4: ๋์คํฌ ๊ณต๊ฐ ๋ถ์กฑ
์ฆ์:
ERROR: could not extend file "base/16384/12345": No space left on device
์ง๋จ ๋จ๊ณ:
SELECT
pg_database.datname,
pg_size_pretty(pg_database_size(pg_database.datname)) AS size
FROM pg_database
ORDER BY pg_database_size(pg_database.datname) DESC;
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC
LIMIT 10;
SELECT
schemaname,
tablename,
indexname,
pg_size_pretty(pg_relation_size(indexrelid)) AS size
FROM pg_stat_user_indexes
ORDER BY pg_relation_size(indexrelid) DESC
LIMIT 10;
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS total_size,
n_dead_tup,
ROUND(100.0 * n_dead_tup / NULLIF(n_live_tup + n_dead_tup, 0), 2) AS dead_ratio
FROM pg_stat_user_tables
WHERE n_dead_tup > 0
n_dead_tup ;
ํด๊ฒฐ ๋ฐฉ๋ฒ:
VACUUM FULL orders;
VACUUM ANALYZE orders;
CREATE TABLE orders_2024_archive AS
SELECT * FROM orders WHERE created_at < '2024-01-01';
DELETE FROM orders WHERE created_at < '2024-01-01';
VACUUM FULL orders;
DROP INDEX idx_unused_index;
์๋ฐฉ:
- Autovacuum ํ์ฑํ ๋ฐ ํ๋
- ํํฐ์
๋์ผ๋ก ๋ฐ์ดํฐ ๊ด๋ฆฌ
- ์ ๊ธฐ์ ์์นด์ด๋ธ ์ ์ฑ
- ๋์คํฌ ์ฌ์ฉ๋ฅ ๋ชจ๋ํฐ๋ง
Issue 5: ์ฐ๊ฒฐ ๊ฑฐ๋ถ (Too Many Connections)
์ฆ์:
FATAL: remaining connection slots are reserved for non-replication superuser connections
FATAL: sorry, too many clients already
์ง๋จ ๋จ๊ณ:
SELECT count(*) FROM pg_stat_activity;
SHOW max_connections;
SELECT datname, count(*)
FROM pg_stat_activity
GROUP BY datname
ORDER BY count(*) DESC;
SELECT pid, state, state_change, query_start
FROM pg_stat_activity
WHERE state = 'idle'
ORDER BY state_change;
ํด๊ฒฐ ๋ฐฉ๋ฒ:
SELECT pg_terminate_backend(pid)
FROM pg_stat_activity
WHERE state = 'idle' AND now() - state_change > interval '10 minutes';
ALTER SYSTEM SET max_connections = 200;
SELECT pg_reload_conf();
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
listen_port = 6432
listen_addr = *
auth_type = md5
pool_mode = transaction
max_client_conn = 1000
default_pool_size = 20
์๋ฐฉ:
- ์ ํ๋ฆฌ์ผ์ด์
์์ ์ฐ๊ฒฐ ํ ์ฌ์ฉ
- PgBouncer ๋๋ pgPool ๋์
- ์ฐ๊ฒฐ ํ์์์ ์ค์
- ๋ชจ๋ํฐ๋ง ์๋ฆผ ์ค์
Security Guidelines
ํ๊ตญ ๋ฒ๊ท ์ค์ ์ฒดํฌ๋ฆฌ์คํธ
๊ฐ์ธ์ ๋ณด๋ณดํธ๋ฒ
CREATE TABLE users (
user_id SERIAL PRIMARY KEY,
name VARCHAR(100),
resident_number BYTEA,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
INSERT INTO users (name, resident_number)
VALUES ('ํ๊ธธ๋', pgp_sym_encrypt('901231-1234567', current_setting('app.encryption_key')));
SELECT name, pgp_sym_decrypt(resident_number, current_setting('app.encryption_key'))
FROM users
WHERE user_id = 123;
์ ์๊ธ์ต๊ฑฐ๋๋ฒ
CREATE TABLE transaction_logs (
log_id BIGSERIAL PRIMARY KEY,
transaction_id VARCHAR(50) NOT NULL,
user_id INT,
transaction_type VARCHAR(50),
amount DECIMAL(15,2),
status VARCHAR(20),
ip_address INET,
user_agent TEXT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT check_amount CHECK (amount >= 0)
);
CREATE TABLE transaction_logs (
log_id BIGSERIAL,
transaction_id VARCHAR(50) NOT NULL,
created_at TIMESTAMP NOT NULL
) PARTITION BY RANGE (created_at);
CREATE TABLE transaction_logs_2025_01 PARTITION OF transaction_logs
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01');
Performance Benchmarks
PostgreSQL ๋ฒค์น๋งํฌ (TPC-C ๊ธฐ์ค)
| ํ๋์จ์ด | Connections | TPS | ํ๊ท ์๋ต ์๊ฐ |
|---|
| 4 vCPU, 16GB RAM, SSD | 50 | 1,200 | 15ms |
| 8 vCPU, 32GB RAM, SSD | 100 | 3,500 | 12ms |
| 16 vCPU, 64GB RAM, NVMe | 200 | 8,000 | 8ms |
MongoDB ๋ฒค์น๋งํฌ (YCSB ๊ธฐ์ค)
| ์ํฌ๋ก๋ | ์ฝ๊ธฐ | ์ฐ๊ธฐ | ์ฒ๋ฆฌ๋ (ops/sec) |
|---|
| Read-Heavy | 95% | 5% | 15,000 |
| Balanced | 50% | 50% | 8,000 |
| Write-Heavy | 5% | 95% | 5,500 |
Redis ๋ฒค์น๋งํฌ
| ๋ช
๋ น์ด | QPS | ํ๊ท ๋ ์ดํด์ |
|---|
| GET | 100,000 | 0.2ms |
| SET | 80,000 | 0.3ms |
| INCR | 100,000 | 0.2ms |
| LPUSH | 70,000 | 0.4ms |
Migration Guides
MySQL โ PostgreSQL
ํธํ์ฑ ์ด์:
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY
);
CREATE TABLE users (
id SERIAL PRIMARY KEY
);
๋ง์ด๊ทธ๋ ์ด์
๋๊ตฌ:
apt-get install pgloader
pgloader mysql://user:pass@localhost/mydb postgresql://user:pass@localhost/mydb
pgloader --schema-only mysql://... postgresql://...
SELECT COUNT(*) FROM users; -- MySQL
SELECT COUNT(*) FROM users; -- PostgreSQL
๋ง์ด๊ทธ๋ ์ด์
์ฒดํฌ๋ฆฌ์คํธ:
Version History
v1.0.0 (2025-01-15)
- ์ด๊ธฐ ๋ฆด๋ฆฌ์ค
- PostgreSQL, MySQL, MongoDB, Redis ์ง์
- ์ฟผ๋ฆฌ ์ต์ ํ, ์คํค๋ง ์ค๊ณ, ๋ณด์ ๊ฐ์ด๋ ํฌํจ
- ํ๊ตญ ๋ฒ๊ท ์ค์ ์น์
์ถ๊ฐ
ํฅํ ๊ณํ
- v1.1.0: Elasticsearch, ClickHouse ์ถ๊ฐ
- v1.2.0: ํด๋ผ์ฐ๋ DB (AWS RDS, Aurora) ์ต์ ํ ๊ฐ์ด๋
- v1.3.0: ๋ํํ ์ฟผ๋ฆฌ ๋ถ์ (์๋ EXPLAIN)
- v2.0.0: ์ค์๊ฐ ์ฑ๋ฅ ๋ชจ๋ํฐ๋ง ํตํฉ
Additional Resources
๊ณต์ ๋ฌธ์
ํ์ต ์๋ฃ
ํ๊ตญ ์ปค๋ฎค๋ํฐ
๋ฒค์น๋งํฌ
License
์ด ์คํฌ์ MIT ๋ผ์ด์ ์ค ํ์ ๋ฐฐํฌ๋ฉ๋๋ค.
Support
๋ฌธ์: db-expert-skill@example.com
๋ง์ง๋ง ์
๋ฐ์ดํธ: 2025-01-15
์์ฑ์: Claude Skills Generator
๋ฒ์ : 1.0.0