היום למדתי: פרטישן לפי זמן ומפתח ראשי
לקחתי טבלת פוסטגרס בשביל הסיפור נניח שזו טבלת לקוחות:
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}'))