🗄️ 데이터베이스

TiDB

MySQL Compatible Distributed NewSQL Database

MySQL 호환 분산 NewSQL 데이터베이스입니다. HTAP(Hybrid Transactional/Analytical Processing)을 지원하여 하나의 데이터베이스에서 OLTP와 OLAP 워크로드를 동시에 처리합니다.

📖 상세 설명

TiDB는 PingCAP에서 개발한 오픈소스 분산 SQL 데이터베이스입니다. Google의 Spanner와 F1 논문에서 영감을 받아 설계되었으며, MySQL 프로토콜을 완벽히 지원하여 기존 MySQL 애플리케이션을 그대로 마이그레이션할 수 있습니다.

HTAP (Hybrid Transactional/Analytical Processing): TiDB의 핵심 차별점입니다. TiKV(행 저장소)로 OLTP 처리, TiFlash(컬럼 저장소)로 OLAP 처리를 동시에 수행합니다. 실시간 분석이 필요한 서비스에서 별도의 데이터 웨어하우스 없이 즉시 분석 쿼리를 실행할 수 있습니다.

수평 확장(Horizontal Scaling): 노드를 추가하면 자동으로 데이터가 분산되고 처리량이 증가합니다. 수 TB에서 수백 TB까지 확장 가능하며, 쓰기/읽기 모두 선형적으로 확장됩니다.

강력한 일관성: Raft 합의 알고리즘을 사용하여 분산 환경에서도 강력한 일관성을 보장합니다. 기본적으로 Snapshot Isolation을 제공하며, Pessimistic/Optimistic 트랜잭션 모드를 선택할 수 있습니다.

클라우드 네이티브: Kubernetes 위에서 TiDB Operator를 통해 자동화된 배포, 스케일링, 백업/복구가 가능합니다. TiDB Cloud를 통해 완전 관리형 서비스도 제공됩니다.

💻 코드 예제

-- TiDB SQL 예제 (MySQL 호환)

-- ============================================
-- 기본 테이블 생성 (MySQL과 동일)
-- ============================================

CREATE DATABASE ecommerce;
USE ecommerce;

-- 사용자 테이블
CREATE TABLE users (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    email VARCHAR(255) UNIQUE NOT NULL,
    name VARCHAR(100) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    -- 샤딩 힌트 (선택적)
    INDEX idx_email (email)
) ENGINE=InnoDB;

-- 주문 테이블 (파티셔닝 예시)
CREATE TABLE orders (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    user_id BIGINT NOT NULL,
    total_amount DECIMAL(15,2) NOT NULL,
    status ENUM('pending', 'paid', 'shipped', 'delivered') DEFAULT 'pending',
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_user_id (user_id),
    INDEX idx_created_at (created_at)
) ENGINE=InnoDB
-- TiDB에서 Range 파티셔닝 (대용량 테이블 최적화)
PARTITION BY RANGE (YEAR(created_at)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION pmax VALUES LESS THAN MAXVALUE
);


-- ============================================
-- HTAP: TiFlash 컬럼 저장소 설정
-- ============================================

-- TiFlash 레플리카 추가 (실시간 분석용)
ALTER TABLE orders SET TIFLASH REPLICA 1;
ALTER TABLE users SET TIFLASH REPLICA 1;

-- TiFlash 동기화 상태 확인
SELECT * FROM information_schema.tiflash_replica;


-- ============================================
-- HTAP 쿼리 예시
-- ============================================

-- OLTP: 단순 조회 (TiKV 자동 사용)
SELECT * FROM orders WHERE user_id = 12345 LIMIT 10;

-- OLAP: 분석 쿼리 (TiFlash 자동 사용)
SELECT
    DATE_FORMAT(created_at, '%Y-%m') AS month,
    COUNT(*) AS order_count,
    SUM(total_amount) AS total_sales,
    AVG(total_amount) AS avg_order_value
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY month
ORDER BY month;

-- 강제로 TiFlash 사용 (힌트)
SELECT /*+ READ_FROM_STORAGE(TIFLASH[orders]) */
    status, COUNT(*) AS cnt
FROM orders
GROUP BY status;

-- 강제로 TiKV 사용 (힌트)
SELECT /*+ READ_FROM_STORAGE(TIKV[orders]) */
    * FROM orders WHERE id = 1;


-- ============================================
-- 분산 트랜잭션
-- ============================================

-- Pessimistic 트랜잭션 (기본값, MySQL과 동일)
START TRANSACTION;

UPDATE accounts SET balance = balance - 100
WHERE id = 'acc-001';

UPDATE accounts SET balance = balance + 100
WHERE id = 'acc-002';

-- 제약조건 확인
SELECT balance FROM accounts WHERE id = 'acc-001';

COMMIT;


-- ============================================
-- 유용한 시스템 쿼리
-- ============================================

-- 클러스터 상태 확인
SELECT * FROM information_schema.cluster_info;

-- 느린 쿼리 확인
SELECT * FROM information_schema.slow_query
ORDER BY query_time DESC LIMIT 10;

-- 리전 분포 확인
SELECT * FROM information_schema.tikv_region_status
WHERE table_name = 'orders';

-- 실행 계획 (분산 처리 확인)
EXPLAIN ANALYZE
SELECT * FROM orders WHERE user_id = 12345;
# TiDB + Python (mysql-connector 또는 pymysql)
import mysql.connector
from mysql.connector import pooling
from decimal import Decimal
from contextlib import contextmanager

# ============================================
# 연결 설정
# ============================================

# TiDB 연결 풀 (MySQL 드라이버 그대로 사용)
db_config = {
    'host': 'tidb-host.example.com',
    'port': 4000,  # TiDB 기본 포트
    'user': 'root',
    'password': 'your_password',
    'database': 'ecommerce',
    'pool_name': 'tidb_pool',
    'pool_size': 10,
    'autocommit': False,
}

connection_pool = mysql.connector.pooling.MySQLConnectionPool(**db_config)

@contextmanager
def get_connection():
    conn = connection_pool.get_connection()
    try:
        yield conn
    finally:
        conn.close()


# ============================================
# CRUD 작업
# ============================================

class OrderService:
    def create_order(self, user_id: int, items: list) -> dict:
        """주문 생성 - 분산 트랜잭션"""
        with get_connection() as conn:
            cursor = conn.cursor(dictionary=True)

            try:
                # TiDB는 기본적으로 Pessimistic 트랜잭션
                total = sum(item['price'] * item['quantity'] for item in items)

                # 주문 생성
                cursor.execute("""
                    INSERT INTO orders (user_id, total_amount, status)
                    VALUES (%s, %s, 'pending')
                """, (user_id, total))

                order_id = cursor.lastrowid

                # 주문 아이템 생성
                for item in items:
                    cursor.execute("""
                        INSERT INTO order_items (order_id, product_id, quantity, price)
                        VALUES (%s, %s, %s, %s)
                    """, (order_id, item['product_id'], item['quantity'], item['price']))

                    # 재고 차감
                    cursor.execute("""
                        UPDATE products
                        SET stock = stock - %s
                        WHERE id = %s AND stock >= %s
                    """, (item['quantity'], item['product_id'], item['quantity']))

                    if cursor.rowcount == 0:
                        raise ValueError(f"재고 부족: {item['product_id']}")

                conn.commit()

                return {'order_id': order_id, 'total': total}

            except Exception as e:
                conn.rollback()
                raise e

    def get_sales_analytics(self, start_date: str, end_date: str) -> list:
        """매출 분석 - TiFlash OLAP 쿼리"""
        with get_connection() as conn:
            cursor = conn.cursor(dictionary=True)

            # TiFlash 힌트로 컬럼 저장소 강제 사용
            cursor.execute("""
                SELECT /*+ READ_FROM_STORAGE(TIFLASH[orders, order_items]) */
                    DATE_FORMAT(o.created_at, '%%Y-%%m-%%d') AS date,
                    COUNT(DISTINCT o.id) AS order_count,
                    SUM(o.total_amount) AS total_sales,
                    COUNT(oi.id) AS item_count
                FROM orders o
                JOIN order_items oi ON o.id = oi.order_id
                WHERE o.created_at BETWEEN %s AND %s
                GROUP BY date
                ORDER BY date
            """, (start_date, end_date))

            return cursor.fetchall()


# ============================================
# 재시도 로직 (분산 트랜잭션 충돌 대비)
# ============================================

import time
import random

def execute_with_retry(fn, max_retries=3):
    """트랜잭션 충돌 시 재시도"""
    for attempt in range(max_retries):
        try:
            return fn()
        except mysql.connector.Error as e:
            # TiDB 트랜잭션 충돌 에러 코드
            if e.errno in (9007, 8028) and attempt < max_retries - 1:
                # 지수 백오프 + 지터
                wait_time = (2 ** attempt) + random.uniform(0, 1)
                print(f"재시도 {attempt + 1}/{max_retries}: {wait_time:.2f}초 대기")
                time.sleep(wait_time)
                continue
            raise


# ============================================
# 사용 예시
# ============================================

order_service = OrderService()

# OLTP: 주문 생성
result = execute_with_retry(
    lambda: order_service.create_order(
        user_id=12345,
        items=[
            {'product_id': 1, 'quantity': 2, 'price': Decimal('10000')},
            {'product_id': 2, 'quantity': 1, 'price': Decimal('25000')}
        ]
    )
)
print(f"주문 생성: {result}")

# OLAP: 실시간 분석 (TiFlash)
analytics = order_service.get_sales_analytics('2024-01-01', '2024-12-31')
print(f"매출 분석: {analytics}")
# TiDB CLI 및 운영 명령어

# ============================================
# TiUP으로 로컬 설치 (개발/테스트)
# ============================================

# TiUP 설치 (TiDB 패키지 매니저)
curl --proto '=https' --tlsv1.2 -sSf https://tiup-mirrors.pingcap.com/install.sh | sh

# 환경변수 적용
source ~/.bashrc

# 로컬 클러스터 실행 (TiDB + TiKV + PD + TiFlash)
tiup playground --db 1 --kv 3 --pd 1 --tiflash 1

# MySQL 클라이언트로 접속
mysql -h 127.0.0.1 -P 4000 -u root


# ============================================
# 프로덕션 클러스터 배포
# ============================================

# 클러스터 배포 설정 파일 (topology.yaml)
cat > topology.yaml << 'EOF'
global:
  user: "tidb"
  ssh_port: 22
  deploy_dir: "/tidb-deploy"
  data_dir: "/tidb-data"

server_configs:
  tidb:
    log.slow-threshold: 300

pd_servers:
  - host: 10.0.1.1
  - host: 10.0.1.2
  - host: 10.0.1.3

tidb_servers:
  - host: 10.0.1.4
  - host: 10.0.1.5

tikv_servers:
  - host: 10.0.1.6
  - host: 10.0.1.7
  - host: 10.0.1.8

tiflash_servers:
  - host: 10.0.1.9
EOF

# 클러스터 배포
tiup cluster deploy tidb-prod v7.5.0 ./topology.yaml --user root -p

# 클러스터 시작
tiup cluster start tidb-prod

# 클러스터 상태 확인
tiup cluster display tidb-prod


# ============================================
# 클러스터 관리
# ============================================

# 노드 스케일 아웃 (TiKV 추가)
tiup cluster scale-out tidb-prod scale-out.yaml

# 노드 스케일 인 (TiKV 제거)
tiup cluster scale-in tidb-prod --node 10.0.1.8:20160

# 설정 변경
tiup cluster edit-config tidb-prod

# 롤링 업그레이드
tiup cluster upgrade tidb-prod v7.6.0


# ============================================
# 백업 및 복구 (BR 도구)
# ============================================

# 전체 백업 (S3)
tiup br backup full \
    --pd "10.0.1.1:2379" \
    --storage "s3://bucket/backup" \
    --s3.region "ap-northeast-2"

# 데이터베이스 백업
tiup br backup db \
    --pd "10.0.1.1:2379" \
    --db "ecommerce" \
    --storage "s3://bucket/backup"

# 복구
tiup br restore full \
    --pd "10.0.1.1:2379" \
    --storage "s3://bucket/backup"


# ============================================
# 데이터 마이그레이션 (MySQL -> TiDB)
# ============================================

# TiDB Data Migration (DM) 설정
tiup dmctl --master-addr 10.0.1.1:8261 \
    operate-source create ./source.yaml

# 마이그레이션 태스크 시작
tiup dmctl --master-addr 10.0.1.1:8261 \
    start-task ./task.yaml


# ============================================
# 모니터링 (Grafana/Prometheus)
# ============================================

# TiDB Dashboard 접속 (기본 포트)
# http://PD_IP:2379/dashboard

# Grafana 접속 (TiUP으로 설치 시)
# http://GRAFANA_IP:3000

# 느린 쿼리 로그 확인
tiup ctl:v7.5.0 pd -u http://10.0.1.1:2379 log slow-query

🗣️ 실무에서 이렇게 말하세요

💬 기술 검토 회의에서
"실시간 대시보드 때문에 MySQL에서 분석 쿼리 돌리면 서비스 영향 있잖아요. TiDB로 가면 TiFlash가 분석 쿼리 처리해서 운영 DB 부하 없이 실시간 분석 가능해요. ETL 파이프라인 없이요."
💬 마이그레이션 계획에서
"TiDB는 MySQL 프로토콜 완벽 호환이라 드라이버 변경 없이 연결 문자열만 바꾸면 돼요. DM 도구로 MySQL 실시간 동기화하면서 점진적으로 옮길 수 있고요. 다만 일부 MySQL 전용 함수는 체크해봐야 해요."
💬 성능 튜닝 논의에서
"이 분석 쿼리가 TiKV로 가서 느린 것 같아요. TiFlash 레플리카 설정했는지 확인하고, 힌트로 강제 지정해보세요. information_schema.tiflash_replica에서 동기화 상태도 체크해보시고요."

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

AUTO_INCREMENT 핫스팟

AUTO_INCREMENT는 순차적이라 핫스팟 발생 가능. AUTO_RANDOM 또는 SHARD_ROW_ID_BITS 사용을 권장합니다.

TiFlash 동기화 지연

TiFlash는 비동기 복제라 최신 데이터가 지연될 수 있습니다. 실시간성이 중요하면 TiKV 힌트를 사용하세요.

MySQL 비호환 기능

저장 프로시저, 트리거, 일부 함수는 제한적입니다. 마이그레이션 전 호환성 체크 도구로 검증하세요.

TiDB 베스트 프랙티스

대용량 테이블에 TiFlash 레플리카, AUTO_RANDOM으로 핫스팟 방지, 정기적 ANALYZE, PD 3노드 이상 구성.

🔗 관련 용어

📚 더 배우기