UUID أم bigint: كيف تختار المفتاح الأساسي لجدولك

ابدأ من bigint. هذا افتراضك الأول في كل جدول جديد. أما UUID فقرار تفرضه حاجة معمارية محدّدة، لا رغبة في شكل الروابط. ما تغيّر خلال 2024-2025 أن UUIDv7 أزال الجزء الأسوأ من كلفة UUID، لكنه لم يُلغِ الفارق كله.
متى تحتاج UUID فعلاً
المعرّف العالمي ضرورة حين ينشأ المفتاح خارج القاعدة أو خارج عقدة واحدة:
- توليد المعرّف في العميل قبل وصول الطلب إلى الخادم، كتطبيق يُنشئ السجل ثم يرفعه.
- دمج قواعد بيانات منفصلة في قاعدة واحدة دون تصادم المفاتيح.
- المزامنة دون اتصال، حيث يكتب الجهاز محلياً ثم يزامن لاحقاً.
- التجزئة على عدة عقد بلا منسّق مركزي للأرقام.
إن كانت حاجتك الوحيدة إخفاء التسلسل في الروابط فهذا ليس سبباً لتغيير المفتاح الأساسي.
لماذا يؤذي UUIDv4 الفهرس
القيمة عشوائية بالكامل. يهبط كل إدخال في موضع عشوائي من شجرة B بدلاً من الطرف الأيمن، فيتكرّر انقسام الصفحات. يضع توثيق MySQL الفارق بالأرقام: الإدخال بترتيب تصاعدي أو تنازلي يملأ الصفحة بنحو 15/16 من سعتها، بينما الإدخال العشوائي يترك امتلاءها بين 1/2 و15/16.
الضرر ليس متساوياً بين المحرّكين. في InnoDB تُخزَّن الصفوف نفسها داخل شجرة المفتاح الأساسي، ويحمل كل سجل في الفهرس الثانوي أعمدة هذا المفتاح، فالكلفة مضاعفة في الحجم والكتابة. في PostgreSQL كل الفهارس ثانوية والصفوف في كومة منفصلة، فينحصر الأذى في فهرس المفتاح وحده. في MySQL يدخل عدد فهارسك الثانوية في فاتورة UUID، لا في PostgreSQL — وهو سبب إضافي لتقليل عددها بدمجها في فهارس مركّبة مرتّبة جيداً، كما يشرح الفهرس المركب وترتيب أعمدته.
في فبراير 2024 نشر جيريمي شنايدر قياساً على نسخة تطويرية من PostgreSQL مرقّعة بدعم UUIDv7، أدخل فيه مليون صف إلى جدول فيه 20 مليون صف تحت حمل متزامن. النتيجة: 375 ثانية مع UUIDv4، مقابل 290 ثانية مع UUIDv7 و290 ثانية مع bigint، وحجم نهائي 2.65 و2.47 و1.97 غيغابايت على الترتيب.
نفس بقاء القيم الجديدة عند الطرف الأيمن من الشجرة هو ما يجعل هذا العمود صالحاً لاحقاً كعمود مؤشر: Cursor Pagination أم Offset يعتمد على قفزة فهرس مباشرة إلى آخر قيمة رآها المستخدم، وهذا لا يعمل بكفاءة إلا إذا بقي الإدخال مرتّباً كما هو الحال هنا مع bigint أو UUIDv7، لا UUIDv4.
ما الذي حلّه UUIDv7 وما بقي
صدر RFC 9562 في مايو 2024 وألغى RFC 4122. تضع النسخة السابعة طابعاً زمنياً بالمللي ثانية في أول 48 بت، فتصير القيم مرتّبة بترتيب البايتات، وتعود الإدخالات إلى الطرف الأيمن من الشجرة.
لكن بقيت ثلاثة أمور. الأول الحجم: 16 بايت مقابل 8، وأثره في InnoDB يمتدّ إلى كل فهرس ثانوي لأنه يحمل المفتاح الأساسي. والثاني تسريب وقت الإنشاء؛ ففي PostgreSQL 18 يُستخرج بأمر واحد عبر uuid_extract_timestamp(). والثالث أن ضمان تصاعد القيم داخل المللي ثانية الواحدة اختياري في المواصفة، وبعض التنفيذات لا يوفّره.
ماذا تكتب في CREATE TABLE
في PostgreSQL 18، الصادر في 25 سبتمبر 2025، صارت uuidv7() مدمجة. أما gen_random_uuid() فتبقى للنسخة الرابعة. قبل الإصدار 18 كان الحصول على v7 يحتاج امتداداً خارجياً مثل pg_uuidv7 أو توليداً في التطبيق.
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
created_at timestamptz NOT NULL DEFAULT now()
);
CREATE TABLE events (
id uuid PRIMARY KEY DEFAULT uuidv7()
);
MySQL لا يوفّر UUIDv7 أصلياً حتى اليوم، ودالة UUID() فيه تُرجع v1 لا v4 بنصّ التوثيق. عملياً: ولّد v7 في التطبيق وخزّنه ثنائياً. والعمود المولَّد id_text يعيد القيمة بصيغتها النصية المألوفة عند القراءة، وهو عمود افتراضي لا يُخزَّن فلا يكلّفك مساحة.
CREATE TABLE uploads (
id BINARY(16) NOT NULL,
id_text CHAR(36) COLLATE ascii_general_ci
GENERATED ALWAYS AS (BIN_TO_UUID(id)) VIRTUAL,
PRIMARY KEY (id)
) ENGINE=InnoDB;
ثلاثة أخطاء تتكرّر
تخزين UUID نصّاً في MySQL. عمود CHAR(36) بترميز utf8mb4 قد يحجز حتى 144 بايت للقيمة الواحدة، مضروبةً في كل فهرس ثانوي يحمل المفتاح. الصواب BINARY(16).
تمرير swap_flag = 1 إلى UUID_TO_BIN() مع UUIDv7. يقصر التوثيق تبديل أجزاء الوقت على v1، ويقول صراحة إنه بلا فائدة لغيره. ومع v7 يزيح التبديل الطرف الأعلى من الطابع الزمني عن موضعه فيفسد الترتيب. مرّر 0.
اختيار int بدل bigint. السقف 2147483647، وقد بقيت Basecamp في وضع القراءة فقط نحو خمس ساعات في نوفمبر 2018 حين بلغ عمود من هذا النوع في جدول الأحداث سقفه.
القرار
| الحالة | الاختيار |
|---|---|
| خادم واحد وقاعدة واحدة | bigint |
| توليد من العميل أو معمارية موزّعة | UUIDv7 لا UUIDv4 |
| إخفاء التسلسل في الروابط فقط | bigint داخلي مع عمود UUIDv7 عام فريد |
النمط الثالث هو الأنفع لمعظم التطبيقات: مفتاح رقمي رخيص للربط بين الجداول، ومعرّف عام فريد للواجهة. والقيد الفريد على العمود العام هو نفسه الأداة التي تعتمدها في التحقق من توقيع الويب هوك ومنع تكرار الأحداث لمنع معالجة الحدث مرتين.
CREATE TABLE customers (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
public_id uuid NOT NULL DEFAULT uuidv7() UNIQUE
);
سيظهر public_id في الروابط، لكن صعوبة تخمينه ليست تحكّماً في الوصول. من يطلب سجلاً ليس له يُمنع بفحص الصلاحية على الخادم، لا بعشوائية المعرّف. ولك في سرقة جلسة المتصفح مثال حيّ: قيمة عشوائية يستحيل تخمينها، ومع ذلك تفتح الحساب كاملاً لمن وضع يده عليها.
واختيار المفتاح قرار واحد من قرارات الجدول؛ القرار المجاور له هو سلوك المعاملات نفسها، وتجده في مستويات العزل في قواعد البيانات: متى ترفعها ومتى تقفل.