[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"article-deep-evm-25-postgresql-teibeur-patisyeoning-cheongman-haeng":3},{"article":4,"author":55},{"id":5,"category_id":6,"title":7,"slug":8,"excerpt":9,"content_md":10,"content_html":11,"locale":12,"author_id":13,"published":14,"published_at":15,"meta_title":16,"meta_description":17,"focus_keyword":18,"og_image":19,"canonical_url":19,"robots_meta":20,"created_at":15,"updated_at":15,"tags":21,"category_name":24,"related_articles":35},"d5000000-0000-0000-0000-000000000125","a0000000-0000-0000-0000-000000000055","Deep EVM #25: PostgreSQL 테이블 파티셔닝 — 테이블이 1천만 행을 넘을 때","deep-evm-25-postgresql-teibeur-patisyeoning-cheongman-haeng","PostgreSQL 테이블 파티셔닝 실용 가이드. 범위, 리스트, 해시 파티셔닝과 실제 예제, 마이그레이션 전략, 쿼리 계획을 다룹니다.","## 파티셔닝이 필요한 시점\n\n작게 시작한 `transactions` 테이블이 이제 3,400만 행에 도달했습니다. 50ms 걸리던 쿼리가 이제 5초 걸립니다. VACUUM은 몇 시간 동안 실행되며 다른 테이블의 autovacuum을 차단합니다. 인덱스 재구축은 테이블을 몇 분간 오프라인으로 만듭니다. 데이터베이스가 느린 것이 아닙니다 — 테이블이 단일 힙 파일로 너무 커진 것입니다.\n\n테이블 파티셔닝은 논리적 테이블을 여러 물리적 테이블(파티션)로 분할합니다. PostgreSQL의 쿼리 플래너가 올바른 파티션으로 쿼리를 자동 라우팅하여 필요한 데이터만 스캔합니다.\n\n### 파티셔닝의 경험 법칙\n\n- **파티셔닝할 때:** 테이블이 1천만 행 초과, 쿼리가 일관되게 테이블의 20% 이상 스캔, VACUUM이 데드 튜플을 따라잡지 못할 때\n- **파티셔닝하지 말 때:** 테이블이 100만 행 미만(오버헤드가 이익 초과), 쿼리가 항상 인덱스를 타는 경우(파티션 프루닝 불필요), 쓰기 패턴이 랜덤(자연스러운 파티션 키 없음)\n\n## 파티션 전략\n\nPostgreSQL은 세 가지 네이티브 파티셔닝 전략을 지원합니다:\n\n### 범위 파티셔닝\n\n값 범위로 분할합니다. 시계열 데이터에 이상적입니다:\n\n```sql\nCREATE TABLE transactions (\n    id          BIGINT GENERATED ALWAYS AS IDENTITY,\n    block_number BIGINT NOT NULL,\n    tx_hash     BYTEA NOT NULL,\n    from_addr   BYTEA NOT NULL,\n    to_addr     BYTEA NOT NULL,\n    value       NUMERIC NOT NULL,\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n) PARTITION BY RANGE (block_number);\n\n-- 월별 파티션\nCREATE TABLE transactions_2024_01 PARTITION OF transactions\n    FOR VALUES FROM (18900000) TO (19100000);\nCREATE TABLE transactions_2024_02 PARTITION OF transactions\n    FOR VALUES FROM (19100000) TO (19300000);\nCREATE TABLE transactions_2024_03 PARTITION OF transactions\n    FOR VALUES FROM (19300000) TO (19500000);\n```\n\n범위 파티셔닝은 블록 번호나 타임스탬프 기반 쿼리에서 파티션 프루닝의 이점을 극대화합니다.\n\n### 리스트 파티셔닝\n\n특정 값 목록으로 분할합니다. 카테고리별 분리에 유용합니다:\n\n```sql\nCREATE TABLE events (\n    id         BIGINT GENERATED ALWAYS AS IDENTITY,\n    chain_id   INT NOT NULL,\n    event_type TEXT NOT NULL,\n    data       JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n) PARTITION BY LIST (chain_id);\n\nCREATE TABLE events_ethereum PARTITION OF events\n    FOR VALUES IN (1);\nCREATE TABLE events_polygon PARTITION OF events\n    FOR VALUES IN (137);\nCREATE TABLE events_arbitrum PARTITION OF events\n    FOR VALUES IN (42161);\n```\n\n### 해시 파티셔닝\n\n해시 값으로 균등 분배합니다. 핫스팟 없이 쓰기를 분산시킵니다:\n\n```sql\nCREATE TABLE addresses (\n    address BYTEA NOT NULL,\n    balance NUMERIC NOT NULL,\n    nonce   BIGINT NOT NULL\n) PARTITION BY HASH (address);\n\nCREATE TABLE addresses_p0 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 0);\nCREATE TABLE addresses_p1 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 1);\nCREATE TABLE addresses_p2 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 2);\nCREATE TABLE addresses_p3 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 3);\n```\n\n## 기존 테이블 마이그레이션\n\n3,400만 행을 가진 기존 테이블을 파티션 테이블로 마이그레이션하는 것은 신중하게 수행해야 합니다:\n\n```sql\n-- 1. 새 파티션 테이블 생성\nCREATE TABLE transactions_new (\n    LIKE transactions INCLUDING ALL\n) PARTITION BY RANGE (block_number);\n\n-- 2. 파티션 생성\nCREATE TABLE transactions_new_p1 PARTITION OF transactions_new\n    FOR VALUES FROM (0) TO (19000000);\nCREATE TABLE transactions_new_p2 PARTITION OF transactions_new\n    FOR VALUES FROM (19000000) TO (19500000);\nCREATE TABLE transactions_new_p3 PARTITION OF transactions_new\n    FOR VALUES FROM (19500000) TO (20000000);\n\n-- 3. 배치로 데이터 복사\nINSERT INTO transactions_new\nSELECT * FROM transactions\nWHERE block_number \u003C 19000000;\n\nINSERT INTO transactions_new\nSELECT * FROM transactions\nWHERE block_number >= 19000000 AND block_number \u003C 19500000;\n\n-- 4. 원자적 교체\nBEGIN;\nALTER TABLE transactions RENAME TO transactions_old;\nALTER TABLE transactions_new RENAME TO transactions;\nCOMMIT;\n```\n\n## EXPLAIN ANALYZE로 파티션 프루닝 확인\n\n파티셔닝이 올바르게 작동하는지 EXPLAIN ANALYZE로 확인합니다:\n\n```sql\nEXPLAIN ANALYZE\nSELECT * FROM transactions\nWHERE block_number BETWEEN 19200000 AND 19300000;\n\n-- 출력에서 확인:\n-- Append\n--   -> Seq Scan on transactions_2024_02  (실제 스캔)\n--   Subplans Removed: 19              (프루닝된 파티션 수)\n```\n\n\"Subplans Removed\"가 표시되면 파티션 프루닝이 작동하는 것입니다.\n\n## 자동 파티션 관리\n\n새 파티션을 자동으로 생성하는 cron 작업을 설정합니다:\n\n```sql\nCREATE OR REPLACE FUNCTION create_next_partition()\nRETURNS void AS $$\nDECLARE\n    max_block BIGINT;\n    partition_name TEXT;\n    range_start BIGINT;\n    range_end BIGINT;\nBEGIN\n    SELECT MAX(block_number) INTO max_block FROM transactions;\n    range_start := (max_block \u002F 200000 + 1) * 200000;\n    range_end := range_start + 200000;\n    partition_name := 'transactions_' || range_start::TEXT;\n\n    EXECUTE format(\n        'CREATE TABLE IF NOT EXISTS %I PARTITION OF transactions FOR VALUES FROM (%s) TO (%s)',\n        partition_name, range_start, range_end\n    );\nEND;\n$$ LANGUAGE plpgsql;\n```\n\n## 결론\n\nPostgreSQL 테이블 파티셔닝은 대규모 테이블을 관리 가능한 단위로 분할하여 쿼리 성능, VACUUM 효율성, 인덱스 유지 보수를 크게 개선합니다. 핵심은 올바른 파티션 키 선택입니다: 시계열 데이터에는 범위 파티셔닝, 카테고리별 분리에는 리스트 파티셔닝, 균등 분배에는 해시 파티셔닝을 사용합니다. 파티셔닝은 테이블이 커지기 전에 계획하는 것이 이상적이지만, 이 글에서 보여준 것처럼 기존 테이블을 마이그레이션하는 것도 충분히 가능합니다.","\u003Ch2 id=\"\">파티셔닝이 필요한 시점\u003C\u002Fh2>\n\u003Cp>작게 시작한 \u003Ccode>transactions\u003C\u002Fcode> 테이블이 이제 3,400만 행에 도달했습니다. 50ms 걸리던 쿼리가 이제 5초 걸립니다. VACUUM은 몇 시간 동안 실행되며 다른 테이블의 autovacuum을 차단합니다. 인덱스 재구축은 테이블을 몇 분간 오프라인으로 만듭니다. 데이터베이스가 느린 것이 아닙니다 — 테이블이 단일 힙 파일로 너무 커진 것입니다.\u003C\u002Fp>\n\u003Cp>테이블 파티셔닝은 논리적 테이블을 여러 물리적 테이블(파티션)로 분할합니다. PostgreSQL의 쿼리 플래너가 올바른 파티션으로 쿼리를 자동 라우팅하여 필요한 데이터만 스캔합니다.\u003C\u002Fp>\n\u003Ch3>파티셔닝의 경험 법칙\u003C\u002Fh3>\n\u003Cul>\n\u003Cli>\u003Cstrong>파티셔닝할 때:\u003C\u002Fstrong> 테이블이 1천만 행 초과, 쿼리가 일관되게 테이블의 20% 이상 스캔, VACUUM이 데드 튜플을 따라잡지 못할 때\u003C\u002Fli>\n\u003Cli>\u003Cstrong>파티셔닝하지 말 때:\u003C\u002Fstrong> 테이블이 100만 행 미만(오버헤드가 이익 초과), 쿼리가 항상 인덱스를 타는 경우(파티션 프루닝 불필요), 쓰기 패턴이 랜덤(자연스러운 파티션 키 없음)\u003C\u002Fli>\n\u003C\u002Ful>\n\u003Ch2 id=\"\">파티션 전략\u003C\u002Fh2>\n\u003Cp>PostgreSQL은 세 가지 네이티브 파티셔닝 전략을 지원합니다:\u003C\u002Fp>\n\u003Ch3>범위 파티셔닝\u003C\u002Fh3>\n\u003Cp>값 범위로 분할합니다. 시계열 데이터에 이상적입니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE TABLE transactions (\n    id          BIGINT GENERATED ALWAYS AS IDENTITY,\n    block_number BIGINT NOT NULL,\n    tx_hash     BYTEA NOT NULL,\n    from_addr   BYTEA NOT NULL,\n    to_addr     BYTEA NOT NULL,\n    value       NUMERIC NOT NULL,\n    created_at  TIMESTAMPTZ NOT NULL DEFAULT NOW()\n) PARTITION BY RANGE (block_number);\n\n-- 월별 파티션\nCREATE TABLE transactions_2024_01 PARTITION OF transactions\n    FOR VALUES FROM (18900000) TO (19100000);\nCREATE TABLE transactions_2024_02 PARTITION OF transactions\n    FOR VALUES FROM (19100000) TO (19300000);\nCREATE TABLE transactions_2024_03 PARTITION OF transactions\n    FOR VALUES FROM (19300000) TO (19500000);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>범위 파티셔닝은 블록 번호나 타임스탬프 기반 쿼리에서 파티션 프루닝의 이점을 극대화합니다.\u003C\u002Fp>\n\u003Ch3>리스트 파티셔닝\u003C\u002Fh3>\n\u003Cp>특정 값 목록으로 분할합니다. 카테고리별 분리에 유용합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE TABLE events (\n    id         BIGINT GENERATED ALWAYS AS IDENTITY,\n    chain_id   INT NOT NULL,\n    event_type TEXT NOT NULL,\n    data       JSONB,\n    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n) PARTITION BY LIST (chain_id);\n\nCREATE TABLE events_ethereum PARTITION OF events\n    FOR VALUES IN (1);\nCREATE TABLE events_polygon PARTITION OF events\n    FOR VALUES IN (137);\nCREATE TABLE events_arbitrum PARTITION OF events\n    FOR VALUES IN (42161);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch3>해시 파티셔닝\u003C\u002Fh3>\n\u003Cp>해시 값으로 균등 분배합니다. 핫스팟 없이 쓰기를 분산시킵니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE TABLE addresses (\n    address BYTEA NOT NULL,\n    balance NUMERIC NOT NULL,\n    nonce   BIGINT NOT NULL\n) PARTITION BY HASH (address);\n\nCREATE TABLE addresses_p0 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 0);\nCREATE TABLE addresses_p1 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 1);\nCREATE TABLE addresses_p2 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 2);\nCREATE TABLE addresses_p3 PARTITION OF addresses\n    FOR VALUES WITH (MODULUS 4, REMAINDER 3);\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">기존 테이블 마이그레이션\u003C\u002Fh2>\n\u003Cp>3,400만 행을 가진 기존 테이블을 파티션 테이블로 마이그레이션하는 것은 신중하게 수행해야 합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">-- 1. 새 파티션 테이블 생성\nCREATE TABLE transactions_new (\n    LIKE transactions INCLUDING ALL\n) PARTITION BY RANGE (block_number);\n\n-- 2. 파티션 생성\nCREATE TABLE transactions_new_p1 PARTITION OF transactions_new\n    FOR VALUES FROM (0) TO (19000000);\nCREATE TABLE transactions_new_p2 PARTITION OF transactions_new\n    FOR VALUES FROM (19000000) TO (19500000);\nCREATE TABLE transactions_new_p3 PARTITION OF transactions_new\n    FOR VALUES FROM (19500000) TO (20000000);\n\n-- 3. 배치로 데이터 복사\nINSERT INTO transactions_new\nSELECT * FROM transactions\nWHERE block_number &lt; 19000000;\n\nINSERT INTO transactions_new\nSELECT * FROM transactions\nWHERE block_number &gt;= 19000000 AND block_number &lt; 19500000;\n\n-- 4. 원자적 교체\nBEGIN;\nALTER TABLE transactions RENAME TO transactions_old;\nALTER TABLE transactions_new RENAME TO transactions;\nCOMMIT;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"explain-analyze\">EXPLAIN ANALYZE로 파티션 프루닝 확인\u003C\u002Fh2>\n\u003Cp>파티셔닝이 올바르게 작동하는지 EXPLAIN ANALYZE로 확인합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">EXPLAIN ANALYZE\nSELECT * FROM transactions\nWHERE block_number BETWEEN 19200000 AND 19300000;\n\n-- 출력에서 확인:\n-- Append\n--   -&gt; Seq Scan on transactions_2024_02  (실제 스캔)\n--   Subplans Removed: 19              (프루닝된 파티션 수)\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Cp>“Subplans Removed“가 표시되면 파티션 프루닝이 작동하는 것입니다.\u003C\u002Fp>\n\u003Ch2 id=\"\">자동 파티션 관리\u003C\u002Fh2>\n\u003Cp>새 파티션을 자동으로 생성하는 cron 작업을 설정합니다:\u003C\u002Fp>\n\u003Cpre>\u003Ccode class=\"language-sql\">CREATE OR REPLACE FUNCTION create_next_partition()\nRETURNS void AS $$\nDECLARE\n    max_block BIGINT;\n    partition_name TEXT;\n    range_start BIGINT;\n    range_end BIGINT;\nBEGIN\n    SELECT MAX(block_number) INTO max_block FROM transactions;\n    range_start := (max_block \u002F 200000 + 1) * 200000;\n    range_end := range_start + 200000;\n    partition_name := 'transactions_' || range_start::TEXT;\n\n    EXECUTE format(\n        'CREATE TABLE IF NOT EXISTS %I PARTITION OF transactions FOR VALUES FROM (%s) TO (%s)',\n        partition_name, range_start, range_end\n    );\nEND;\n$$ LANGUAGE plpgsql;\n\u003C\u002Fcode>\u003C\u002Fpre>\n\u003Ch2 id=\"\">결론\u003C\u002Fh2>\n\u003Cp>PostgreSQL 테이블 파티셔닝은 대규모 테이블을 관리 가능한 단위로 분할하여 쿼리 성능, VACUUM 효율성, 인덱스 유지 보수를 크게 개선합니다. 핵심은 올바른 파티션 키 선택입니다: 시계열 데이터에는 범위 파티셔닝, 카테고리별 분리에는 리스트 파티셔닝, 균등 분배에는 해시 파티셔닝을 사용합니다. 파티셔닝은 테이블이 커지기 전에 계획하는 것이 이상적이지만, 이 글에서 보여준 것처럼 기존 테이블을 마이그레이션하는 것도 충분히 가능합니다.\u003C\u002Fp>\n","ko","b0000000-0000-0000-0000-000000000001",true,"2026-03-28T10:44:28.299468Z","PostgreSQL 테이블 파티셔닝 — 테이블이 1천만 행을 넘을 때","범위, 리스트, 해시 전략을 사용한 PostgreSQL 테이블 파티셔닝 실용 가이드. 3,400만 행을 20개 파티션으로 마이그레이션한 실제 예제.","PostgreSQL 테이블 파티셔닝",null,"index, follow",[22,27,31],{"id":23,"name":24,"slug":25,"created_at":26},"c0000000-0000-0000-0000-000000000012","DevOps","devops","2026-03-28T10:44:21.513630Z",{"id":28,"name":29,"slug":30,"created_at":26},"c0000000-0000-0000-0000-000000000022","Performance","performance",{"id":32,"name":33,"slug":34,"created_at":26},"c0000000-0000-0000-0000-000000000005","PostgreSQL","postgresql",[36,43,49],{"id":37,"title":38,"slug":39,"excerpt":40,"locale":12,"category_name":41,"published_at":42},"d0000000-0000-0000-0000-000000000674","2026년, Bali가 동남아시아의 임팩트 테크 허브가 되고 있는 이유","bali-2026-dongnamasia-impaekteu-tekeu-heobeu-iyu","Bali는 동남아시아 스타트업 생태계에서 16위를 차지하고 있습니다. Web3 빌더, AI 지속가능성 스타트업, 에코 여행 테크 기업이 집중되면서, 이 섬은 지역 임팩트 테크의 수도로 자리매김하고 있습니다.","엔지니어링","2026-03-28T10:44:49.294484Z",{"id":44,"title":45,"slug":46,"excerpt":47,"locale":12,"category_name":41,"published_at":48},"d0000000-0000-0000-0000-000000000673","ASEAN 데이터 보호 패치워크: 개발자를 위한 컴플라이언스 체크리스트","asean-deiteo-boho-paechiwokeu-gaebaljaleul-wihan-keompeullaieonseuchekeuriseuteu","7개 ASEAN 국가가 포괄적인 데이터 보호법을 시행하고 있으며, 각각 다른 동의 모델, 현지화 요건, 벌칙 구조를 가지고 있습니다. 다중 국가 애플리케이션을 구축하는 개발자를 위한 실용적인 컴플라이언스 체크리스트입니다.","2026-03-28T10:44:49.286400Z",{"id":50,"title":51,"slug":52,"excerpt":53,"locale":12,"category_name":41,"published_at":54},"d0000000-0000-0000-0000-000000000672","Indonesia 290억 달러 디지털 전환: 소프트웨어 기업을 위한 기회","indonesia-290eok-dallleo-dijiteol-jeonhwan-sopeuteuweo-gieopui-gihoe","Indonesia IT 서비스 시장은 2026년 290.3억 달러에 달할 것으로 예상되며, 이는 2025년 243.7억 달러에서 증가한 수치입니다. 클라우드 인프라, AI, 전자상거래, 데이터센터가 동남아시아에서 가장 빠른 성장을 주도하고 있습니다.","2026-03-28T10:44:49.265609Z",{"id":13,"name":56,"slug":57,"bio":58,"photo_url":19,"linkedin":19,"role":59,"created_at":60,"updated_at":60},"Open Soft Team","open-soft-team","The engineering team at Open Soft, building premium software solutions from Bali, Indonesia.","Engineering Team","2026-03-28T08:31:22.226811Z"]