ข้ามไปยังเนื้อหา

Partitioning

table บาง table โตขึ้นไม่มีขีดจำกัด table ของ event, log line, หรือค่าจาก sensor สามารถถึงพันล้าน row ได้ และเมื่อขนาดเท่านั้นทุกอย่างก็ยากขึ้น: index พองตัว, งาน maintenance รันเป็นชั่วโมง, และการลบข้อมูลเก่าก็ lock table ไว้นานแสนนาน Partitioning จัดการเรื่องนี้ด้วยการแบ่ง table เชิงตรรกะขนาดมหึมาหนึ่ง table ออกเป็น table ทางกายภาพที่เล็กกว่าหลาย table โดยที่ query ของคุณยังเห็นเป็น table เดียวเหมือนเดิม

Declarative partitioning ของ PostgreSQL ให้คุณกำหนดกฎการแบ่งเพียงครั้งเดียว คุณเขียนและอ่านจาก table parent ตามปกติ และ server จะ route แต่ละ row ไปยัง partition ที่ถูกต้องให้เองเบื้องหลัง

คุณประกาศ table parent ให้เป็นแบบ partitioned แล้วกำหนด partition key ให้ — column ที่ค่าในนั้นตัดสินว่า row หนึ่งไปอยู่ partition ไหน ตัว parent เองไม่เก็บข้อมูลเลย แต่ทำหน้าที่เป็นชั้น routing อยู่เหนือ child partition

flowchart TD
  Parent[events - logical parent table] --> P1[events_2024_q1]
  Parent --> P2[events_2024_q2]
  Parent --> P3[events_2024_q3]
  Parent --> P4[events_2024_q4]
table เชิงตรรกะหนึ่งที่ถูก route ข้ามหลาย partition

มีกลยุทธ์ partitioning สามแบบ เลือกตามวิธีที่ key แมปไปยัง partition

กลยุทธ์row ถูกกำหนดโดย…เหมาะสำหรับ
RANGEค่าที่ตกอยู่ในช่วงหนึ่ง เช่นช่วงวันที่ข้อมูล time-series ที่ partition ตามเดือน
LISTkey ที่ตรงกับค่าหนึ่งในชุดค่าที่กำหนดตายตัวการแบ่งตาม region, status, หรือ tenant
HASHhash ของ key ที่กระจายอย่างสม่ำเสมอข้าม bucketการกระจายอย่างสม่ำเสมอเมื่อไม่มีช่วงตามธรรมชาติ

กรณีที่พบบ่อยที่สุดคือ range partitioning ตามเวลา คุณประกาศ parent ด้วย PARTITION BY RANGE แล้วสร้าง child partition หนึ่งตัวต่อหนึ่งช่วงเวลา

-- Parent table, partitioned by the created_at timestamp
CREATE TABLE events (
id bigint GENERATED ALWAYS AS IDENTITY,
created_at timestamptz NOT NULL,
kind text NOT NULL,
payload jsonb
) PARTITION BY RANGE (created_at);
-- One partition per quarter; the bounds are inclusive lower, exclusive upper
CREATE TABLE events_2024_q1 PARTITION OF events
FOR VALUES FROM ('2024-01-01') TO ('2024-04-01');
CREATE TABLE events_2024_q2 PARTITION OF events
FOR VALUES FROM ('2024-04-01') TO ('2024-07-01');

การ insert เข้า events จะลง row ใน partition ที่ตรงกันโดยอัตโนมัติ

-- This row routes itself into events_2024_q1
INSERT INTO events (created_at, kind, payload)
VALUES ('2024-02-14', 'signup', '{"plan": "pro"}');

ข้อสังเกตเกี่ยวกับ partition key: ต้องเป็นส่วนหนึ่งของ primary key บน table ที่ partition ไว้ จึงเป็นเหตุผลที่ตัวอย่างใช้ GENERATED ALWAYS AS IDENTITY โดยไม่มี PRIMARY KEY แบบเดี่ยวบน id เพียงตัวเดียว

PARTITION BY LIST กำหนด row ด้วยค่า key ที่ตรงกันพอดี — สะดวกเมื่อมีหมวดหมู่ตามธรรมชาติอยู่

CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY,
region text NOT NULL,
name text NOT NULL
) PARTITION BY LIST (region);
CREATE TABLE customers_emea PARTITION OF customers
FOR VALUES IN ('eu', 'me', 'africa');
CREATE TABLE customers_amer PARTITION OF customers
FOR VALUES IN ('us', 'ca', 'latam');

PARTITION BY HASH กระจาย row อย่างสม่ำเสมอข้าม bucket จำนวนที่กำหนดตายตัวเมื่อไม่มี range หรือ list ใดที่สมเหตุสมผล

CREATE TABLE sessions (
id bigint NOT NULL,
token text NOT NULL
) PARTITION BY HASH (id);
CREATE TABLE sessions_0 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 0);
CREATE TABLE sessions_1 PARTITION OF sessions
FOR VALUES WITH (MODULUS 4, REMAINDER 1);

ผลตอบแทนที่แท้จริงคือ partition pruning: เมื่อ clause WHERE ของ query กรองบน partition key planner จะข้าม partition ทุกตัวที่ไม่อาจมี row ที่ตรงกัน และ scan เฉพาะตัวที่เกี่ยวข้องเท่านั้น

-- The planner reads only events_2024_q1, not the whole table
EXPLAIN
SELECT count(*) FROM events
WHERE created_at >= '2024-02-01' AND created_at < '2024-03-01';

แผนแสดงเพียง partition ของไตรมาสเดียวที่ถูก scan ถ้าคุณกลับไป query โดยไม่กล่าวถึง created_at planner ก็ไม่มีทางจำกัดให้แคบลงและต้อง scan ทุก partition — ซึ่งช้ากว่า table เดียวที่ทำ index ดี Pruning คือจุดประสงค์ทั้งหมด และ pruning ต้องการ partition key ใน clause WHERE

Partitioning ไม่ได้มาฟรี: มี table ให้จัดการมากขึ้น มีชั้น routing ในทุกการเขียน และ query ที่มองข้าม key อาจช้าลงได้ จะคุ้มค่าก็ต่อเมื่อ:

  • table ใหญ่จริง ๆ — คิดถึงหลายร้อยล้าน row ขึ้นไป
  • query ส่วนใหญ่กรองบนมิติเดียวที่คุณ partition ตามได้ ซึ่งโดยปกติคือเวลา
  • คุณทิ้งข้อมูลเก่าเป็นกลุ่มก้อน การ detach หรือ drop ทั้ง partition แทบจะทันที เทียบกับ DELETE ที่ scan และ lock หลายล้าน row

ไม่กี่ล้าน row พร้อม index ที่ดีไม่ต้องการ partitioning การรีบใส่ partitioning เร็วเกินไปคือการซื้อความซับซ้อนมาเปล่า ๆ

  • เลือก partition key ให้ตรงกับ query pattern หลักของคุณ ถ้าคุณ query ตามวันที่ ก็ partition ตามวันที่เพื่อให้ pruning ทำงานได้
  • Pruning เกิดขึ้นเฉพาะเมื่อ partition key ปรากฏใน clause WHERE เท่านั้น query ที่กรองบนสิ่งอื่นจะ scan ทุก partition
  • สร้าง partition ในอนาคตไว้ล่วงหน้า หรือทำให้เป็นอัตโนมัติ การ insert ที่ไม่มี partition ตรงกันจะล้มเหลว เว้นแต่คุณจะมี default partition
  • การทิ้งข้อมูลเก่าด้วยการ detach หรือ drop ทั้ง partition เร็วและเบากว่า DELETE แบบทีละ row อย่างมหาศาล
  • ทำ index ที่ระดับ partition ไม่ใช่แค่ที่ parent ถ้าสร้าง index ไว้บน parent ที่ partition แล้ว PostgreSQL จะสร้างต่อลงไปในทุก partition ให้อัตโนมัติ เป็นวิธีง่าย ๆ ในการรักษาให้สอดคล้องกัน
ตัวเลือกBenefitCost
Partitioning table ขนาดใหญ่maintenance เร็วขึ้น (vacuum/rebuild index ต่อ partition), drop ข้อมูลเก่าเป็นกลุ่มก้อนได้เกือบทันทีquery ต้องออกแบบให้ตรงกับ partition key, มี table ให้จัดการมากขึ้น
ไม่ partition (table เดียว)query และ schema เรียบง่าย ไม่ต้องคิดเรื่อง partition keymaintenance ช้าลงเรื่อย ๆ เมื่อ table โต, การลบข้อมูลเก่าต้อง DELETE ทีละ row
  • Partition table ตั้งแต่ยังเล็ก ทั้งที่ยังไม่ถึงขนาดที่ต้องการ — ซื้อความซับซ้อนมาโดยไม่ได้ประโยชน์ รอจนกว่า table จะใหญ่จริง ๆ (หลายร้อยล้าน row) ก่อนค่อย partition
  • เลือก partition key ที่ query ส่วนใหญ่ไม่ได้กรองด้วย — planner ทำ prune ไม่ได้และต้อง scan ทุก partition อยู่ดี เลือก key ให้ตรงกับ query pattern จริง
  • ลืมสร้าง default partition — การ insert row ที่ไม่ตรงกับ partition ใดเลยจะล้มเหลวทันที ถ้าไม่มี default partition รองรับไว้

💡 ตัวอย่างจากของจริง

Instagram — partition/shard table ขนาดมหึมาอย่าง post หรือ event ตามเวลา หรือ user ID เพื่อให้ drop หรือ archive partition เก่าเป็น operation ระดับ metadata ที่เร็วมาก แทนที่จะต้อง DELETE ทีละ row

Time-series/event logging platform — ระบบเก็บ log หรือ metric จำนวนมากมักใช้ range partitioning ตามวันหรือเดือน เพื่อให้ retention policy ทำได้ด้วยการ drop partition แทนการ delete แบบสแกนทั้ง table

อะไรเป็นตัวตัดสินว่า row หนึ่งถูกเก็บอยู่ใน partition ใด?
partition pruning ให้ planner ข้าม partition ได้เฉพาะเมื่อ...
partitioning คุ้มค่าอย่างชัดเจนที่สุดสำหรับ workload แบบใด?