🗄️ 데이터베이스

PostgreSQL

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를 구현할 수 있습니다."

⚠️ 주의사항 & 베스트 프랙티스

VACUUM 방치 금지

MVCC로 인해 dead tuple이 쌓입니다. autovacuum 설정을 확인하고, 대량 DELETE/UPDATE 후에는 수동 VACUUM ANALYZE를 실행하세요.

Connection 고갈 주의

PostgreSQL은 연결당 프로세스를 생성합니다. PgBouncer 같은 Connection Pooler를 사용하고, max_connections을 무작정 늘리지 마세요.

JSONB 과용 금지

JSONB는 유연하지만, 자주 조회/조인되는 필드는 정규화된 컬럼으로 분리하세요. 쿼리 최적화와 인덱싱이 더 효과적입니다.

PostgreSQL 베스트 프랙티스

적절한 인덱스 전략(B-tree, GIN, GiST 선택), 파티셔닝으로 대용량 테이블 관리, pg_stat_statements로 슬로우 쿼리 모니터링, 정기적인 pg_dump 백업.

🔗 관련 용어

📚 더 배우기