PostgreSQL
Postgres, The World's Most Advanced Open Source Database
PostgreSQL은 35년 이상의 역사를 가진 가장 진보된 오픈소스 객체-관계형 데이터베이스입니다. ACID를 완벽히 준수하며, JSON/JSONB, 전문검색, GIS, 확장 기능을 통해 엔터프라이즈급 안정성과 유연성을 제공합니다.
Postgres, The World's Most Advanced Open Source Database
PostgreSQL은 35년 이상의 역사를 가진 가장 진보된 오픈소스 객체-관계형 데이터베이스입니다. ACID를 완벽히 준수하며, JSON/JSONB, 전문검색, GIS, 확장 기능을 통해 엔터프라이즈급 안정성과 유연성을 제공합니다.
PostgreSQL의 탄생과 철학 - 1986년 UC Berkeley에서 시작된 POSTGRES 프로젝트가 기원입니다. "확장 가능하고 표준을 준수하는 데이터베이스"라는 철학 아래, SQL 표준을 가장 충실히 구현한 오픈소스 RDBMS로 발전했습니다. 현재 Apple, Instagram, Spotify 등 수많은 기업이 핵심 시스템에 사용합니다.
MVCC(Multi-Version Concurrency Control) - PostgreSQL은 MVCC를 통해 읽기와 쓰기가 서로 블로킹하지 않습니다. 각 트랜잭션은 데이터의 스냅샷을 보며, 동시에 여러 버전의 데이터가 존재할 수 있습니다. 이로 인해 높은 동시성과 일관된 읽기를 보장합니다.
확장성(Extensibility) - PostgreSQL의 가장 큰 강점입니다. 사용자 정의 데이터 타입, 함수, 연산자, 인덱스 타입, 언어를 추가할 수 있습니다. PostGIS(지리정보), TimescaleDB(시계열), pgvector(벡터 검색) 등 강력한 확장이 생태계를 풍성하게 합니다.
JSON/JSONB 지원 - 관계형과 NoSQL의 장점을 결합합니다. JSONB는 바이너리 형태로 저장되어 인덱싱과 쿼리가 빠르며, GIN 인덱스로 JSON 내부 필드도 검색할 수 있습니다. 이를 통해 스키마 유연성과 ACID를 동시에 확보합니다.
전문검색(Full-Text Search) - tsvector와 tsquery를 이용한 내장 전문검색을 지원합니다. 한국어는 pg_bigm이나 mecab 기반 확장으로 형태소 분석이 가능하며, Elasticsearch 없이도 기본적인 검색 기능을 구현할 수 있습니다.
# PostgreSQL 연결 및 CRUD - Python + psycopg2
import psycopg2
from psycopg2.extras import RealDictCursor, Json
from contextlib import contextmanager
# 연결 설정
DATABASE_URL = "postgresql://user:password@localhost:5432/mydb"
@contextmanager
def get_connection():
"""컨텍스트 매니저로 안전한 연결 관리"""
conn = psycopg2.connect(DATABASE_URL)
try:
yield conn
conn.commit()
except Exception as e:
conn.rollback()
raise e
finally:
conn.close()
# 테이블 생성 및 JSONB 활용
def create_tables():
with get_connection() as conn:
with conn.cursor() as cur:
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
price DECIMAL(10, 2) NOT NULL,
metadata JSONB DEFAULT '{}',
search_vector TSVECTOR,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- JSONB 필드에 GIN 인덱스 (빠른 JSON 검색)
CREATE INDEX IF NOT EXISTS idx_products_metadata
ON products USING GIN (metadata);
-- 전문검색 인덱스
CREATE INDEX IF NOT EXISTS idx_products_search
ON products USING GIN (search_vector);
""")
print("✅ 테이블 생성 완료")
# JSONB 데이터 삽입 및 조회
def jsonb_operations():
with get_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
# JSONB 데이터 삽입
cur.execute("""
INSERT INTO products (name, price, metadata)
VALUES (%s, %s, %s)
RETURNING id, name, metadata
""", (
"MacBook Pro 16",
3499.00,
Json({
"brand": "Apple",
"specs": {"cpu": "M3 Pro", "ram": "36GB", "storage": "512GB"},
"tags": ["laptop", "professional", "macOS"]
})
))
product = cur.fetchone()
print(f"삽입됨: {product}")
# JSONB 쿼리 - 중첩 필드 검색
cur.execute("""
SELECT name, price, metadata->'specs'->>'cpu' as cpu
FROM products
WHERE metadata @> '{"brand": "Apple"}'
AND (metadata->'specs'->>'ram')::int >= 32
""")
results = cur.fetchall()
print(f"Apple 제품 (RAM >= 32GB): {results}")
# 트랜잭션과 Savepoint
def transaction_with_savepoint():
with get_connection() as conn:
with conn.cursor() as cur:
try:
cur.execute("INSERT INTO products (name, price) VALUES ('Item A', 100)")
# Savepoint 생성
cur.execute("SAVEPOINT sp1")
cur.execute("INSERT INTO products (name, price) VALUES ('Item B', 200)")
# 조건에 따라 롤백
if some_condition_fails():
cur.execute("ROLLBACK TO SAVEPOINT sp1")
print("⚠️ Item B 롤백됨, Item A는 유지")
else:
cur.execute("RELEASE SAVEPOINT sp1")
except Exception as e:
print(f"❌ 전체 롤백: {e}")
raise
# CTE(Common Table Expression) 예제
def complex_query_with_cte():
with get_connection() as conn:
with conn.cursor(cursor_factory=RealDictCursor) as cur:
cur.execute("""
WITH product_stats AS (
SELECT
metadata->>'brand' as brand,
COUNT(*) as count,
AVG(price) as avg_price
FROM products
WHERE metadata ? 'brand'
GROUP BY metadata->>'brand'
),
ranked AS (
SELECT *,
RANK() OVER (ORDER BY avg_price DESC) as price_rank
FROM product_stats
)
SELECT * FROM ranked WHERE count >= 5
""")
return cur.fetchall()
if __name__ == "__main__":
create_tables()
jsonb_operations()
// PostgreSQL 연결 및 CRUD - Node.js + pg
const { Pool } = require('pg');
// 커넥션 풀 설정
const pool = new Pool({
connectionString: 'postgresql://user:password@localhost:5432/mydb',
max: 20, // 최대 연결 수
idleTimeoutMillis: 30000, // 유휴 연결 타임아웃
connectionTimeoutMillis: 2000,
});
// 트랜잭션 헬퍼
async function withTransaction(callback) {
const client = await pool.connect();
try {
await client.query('BEGIN');
const result = await callback(client);
await client.query('COMMIT');
return result;
} catch (error) {
await client.query('ROLLBACK');
throw error;
} finally {
client.release();
}
}
// JSONB 데이터 조작
async function jsonbOperations() {
// JSONB 삽입
const insertResult = await pool.query(`
INSERT INTO products (name, price, metadata)
VALUES ($1, $2, $3)
RETURNING *
`, [
'Galaxy S24 Ultra',
1299.00,
{
brand: 'Samsung',
specs: { display: '6.8"', ram: '12GB', storage: '256GB' },
tags: ['smartphone', 'android', 'flagship']
}
]);
console.log('삽입됨:', insertResult.rows[0]);
// JSONB 배열에서 검색
const searchResult = await pool.query(`
SELECT name, price, metadata->'tags' as tags
FROM products
WHERE metadata->'tags' ? $1
`, ['flagship']);
console.log('flagship 태그:', searchResult.rows);
// JSONB 필드 업데이트 (jsonb_set)
await pool.query(`
UPDATE products
SET metadata = jsonb_set(metadata, '{specs,ram}', '"16GB"')
WHERE name = $1
`, ['Galaxy S24 Ultra']);
}
// 배치 삽입 (COPY 대안)
async function batchInsert(products) {
const values = products.map((p, i) => {
const offset = i * 3;
return `($${offset + 1}, $${offset + 2}, $${offset + 3})`;
}).join(', ');
const params = products.flatMap(p => [p.name, p.price, p.metadata]);
await pool.query(`
INSERT INTO products (name, price, metadata)
VALUES ${values}
ON CONFLICT (name) DO UPDATE
SET price = EXCLUDED.price,
metadata = products.metadata || EXCLUDED.metadata
`, params);
}
// 스트리밍 대용량 조회
async function streamLargeResult() {
const client = await pool.connect();
try {
// 커서 사용
await client.query('BEGIN');
await client.query(`
DECLARE product_cursor CURSOR FOR
SELECT * FROM products ORDER BY created_at
`);
let batch;
while (true) {
const result = await client.query('FETCH 1000 FROM product_cursor');
batch = result.rows;
if (batch.length === 0) break;
// 배치 처리
await processBatch(batch);
}
await client.query('CLOSE product_cursor');
await client.query('COMMIT');
} finally {
client.release();
}
}
// Listen/Notify (실시간 알림)
async function setupNotifications() {
const client = await pool.connect();
client.on('notification', (msg) => {
console.log('알림 수신:', msg.channel, JSON.parse(msg.payload));
});
await client.query('LISTEN product_changes');
// 다른 세션에서 NOTIFY 호출 시 알림 수신
// NOTIFY product_changes, '{"action": "insert", "id": 123}';
}
// 실행
(async () => {
await jsonbOperations();
console.log('✅ PostgreSQL 작업 완료');
await pool.end();
})();
-- PostgreSQL 고급 SQL 예제
-- ============================================
-- 1. JSONB 고급 쿼리
-- ============================================
-- JSONB 생성 및 조작
SELECT jsonb_build_object(
'name', name,
'price', price,
'discounted', price * 0.9
) as product_json
FROM products;
-- JSONB 배열 펼치기
SELECT p.name, tag
FROM products p,
jsonb_array_elements_text(p.metadata->'tags') as tag
WHERE tag LIKE '%pro%';
-- JSONB 집계
SELECT
metadata->>'brand' as brand,
jsonb_agg(name) as products,
jsonb_object_agg(name, price) as price_map
FROM products
GROUP BY metadata->>'brand';
-- ============================================
-- 2. 윈도우 함수
-- ============================================
SELECT
name,
price,
metadata->>'brand' as brand,
-- 브랜드별 순위
RANK() OVER (PARTITION BY metadata->>'brand' ORDER BY price DESC) as rank_in_brand,
-- 누적 합계
SUM(price) OVER (ORDER BY created_at) as running_total,
-- 이전 행과 비교
LAG(price) OVER (ORDER BY created_at) as prev_price,
-- 이동 평균
AVG(price) OVER (ORDER BY created_at ROWS BETWEEN 2 PRECEDING AND CURRENT ROW) as moving_avg
FROM products;
-- ============================================
-- 3. 전문검색 (Full-Text Search)
-- ============================================
-- 검색 벡터 업데이트
UPDATE products
SET search_vector =
setweight(to_tsvector('english', name), 'A') ||
setweight(to_tsvector('english', COALESCE(metadata->>'brand', '')), 'B');
-- 전문검색 쿼리
SELECT name, ts_rank(search_vector, query) as rank
FROM products, to_tsquery('english', 'apple | samsung') as query
WHERE search_vector @@ query
ORDER BY rank DESC;
-- 하이라이트
SELECT ts_headline('english', name, to_tsquery('macbook'))
FROM products
WHERE search_vector @@ to_tsquery('macbook');
-- ============================================
-- 4. CTE와 재귀 쿼리
-- ============================================
-- 카테고리 계층 조회 (재귀 CTE)
WITH RECURSIVE category_tree AS (
-- 기본 케이스: 최상위 카테고리
SELECT id, name, parent_id, 1 as depth, ARRAY[name] as path
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- 재귀 케이스: 하위 카테고리
SELECT c.id, c.name, c.parent_id, ct.depth + 1, ct.path || c.name
FROM categories c
JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY path;
-- ============================================
-- 5. UPSERT (INSERT ON CONFLICT)
-- ============================================
INSERT INTO products (name, price, metadata)
VALUES ('iPhone 15', 999, '{"brand": "Apple"}')
ON CONFLICT (name) DO UPDATE
SET
price = EXCLUDED.price,
metadata = products.metadata || EXCLUDED.metadata,
updated_at = CURRENT_TIMESTAMP
WHERE products.price != EXCLUDED.price
RETURNING *;
-- ============================================
-- 6. 성능 분석
-- ============================================
-- 쿼리 실행 계획 분석
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM products
WHERE metadata @> '{"brand": "Apple"}'
ORDER BY price DESC
LIMIT 10;
-- 테이블 통계 확인
SELECT
relname as table,
n_live_tup as rows,
n_dead_tup as dead_rows,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables;
-- ============================================
-- 7. 파티셔닝
-- ============================================
-- 범위 파티션 테이블 생성
CREATE TABLE orders (
id SERIAL,
customer_id INT,
order_date DATE,
total DECIMAL(10,2)
) PARTITION BY RANGE (order_date);
-- 월별 파티션 생성
CREATE TABLE orders_2024_01 PARTITION OF orders
FOR VALUES FROM ('2024-01-01') TO ('2024-02-01');
CREATE TABLE orders_2024_02 PARTITION OF orders
FOR VALUES FROM ('2024-02-01') TO ('2024-03-01');
"스키마가 자주 변경되는 상품 메타데이터가 있어서 MongoDB를 고려했는데요, PostgreSQL의 JSONB를 쓰면 NoSQL의 유연성과 ACID 트랜잭션을 동시에 가져갈 수 있습니다. GIN 인덱스로 JSON 필드 검색도 빠르고요."
"EXPLAIN ANALYZE 결과를 보니 Sequential Scan이 발생하고 있습니다. JSONB 필드에 GIN 인덱스를 추가하고, 자주 조회되는 중첩 필드는 Generated Column으로 추출해서 B-tree 인덱스를 걸면 성능이 크게 개선될 겁니다."
"벡터 유사도 검색이 필요한데, pgvector 확장을 쓰면 됩니다. PostgreSQL에서 직접 임베딩 저장하고 HNSW 인덱스로 빠른 유사도 검색이 가능해요. 별도의 벡터 DB 없이 기존 인프라에서 RAG를 구현할 수 있습니다."
MVCC로 인해 dead tuple이 쌓입니다. autovacuum 설정을 확인하고, 대량 DELETE/UPDATE 후에는 수동 VACUUM ANALYZE를 실행하세요.
PostgreSQL은 연결당 프로세스를 생성합니다. PgBouncer 같은 Connection Pooler를 사용하고, max_connections을 무작정 늘리지 마세요.
JSONB는 유연하지만, 자주 조회/조인되는 필드는 정규화된 컬럼으로 분리하세요. 쿼리 최적화와 인덱싱이 더 효과적입니다.
적절한 인덱스 전략(B-tree, GIN, GiST 선택), 파티셔닝으로 대용량 테이블 관리, pg_stat_statements로 슬로우 쿼리 모니터링, 정기적인 pg_dump 백업.