[2026] PostgreSQL vs MySQL 차이와 선택 가이드 | 스키마·트랜잭션·운영

[2026] PostgreSQL vs MySQL 차이와 선택 가이드 | 스키마·트랜잭션·운영

이 글의 핵심

PostgreSQL vs MySQL 차이 선택: 스키마·ACID·쿼리·복제·운영 관점에서 비교하고 Node.js·팀 상황에 맞는 선택 기준을 실무 중심으로 정리합니다.

들어가며

PostgreSQLMySQL(및 MariaDB)은 관계형 DB 시장에서 가장 자주 마주치는 선택지입니다. 둘 다 ACID 트랜잭션, 복제, 풍부한 클라이언트 라이브러리를 갖추지만, 스키마 표현력, 쿼리 최적화기, 운영 도구, 호스팅 생태에서 차이가 납니다. “어느 쪽이 더 빠르다” 한 줄로 끝나지 않고, 워크로드·팀 역량·레거시에 맞춰 고르는 것이 안전합니다. 이 글은 벤더 홍보가 아니라 실무에서 의사결정에 쓰는 비교 축—스키마, 트랜잭션, SQL 기능, 운영—을 정리합니다. 비유로 말씀드리면, PostgreSQL규칙과 도구를 많이 갖춘 설계 사무실, MySQL가볍게 짓고 널리 호스팅되는 현장에 가깝습니다. 둘 다 집을 짓지만(데이터를 저장하지만), 복잡한 제약·분석 쿼리를 DB에 두고 싶은지, 운영·호스팅 친숙도를 우선할지가 갈림길이 됩니다.

언제 PostgreSQL을, 언제 MySQL(MariaDB)을 쓰나요?

관점PostgreSQL을 검토하시면 좋은 경우MySQL/MariaDB를 검토하시면 좋은 경우
성능·기능복잡한 SQL, jsonb, 배열·범위 타입, 분석·윈도 함수에 강하게 의존할 때단순 CRUD, 익숙한 레이어·호스팅 위주일 때
사용성스키마·제약·DDL을 트랜잭션 안에서 다루는 패턴이 중요할 때기존 팀·레거시·매뉴얼이 MySQL 중심일 때
적용 시나리오데이터 무결성·표현력을 DB에 두는 도메인웹 호스팅·레플리카 생태·주변 도구와의 궁합

이 글을 읽으면

  • 스키마·제약·인덱스 관점에서 PostgreSQL vs MySQL 차이를 짚을 수 있습니다
  • 트랜잭션 격리·락·복제를 운영 관점에서 비교합니다
  • Node.js 스택과 함께 쓸 때의 ORM·드라이버 선택 힌트를 얻습니다

목차

  1. 개념: RDB 공통점과 비교 축
  2. 스키마·데이터 타입
  3. 트랜잭션·동시성
  4. 쿼리·최적화·인덱싱
  5. 운영·복제·생태계
  6. 성능 비교: 벤치마크와 트레이드오프
  7. 실무 사례
  8. 트러블슈팅
  9. 마무리

개념: RDB 공통점과 비교 축

기본 개념

둘 다 SQL을 사용하는 관계형 데이터베이스이며, 테이블·행·열, 기본키·외래키, 조인으로 데이터를 모델링합니다. Node.js에서는 pg, mysql2, Prisma, Sequelize, Knex 등으로 접근합니다.

왜 “하나의 정답”이 없는가

성능은 스키마 설계, 인덱스, 쿼리 패턴, 하드웨어, 설정에 좌우됩니다. 따라서 기능 적합성(필요한 SQL·타입·무결성)과 운영 비용(모니터링, 백업, 인력)이 더 중요한 결정 요인인 경우가 많습니다.

스키마·데이터 타입

스키마 구조 차이

PostgreSQL: 계층적 네임스페이스 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 하나의 데이터베이스에 여러 스키마
CREATE DATABASE myapp;
\c myapp
CREATE SCHEMA sales;
CREATE SCHEMA inventory;
CREATE TABLE sales.orders (id SERIAL PRIMARY KEY, ...);
CREATE TABLE inventory.products (id SERIAL PRIMARY KEY, ...);
-- 스키마 경로 설정
SET search_path TO sales, public;

MySQL: 데이터베이스 = 스키마 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- 각 스키마가 별도 데이터베이스
CREATE DATABASE sales;
CREATE DATABASE inventory;
USE sales;
CREATE TABLE orders (id INT AUTO_INCREMENT PRIMARY KEY, ...);
USE inventory;
CREATE TABLE products (id INT AUTO_INCREMENT PRIMARY KEY, ...);

고급 데이터 타입

PostgreSQL의 풍부한 타입: 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 배열 타입
CREATE TABLE posts (
    id SERIAL PRIMARY KEY,
    title TEXT,
    tags TEXT[]  -- 문자열 배열
);
INSERT INTO posts (title, tags) 
VALUES ('C++ 가이드', ARRAY['cpp', 'programming', 'tutorial']);
SELECT * FROM posts WHERE 'cpp' = ANY(tags);
-- 범위 타입
CREATE TABLE reservations (
    id SERIAL PRIMARY KEY,
    room_id INT,
    period TSRANGE  -- 시간 범위
);
INSERT INTO reservations (room_id, period)
VALUES (101, '[2026-03-31 14:00, 2026-03-31 16:00)');
-- 겹치는 예약 검색
SELECT * FROM reservations 
WHERE period && '[2026-03-31 15:00, 2026-03-31 17:00)';
-- UUID 타입
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name TEXT
);
-- JSONB (바이너리 JSON, 인덱싱 가능)
CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT,
    specs JSONB
);
INSERT INTO products (name, specs)
VALUES ('노트북', '{"cpu": "i7", "ram": "16GB", "storage": "512GB SSD"}');
-- JSONB 쿼리
SELECT * FROM products WHERE specs->>'cpu' = 'i7';
SELECT * FROM products WHERE specs @> '{"ram": "16GB"}';
-- JSONB 인덱스
CREATE INDEX idx_specs_gin ON products USING GIN (specs);

MySQL의 JSON 지원: 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- JSON 타입 (MySQL 5.7+)
CREATE TABLE products (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    specs JSON
);
INSERT INTO products (name, specs)
VALUES ('노트북', '{"cpu": "i7", "ram": "16GB", "storage": "512GB SSD"}');
-- JSON 쿼리
SELECT * FROM products WHERE JSON_EXTRACT(specs, '$.cpu') = 'i7';
SELECT * FROM products WHERE specs->>'$.cpu' = 'i7';  -- MySQL 8.0+
-- JSON 가상 컬럼 + 인덱스
ALTER TABLE products 
ADD COLUMN cpu VARCHAR(50) AS (specs->>'$.cpu') VIRTUAL;
CREATE INDEX idx_cpu ON products(cpu);

CHECK 제약 및 도메인 무결성

PostgreSQL: 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

CREATE TABLE employees (
    id SERIAL PRIMARY KEY,
    name TEXT NOT NULL,
    age INT CHECK (age >= 18 AND age <= 100),
    email TEXT CHECK (email ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$'),
    salary NUMERIC(10, 2) CHECK (salary > 0),
    department TEXT CHECK (department IN ('engineering', 'sales', 'hr'))
);
-- 도메인 정의 (재사용 가능한 타입)
CREATE DOMAIN email_type AS TEXT
CHECK (VALUE ~* '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}$');
CREATE TABLE users (
    id SERIAL PRIMARY KEY,
    email email_type
);

MySQL: 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- CHECK 제약 (MySQL 8.0.16+)
CREATE TABLE employees (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    age INT CHECK (age >= 18 AND age <= 100),
    email VARCHAR(255) CHECK (email REGEXP '^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Z|a-z]{2,}$'),
    salary DECIMAL(10, 2) CHECK (salary > 0),
    department ENUM('engineering', 'sales', 'hr')  -- ENUM 타입 활용
);

부분 인덱스

PostgreSQL: 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- 조건부 인덱스 (활성 사용자만)
CREATE INDEX idx_active_users ON users(email) WHERE active = true;
-- 특정 범위만 인덱싱
CREATE INDEX idx_recent_orders ON orders(created_at) 
WHERE created_at > '2026-01-01';
-- 인덱스 크기 절감 + 쿼리 속도 향상

MySQL: 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- 부분 인덱스 미지원 (전체 인덱스만 가능)
CREATE INDEX idx_users_email ON users(email);
-- 대안: 필터링된 뷰 + 인덱스
CREATE VIEW active_users AS 
SELECT * FROM users WHERE active = true;

실무 해석: 도메인이 복잡한 제약·타입을 DB에 두고 싶다면 PostgreSQL이 유리한 경우가 많습니다. 단순한 키·값 CRUD 위주면 둘 다 충분한 경우가 많습니다.


트랜잭션·동시성

일상 비유로 이해하기: 동시성은 주방에서 여러 요리를 동시에 하는 것과 비슷합니다. 한 명의 요리사(싱글 스레드)가 국을 끓이다가 불을 줄이고, 그 사이에 야채를 썰고, 다시 국을 확인하는 식이죠. 반면 병렬성은 요리사 여러 명(멀티 스레드)이 각자 다른 요리를 동시에 만드는 겁니다. {#transactions}

MVCC (Multi-Version Concurrency Control)

둘 다 MVCC를 사용하지만 구현 방식이 다릅니다. PostgreSQL: 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 트랜잭션 격리 수준 설정
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- 동시성 충돌 시 자동 재시도 필요
-- Serialization failure 에러 발생 가능

특징:

  • 읽기는 락을 걸지 않음 (MVCC)
  • VACUUM으로 오래된 버전 정리 필요
  • Serializable 격리 수준이 정확히 동작 MySQL (InnoDB): 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
-- 기본 격리 수준: REPEATABLE READ
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;

특징:

  • 읽기도 MVCC 사용
  • Undo 로그로 이전 버전 관리
  • Gap Lock으로 팬텀 읽기 방지

격리 수준 비교

격리 수준PostgreSQLMySQL (InnoDB)
READ UNCOMMITTED지원 (비권장)지원
READ COMMITTED기본값지원
REPEATABLE READ지원기본값
SERIALIZABLESSI 알고리즘락 기반

락 메커니즘

PostgreSQL: 명시적 락 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 행 락
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 공유 락 (읽기 락)
SELECT * FROM orders WHERE id = 1 FOR SHARE;
-- 테이블 락
LOCK TABLE orders IN EXCLUSIVE MODE;
-- Advisory Lock (애플리케이션 레벨 락)
SELECT pg_advisory_lock(12345);
-- 작업 수행
SELECT pg_advisory_unlock(12345);

MySQL: 행 락 및 갭 락 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 행 락
SELECT * FROM orders WHERE id = 1 FOR UPDATE;
-- 공유 락
SELECT * FROM orders WHERE id = 1 LOCK IN SHARE MODE;
-- Gap Lock (범위 락, REPEATABLE READ에서 자동)
SELECT * FROM orders WHERE id BETWEEN 10 AND 20 FOR UPDATE;
-- id 10-20 사이의 "존재하지 않는 행"도 락
-- Next-Key Lock = Record Lock + Gap Lock

DDL 트랜잭션

PostgreSQL: DDL도 트랜잭션 가능 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

BEGIN;
CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT);
ALTER TABLE users ADD COLUMN email TEXT;
CREATE INDEX idx_email ON users(email);
-- 에러 발생 시 모든 DDL 롤백
ROLLBACK;
-- 성공 시 커밋
COMMIT;

MySQL: DDL은 암묵적 커밋 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

START TRANSACTION;
INSERT INTO logs (message) VALUES ('start');
CREATE TABLE temp (id INT);  -- 여기서 자동 COMMIT 발생!
INSERT INTO logs (message) VALUES ('end');  -- 새 트랜잭션
ROLLBACK;  -- 'end'만 롤백, 'start'는 이미 커밋됨

실무 영향:

  • PostgreSQL: 마이그레이션 스크립트를 트랜잭션으로 감싸서 안전하게 실행
  • MySQL: DDL 실패 시 수동 롤백 필요, 신중한 스크립트 작성 필요

동시성 제어 예제

PostgreSQL: Optimistic Locking 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

CREATE TABLE products (
    id SERIAL PRIMARY KEY,
    name TEXT,
    stock INT,
    version INT DEFAULT 0  -- 버전 컬럼
);
-- 재고 감소 (낙관적 락)
UPDATE products 
SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;  -- 현재 버전 확인
-- 영향받은 행이 0이면 충돌 발생 → 재시도

MySQL: Pessimistic Locking 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- 재고 감소 (비관적 락)
START TRANSACTION;
SELECT stock FROM products WHERE id = 1 FOR UPDATE;
-- 다른 트랜잭션은 대기
UPDATE products SET stock = stock - 1 WHERE id = 1;
COMMIT;

실무 해석: 긴 트랜잭션·복잡한 리포트 쿼리가 많다면 데드락·대기 프로파일링이 필수이며, 엔진별로 기본 격리 수준인덱스 설계가 다르게 먹힙니다.


쿼리·최적화·인덱싱

CTE (Common Table Expression)

PostgreSQL: 재귀 CTE 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 조직도 계층 조회
WITH RECURSIVE org_tree AS (
    -- 기본 케이스: 최상위 관리자
    SELECT id, name, manager_id, 1 as level
    FROM employees
    WHERE manager_id IS NULL
    
    UNION ALL
    
    -- 재귀: 하위 직원
    SELECT e.id, e.name, e.manager_id, ot.level + 1
    FROM employees e
    INNER JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT * FROM org_tree ORDER BY level, name;
-- 재귀 CTE로 그래프 탐색
WITH RECURSIVE path AS (
    SELECT id, name, ARRAY[id] as path
    FROM nodes
    WHERE id = 1  -- 시작 노드
    
    UNION ALL
    
    SELECT n.id, n.name, p.path || n.id
    FROM nodes n
    INNER JOIN edges e ON n.id = e.to_id
    INNER JOIN path p ON e.from_id = p.id
    WHERE NOT (n.id = ANY(p.path))  -- 순환 방지
)
SELECT * FROM path;

MySQL: 재귀 CTE (8.0+) 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 동일한 재귀 CTE 지원
-- 실행 예제
WITH RECURSIVE org_tree AS (
    SELECT id, name, manager_id, 1 as level
    FROM employees
    WHERE manager_id IS NULL
    
    UNION ALL
    
    SELECT e.id, e.name, e.manager_id, ot.level + 1
    FROM employees e
    INNER JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT * FROM org_tree ORDER BY level, name;

윈도 함수

PostgreSQL: 고급 윈도 함수 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 순위 함수
SELECT 
    name,
    department,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank,
    DENSE_RANK() OVER (PARTITION BY department ORDER BY salary DESC) as dense_rank,
    ROW_NUMBER() OVER (PARTITION BY department ORDER BY salary DESC) as row_num
FROM employees;
-- 이동 평균
SELECT 
    date,
    revenue,
    AVG(revenue) OVER (
        ORDER BY date 
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) as moving_avg_7days
FROM daily_sales;
-- FILTER 절 (조건부 집계)
SELECT 
    department,
    COUNT(*) as total,
    COUNT(*) FILTER (WHERE salary > 50000) as high_earners
FROM employees
GROUP BY department;

MySQL: 윈도 함수 (8.0+) 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 기본 윈도 함수 지원
SELECT 
    name,
    department,
    salary,
    RANK() OVER (PARTITION BY department ORDER BY salary DESC) as rank
FROM employees;
-- 이동 평균
SELECT 
    date,
    revenue,
    AVG(revenue) OVER (
        ORDER BY date 
        ROWS BETWEEN 6 PRECEDING AND CURRENT ROW
    ) as moving_avg_7days
FROM daily_sales;

풀 텍스트 검색

PostgreSQL: 내장 FTS 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- tsvector 타입으로 검색 인덱스
CREATE TABLE articles (
    id SERIAL PRIMARY KEY,
    title TEXT,
    content TEXT,
    search_vector TSVECTOR
);
-- 검색 벡터 생성
UPDATE articles 
SET search_vector = 
    setweight(to_tsvector('korean', title), 'A') ||
    setweight(to_tsvector('korean', content), 'B');
-- GIN 인덱스
CREATE INDEX idx_search ON articles USING GIN(search_vector);
-- 검색 (한국어 지원)
SELECT title, ts_rank(search_vector, query) as rank
FROM articles, to_tsquery('korean', 'C++ & 메모리') as query
WHERE search_vector @@ query
ORDER BY rank DESC;
-- 하이라이트
SELECT ts_headline('korean', content, to_tsquery('korean', 'C++'))
FROM articles
WHERE id = 1;

MySQL: FULLTEXT 인덱스 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- FULLTEXT 인덱스 생성
CREATE TABLE articles (
    id INT AUTO_INCREMENT PRIMARY KEY,
    title VARCHAR(255),
    content TEXT,
    FULLTEXT KEY idx_fulltext (title, content)
) ENGINE=InnoDB;
-- 자연어 검색
SELECT title, MATCH(title, content) AGAINST('C++ 메모리') as score
FROM articles
WHERE MATCH(title, content) AGAINST('C++ 메모리' IN NATURAL LANGUAGE MODE)
ORDER BY score DESC;
-- 불린 모드 (AND, OR, NOT)
SELECT title
FROM articles
WHERE MATCH(title, content) AGAINST('+C++ +메모리 -포인터' IN BOOLEAN MODE);

쿼리 최적화 및 EXPLAIN

PostgreSQL: 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 상세한 실행 계획
EXPLAIN (ANALYZE, BUFFERS, VERBOSE) 
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2026-01-01';
-- 출력 예시:
-- Buffers: shared hit=1234 read=56
-- Planning Time: 0.123 ms
-- Execution Time: 45.678 ms

MySQL: 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 실행 계획
EXPLAIN FORMAT=JSON
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2026-01-01';
-- 실제 실행 분석
EXPLAIN ANALYZE
SELECT * FROM orders o
JOIN customers c ON o.customer_id = c.id
WHERE o.created_at > '2026-01-01';

인덱스 전략

PostgreSQL: 다양한 인덱스 타입 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- B-tree (기본)
CREATE INDEX idx_email ON users(email);
-- Hash 인덱스 (등호 비교만)
CREATE INDEX idx_hash_email ON users USING HASH(email);
-- GIN (배열, JSONB, 풀텍스트)
CREATE INDEX idx_tags ON posts USING GIN(tags);
-- GiST (지리 데이터, 범위)
CREATE INDEX idx_location ON stores USING GIST(location);
-- BRIN (대용량 시계열 데이터)
CREATE INDEX idx_created ON logs USING BRIN(created_at);
-- 복합 인덱스
CREATE INDEX idx_user_date ON orders(user_id, created_at DESC);
-- 표현식 인덱스
CREATE INDEX idx_lower_email ON users(LOWER(email));

MySQL: 주로 B-tree 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- B-tree 인덱스 (기본)
CREATE INDEX idx_email ON users(email);
-- 복합 인덱스
CREATE INDEX idx_user_date ON orders(user_id, created_at DESC);
-- 함수 기반 인덱스 (MySQL 8.0+)
CREATE INDEX idx_lower_email ON users((LOWER(email)));
-- FULLTEXT 인덱스
CREATE FULLTEXT INDEX idx_content ON articles(content);
-- 공간 인덱스
CREATE SPATIAL INDEX idx_location ON stores(location);

쿼리 힌트

PostgreSQL: 설정 기반 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- 특정 쿼리에만 설정 변경
SET LOCAL enable_seqscan = off;  -- 순차 스캔 비활성화
SELECT * FROM large_table WHERE id > 1000;
-- 병렬 쿼리 설정
SET max_parallel_workers_per_gather = 4;

MySQL: 힌트 구문 아래 코드는 sql를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 인덱스 힌트
SELECT * FROM orders USE INDEX (idx_created) 
WHERE created_at > '2026-01-01';
-- 조인 순서 힌트
SELECT /*+ JOIN_ORDER(o, c) */ *
FROM orders o
JOIN customers c ON o.customer_id = c.id;
-- 인덱스 강제
SELECT * FROM orders FORCE INDEX (idx_user_date)
WHERE user_id = 123;

실무 해석: PostgreSQL은 오래부터 분석 쿼리에 강점이 있다는 평가가 많습니다. 복잡한 CTE, 윈도 함수, 다양한 인덱스 타입이 필요하면 PostgreSQL이 유리합니다.


운영·복제·생태계

복제 (Replication)

PostgreSQL: 스트리밍 복제 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.

-- 마스터 설정 (postgresql.conf)
wal_level = replica
max_wal_senders = 3
wal_keep_size = 1GB
-- 슬레이브 설정
primary_conninfo = 'host=master port=5432 user=replicator password=...'

특징:

  • 물리적 복제 (바이트 단위)
  • 논리적 복제 (테이블 단위, PostgreSQL 10+)
  • 동기/비동기 복제 선택 가능 논리적 복제 예제: 아래 코드는 sql를 사용한 구현 예제입니다. 코드를 직접 실행해보면서 동작을 확인해보세요.
-- 마스터: 발행
CREATE PUBLICATION my_pub FOR TABLE users, orders;
-- 슬레이브: 구독
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=master dbname=mydb user=replicator'
PUBLICATION my_pub;

MySQL: 바이너리 로그 복제 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 마스터 설정 (my.cnf)
-- 실행 예제
[mysqld]
server-id = 1
log-bin = mysql-bin
binlog-format = ROW
-- 슬레이브 설정
[mysqld]
server-id = 2
relay-log = relay-bin
-- 슬레이브 시작
CHANGE MASTER TO
    MASTER_HOST='master',
    MASTER_USER='replicator',
    MASTER_PASSWORD='...',
    MASTER_LOG_FILE='mysql-bin.000001',
    MASTER_LOG_POS=154;
START SLAVE;

특징:

  • Statement-based, Row-based, Mixed 복제
  • GTID (Global Transaction ID) 지원
  • 멀티 소스 복제 가능

백업 및 복구

PostgreSQL: 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 논리적 백업
pg_dump mydb > backup.sql
pg_dump -Fc mydb > backup.dump  # 커스텀 포맷 (압축)
# 복구
psql mydb < backup.sql
pg_restore -d mydb backup.dump
# 물리적 백업 (PITR - Point-In-Time Recovery)
pg_basebackup -D /backup/base -Fp -Xs -P
# WAL 아카이빙으로 특정 시점 복구 가능

MySQL: 아래 코드는 bash를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# 논리적 백업
mysqldump mydb > backup.sql
mysqldump --single-transaction mydb > backup.sql  # 일관성 보장
# 복구
mysql mydb < backup.sql
# 물리적 백업 (Percona XtraBackup)
xtrabackup --backup --target-dir=/backup/
xtrabackup --prepare --target-dir=/backup/
xtrabackup --copy-back --target-dir=/backup/

모니터링

PostgreSQL: 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 활성 쿼리 확인
-- 실행 예제
SELECT pid, usename, state, query, now() - query_start as duration
FROM pg_stat_activity
WHERE state != 'idle'
ORDER BY duration DESC;
-- 느린 쿼리 찾기
SELECT query, calls, total_exec_time, mean_exec_time
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
-- 테이블 통계
SELECT schemaname, tablename, n_live_tup, n_dead_tup
FROM pg_stat_user_tables
ORDER BY n_dead_tup DESC;
-- 인덱스 사용률
SELECT schemaname, tablename, indexname, idx_scan, idx_tup_read
FROM pg_stat_user_indexes
WHERE idx_scan = 0  -- 사용되지 않는 인덱스
ORDER BY pg_relation_size(indexrelid) DESC;
-- 캐시 히트율
SELECT 
    sum(heap_blks_read) as heap_read,
    sum(heap_blks_hit) as heap_hit,
    sum(heap_blks_hit) / (sum(heap_blks_hit) + sum(heap_blks_read)) as ratio
FROM pg_statio_user_tables;

MySQL: 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 활성 쿼리 확인
SELECT id, user, host, db, command, time, state, info
FROM information_schema.processlist
WHERE command != 'Sleep'
ORDER BY time DESC;
-- 느린 쿼리 (Performance Schema)
SELECT 
    DIGEST_TEXT,
    COUNT_STAR as exec_count,
    AVG_TIMER_WAIT/1000000000000 as avg_sec,
    SUM_TIMER_WAIT/1000000000000 as total_sec
FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;
-- 테이블 통계
SELECT 
    table_schema,
    table_name,
    table_rows,
    data_length,
    index_length
FROM information_schema.tables
WHERE table_schema NOT IN ('mysql', 'information_schema', 'performance_schema')
ORDER BY data_length DESC;
-- 인덱스 사용률
SELECT 
    object_schema,
    object_name,
    index_name,
    count_star,
    count_read
FROM performance_schema.table_io_waits_summary_by_index_usage
WHERE index_name IS NOT NULL
ORDER BY count_star DESC;

연결 풀링

PostgreSQL: pgBouncer 아래 코드는 ini를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# pgbouncer.ini
// 실행 예제
[databases]
mydb = host=localhost port=5432 dbname=mydb
[pgbouncer]
listen_addr = 0.0.0.0
listen_port = 6432
auth_type = md5
pool_mode = transaction  # session, transaction, statement
max_client_conn = 1000
default_pool_size = 25

Node.js 연동: 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

const { Pool } = require('pg');
const pool = new Pool({
    host: 'localhost',
    port: 6432,  // pgBouncer 포트
    database: 'mydb',
    user: 'myuser',
    password: 'mypass',
    max: 20,  // 애플리케이션 레벨 풀
    idleTimeoutMillis: 30000
});
const result = await pool.query('SELECT * FROM users WHERE id = $1', [123]);

MySQL: ProxySQL 아래 코드는 ini를 사용한 구현 예제입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

# proxysql.cnf
mysql_servers =
(
    { address="127.0.0.1", port=3306, hostgroup=0 }
)
mysql_users =
(
    { username="myuser", password="mypass", default_hostgroup=0 }
)

Node.js 연동: 아래 코드는 javascript를 사용한 구현 예제입니다. 비동기 처리를 통해 효율적으로 작업을 수행합니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

const mysql = require('mysql2/promise');
const pool = mysql.createPool({
    host: 'localhost',
    port: 6033,  // ProxySQL 포트
    database: 'mydb',
    user: 'myuser',
    password: 'mypass',
    waitForConnections: true,
    connectionLimit: 20,
    queueLimit: 0
});
const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [123]);

확장 및 플러그인

PostgreSQL: 풍부한 확장 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- PostGIS (지리 데이터)
CREATE EXTENSION postgis;
CREATE TABLE stores (
    id SERIAL PRIMARY KEY,
    name TEXT,
    location GEOGRAPHY(POINT, 4326)
);
-- 반경 검색
SELECT name FROM stores
WHERE ST_DWithin(
    location,
    ST_MakePoint(127.0, 37.5)::geography,
    1000  -- 1km
);
-- pg_trgm (유사 문자열 검색)
CREATE EXTENSION pg_trgm;
CREATE INDEX idx_name_trgm ON users USING GIN(name gin_trgm_ops);
SELECT * FROM users WHERE name % 'Alice';  -- 유사도 검색
-- uuid-ossp (UUID 생성)
CREATE EXTENSION "uuid-ossp";
SELECT uuid_generate_v4();

MySQL: 플러그인 다음은 sql를 활용한 상세한 구현 코드입니다. 각 부분의 역할을 이해하면서 코드를 살펴보시기 바랍니다.

-- 공간 데이터 (내장)
-- 실행 예제
CREATE TABLE stores (
    id INT AUTO_INCREMENT PRIMARY KEY,
    name VARCHAR(255),
    location POINT NOT NULL SRID 4326
);
CREATE SPATIAL INDEX idx_location ON stores(location);
-- 반경 검색
SELECT name FROM stores
WHERE ST_Distance_Sphere(
    location,
    ST_GeomFromText('POINT(127.0 37.5)', 4326)
) < 1000;

실무 해석: 팀에 MySQL 운영 경험이 압도적으로 많다면 비용이 낮습니다. PostGIS 등 지리 확장이 필요하면 PostgreSQL이 자연스럽습니다.


성능 비교: 벤치마크와 트레이드오프

공개 TPC-C, sysbench 결과는 하드웨어·버전·튜닝에 따라 매번 달라집니다. 아래는 방향성만 잡는 체크리스트입니다.

시나리오메모
단순 PK 조회·소량 쓰기둘 다 매우 빠름—인덱스가 핵심
복잡한 조인·집계스키마·통계·병렬 쿼리 설정에 따라 역전
고동시 쓰기연결 풀·파티셔닝·샤딩 설계가 DB 종류보다 큰 경우 많음
트레이드오프: “조금 더 빠른 벤치”보다 백업·복구 시간, 장애 시 RPO/RTO, 마이그레이션 난이도를 함께 적으면 결정이 안정됩니다.

실무 사례

  • 금융·재고 등 강한 무결성: CHECK·외래키·트랜잭션 경계를 DB에 두는 편—PostgreSQL 선호 사례가 많음.
  • 기존 CMS·쇼핑몰 스택: MySQL이 기본인 경우 레거시 호환 우선.
  • JSON 반정규화 + 인덱스: PostgreSQL jsonb로 시작 후 검색 요구가 커지면 전문 검색 엔진을 병행. Node 연동은 Node.js 데이터베이스 연동Docker Compose 스택을 함께 보세요. 캐시·엣지·클러스터는 Redis 캐싱, Nginx, Kubernetes(minikube) 순으로, 운영 중 디스크 이슈는 Linux 디스크/inode와 연결됩니다.

트러블슈팅

증상점검
이관 후 쿼리만 느림실행 계획·통계 갱신·인덱스 타입 차이
문자열 정렬 불일치콜레이션·대소문자 규칙
날짜·타임존 버그TIMESTAMP vs TIMESTAMPTZ(PostgreSQL), MySQL의 타임존 처리
ORM 마이그레이션 실패벤더별 DDL 차이—수동 SQL 분기

마무리

  • PostgreSQL은 타입·SQL 표현력·확장(예: PostGIS)에서 강점이 자주 언급되고, MySQL은 레거시·호스팅·단순 워크로드와의 궁합이 좋습니다.
  • 실제 선택은 팀 운영 역량, 워크로드, 버전으로 확정하세요.
  • 캐시 계층은 Redis 캐싱 패턴에서 이어서 설계할 수 있습니다.
... 996 lines not shown ... Token usage: 63706/1000000; 936294 remaining Start-Sleep -Seconds 3