• בלוג
  • היום למדתי: פרטישן לפי זמן ומפתח ראשי

היום למדתי: פרטישן לפי זמן ומפתח ראשי

30/11/2025

לקחתי טבלת פוסטגרס בשביל הסיפור נניח שזו טבלת לקוחות:

CREATE TABLE customers (
    customer_id     INT PRIMARY KEY,
    full_name       TEXT NOT NULL,
    email_address   TEXT NOT NULL,
    created_at      TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
    updated_at      TIMESTAMP WITH TIME ZONE DEFAULT NOW()
);

ואז רציתי לחלק אותה ל Partitions כדי לשפר ביצועי שליפות (רוב השליפות צריכות למצוא לקוחות חדשים). אז הלכתי ל ChatGPT וביקשתי גרסה מחולקת של הטבלה. זה הקוד שהוא הדפיס:

CREATE TABLE customers (
    customer_id     INT PRIMARY KEY,
    full_name       TEXT NOT NULL,
    email_address   TEXT NOT NULL,
    created_at      TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW(),
    updated_at      TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT NOW()
)
PARTITION BY RANGE (created_at);

הוא כמובן טעה.

פוסטגרס לא מרוצה וטוען שהמפתח הראשי צריך להיות חלק מה Partition. וזה ברור - מפתח ראשי אמור להיות ייחודי על כל השורות, אבל אם השורות מאוחסנות בטבלאות שונות אין לפוסטגרס דרך לוודא ייחודיות או לדעת באיזה Partition מפתח ראשי מסוים מאוחסן. בהנתן מפתח ראשי שלא קשור למפתח החלוקה של הטבלה, פוסטגרס עדיין יצטרך לסרוק את כל ה Partitions כדי למצוא את השורה.

הפתרון של ChatGPT לא מלהיב, הוא מציע להשתמש במפתח ראשי שמורכב משני שדות (ה id וה timestamp). אנחנו לא אוהבים מפתחות ראשיים מורכבים. אופציה שניה לא מלהיבה שהוא מציע היא לוותר לגמרי על המפתח הראשי, וגם זה לא מלהיב.

פתרון שנראה לי יותר טוב בתיאוריה (לא מימשתי עדיין) הוא להשתמש במפתח ראשי מסוג UUID שכולל timestamp למשל UUID7 או ULID. במפתח ראשי כזה אפשר לקחת את החלק של הזמן להיות מפתח החלוקה של הטבלה. כאן יש פוסט ארוך שמתאר את הרעיון:

https://elixirforum.com/t/partitioning-postgres-tables-by-timestamp-based-uuids/60916

בקצרה זה הקוד שלו ליצירת הטבלה:

create table(:payloads, primary_key: false, options: "PARTITION BY RANGE(id)") do
  add(:id, :binary_id, null: false, primary_key: true)

  # ... other fields and references
end

והקוד שיוצר את ה Partitions יהיה משהו כזה:

CREATE TABLE #{table}_p#{start_date.year}#{month}
PARTITION OF #{table} FOR VALUES
FROM (ulid_to_uuid('#{start_ulid}'))
TO (ulid_to_uuid('#{end_ulid}'))