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

بقلم فريق تقني · (آخر تحديث: )· برمجة
UUID أم bigint: كيف تختار المفتاح الأساسي لجدولك

ابدأ من bigint. هذا افتراضك الأول في كل جدول جديد، والعدول عنه قرار معماري تفرضه حاجة محددة، لا رغبة في شكل الروابط. ما تغيّر خلال 2024-2025 أن UUIDv7 أزال الجزء الأسوأ من كلفة المعرّفات العالمية: صدر RFC 9562 في مايو 2024 وألغى RFC 4122 القديم، ثم وصلت دالة uuidv7() مدمجة في PostgreSQL 18 الصادر في 25 سبتمبر 2025. لكن الفارق مع bigint لم يُمحَ كله.

متى يفرض UUID نفسه على تصميم قاعدتك

المعرّف العالمي ضرورة حين ينشأ المفتاح خارج القاعدة أو خارج عقدة واحدة:

  • توليد المعرّف في العميل قبل وصول الطلب إلى الخادم.
  • دمج قواعد بيانات منفصلة في قاعدة واحدة دون تصادم المفاتيح.
  • المزامنة دون اتصال، حيث يكتب الجهاز محلياً ثم يزامن لاحقاً.
  • التجزئة على عدة عقد بلا منسّق مركزي يوزّع الأرقام.

خارج هذه الحالات يتفوق الرقم التسلسلي. وإن كانت حاجتك الوحيدة إخفاء التسلسل في الروابط فستجد في آخر المقال نمطاً يحققه دون المساس بالمفتاح. وتنصح PlanetScale بالأعداد الصحيحة مع AUTO_INCREMENT وبعمود منفصل للمعرّف العام، وبنت Shopify حزمة تجعل bigint الافتراضي في مشاريع Rails.

كيف يمزّق المفتاح العشوائي شجرة B-tree

القيمة في UUIDv4 عشوائية بالكامل، فيهبط كل إدخال في موضع عشوائي من شجرة B-tree بدلاً من طرفها الأيمن، ويتكرر انقسام الصفحات. يضع توثيق MySQL الفارق بالأرقام: الإدخال بترتيب تصاعدي أو تنازلي يملأ الصفحة بنحو 15/16 من سعتها، بينما يترك الإدخال العشوائي امتلاءها يتأرجح بين 1/2 و15/16.

والضرر ليس متساوياً بين المحركين. في InnoDB تُخزَّن الصفوف داخل شجرة المفتاح الأساسي نفسها، وينص التوثيق على أن كل سجل في أي فهرس ثانوي يحمل أعمدة المفتاح الأساسي معه، ويصف المفتاح القصير بأنه مكسب صريح. احسبها بنفسك: جدول بمليون صف وثلاثة فهارس ثانوية يخزّن المفتاح 4 مرات لكل صف، أي نحو 64 ميغابايت لبايتات المفتاح وحدها مع UUID مقابل نحو 32 مع bigint، قبل احتساب أثر الامتلاء المنخفض أعلاه. في PostgreSQL كل الفهارس ثانوية والصفوف في كومة منفصلة، فينحصر الأذى في فهرس المفتاح وحده. لذلك يدخل عدد فهارسك الثانوية في فاتورة UUID داخل MySQL تحديداً — سبب إضافي لدمجها في فهارس مركبة مرتبة جيداً كما يشرح الفهرس المركب وترتيب أعمدته.

وثّقت Percona عام 2019 الوجه الثاني للمشكلة: اقتصاد الكاش. مع مفتاح تسلسلي تتركز الكتابة في صفحات ساخنة قليلة تبقى في مجمع الذاكرة المؤقتة؛ أما مع مفتاح عشوائي فكل صفحة ورقية في الشجرة مرشحة لاستقبال الصف التالي، وتنقلب العملية من محدودة بالمعالج إلى محدودة بالقرص فور تجاوز حجم الجدول حجم هذا المجمع. وفي حالة حللوها كان المفتاح مخزناً 6 مرات لكل صف، ونحو 70% من تخزين القاعدة قيم UUID مكررة.

أما الفارق الصافي فقاسه جيريمي شنايدر في فبراير 2024 على نسخة تطويرية من PostgreSQL مرقّعة بدعم UUIDv7: إدخال مليون صف في جدول فيه 20 مليون صف تحت حمل متزامن استغرق 375 ثانية مع UUIDv4 مقابل 290 ثانية لكل من UUIDv7 وbigint، بحجم نهائي 2.65 و2.47 و1.97 غيغابايت على الترتيب.

فاتورة WAL: كيف تصبح 2 غيغابايت 40

في PostgreSQL طبقة كلفة لا تظهر في أي خطة تنفيذ: سجل الكتابة المسبقة WAL. أول تعديل يصيب صفحة 8 كيلوبايت بعد نقطة تحقق يكتب الصفحة كاملة في WAL، لا التغيير وحده. مع bigint تتوالى الإدخالات على الصفحات الأخيرة نفسها فتُدفع كلفة الصورة الكاملة مرة واحدة لكل صفحة؛ مع v4 يلمس كل إدخال صفحة مختلفة تقريباً، فيجر كل صف صورة صفحة كاملة وراءه.

أرقام توماس فوندرا عام 2016: ساعة إدخال بمعدل 5000 صف في الثانية أنتجت نحو 2 غيغابايت من WAL مع bigserial، مقابل أكثر من 40 غيغابايت مع UUID عشوائي. وفي قياسه عام 2018 بلغ معدل إصابة الكاش نحو 99% للمولدات التسلسلية مقابل نحو 85% للعشوائية.

ولا تعوّل على أقراص NVMe لمحو الفارق. قياس مستقل نُشر عام 2025 على PostgreSQL 16 بعشرة ملايين صف وجد إدخالات v7 أسرع بنحو 35% وفهرسها أصغر بنحو 22% من v4، لأن الكلفة الباقية تسكن في إخفاقات الكاش وتضخيم WAL وانخفاض امتلاء الصفحات، لا في زمن القراءة من القرص.

تشريح UUIDv7: زمن في المقدمة وعشوائية في الذيل

يرتّب RFC 9562 بتات النسخة السابعة هكذا: 48 بت طابع يونكس بالمللي ثانية في المقدمة، ثم 4 بت لرقم الإصدار، تليها 12 بت عشوائية باسم rand_a، ثم 2 بت للمتغير، وأخيراً 62 بت عشوائية باسم rand_b. لأن الطابع الزمني يتصدر القيمة، تصير المقارنة بالبايتات مقارنة زمنية، وتعود الإدخالات إلى الطرف الأيمن من الشجرة.

ماذا عن معرّفين وُلدا في المللي ثانية نفسها؟ يعرّف القسم 6.2 من المواصفة 3 طرق اختيارية لضمان التصاعد: عدّاد مخصص، أو زيادة الجزء العشوائي زيادة عشوائية، أو ملء بتات rand_a الاثنتي عشرة بدقة زمنية دون المللي ثانية. تنفيذ PostgreSQL 18 يعتمد الطريقة الثالثة. ولأن هذه الطرق اختيارية، فبعض التنفيذات لا يوفّر أياً منها؛ لا تفترض ترتيباً صارماً داخل المللي ثانية قبل مراجعة مكتبتك.

وبقي بعد الإصلاح أمران. الحجم أولاً: 16 بايت مقابل 8، وأثره في InnoDB يمتد إلى كل فهرس ثانوي. وتسريب وقت الإنشاء ثانياً؛ ففي PostgreSQL 18 يُستخرج بدالة uuid_extract_timestamp()، فلا تستخدم v7 حيث يكون وقت إنشاء السجل نفسه سراً. في المقابل، عودة الإدخال المرتّب تجعل العمود صالحاً كعمود مؤشر للتصفح: Cursor Pagination أم Offset يعتمد على قفزة فهرس مباشرة إلى آخر قيمة رآها المستخدم، وهذا لا يعمل بكفاءة إلا مع bigint أو UUIDv7، لا UUIDv4.

صياغة CREATE TABLE في PostgreSQL 18 وMySQL

في PostgreSQL 18 صارت uuidv7() مدمجة، بينما تبقى gen_random_uuid() للنسخة الرابعة — وهي نفسها لم تُدمج إلا في الإصدار 13 بعد امتدادي uuid-ossp وpgcrypto. وقبل الإصدار 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() فيه تولّد النسخة الأولى من الساعة وعنوان MAC، ويحذر توثيقها من افتراض عدم قابليتها للتخمين ومن استخدامها مع النسخ المبني على الجمل. عملياً: ولّد v7 في التطبيق وخزّنه ثنائياً، وأضف عموداً مولَّداً افتراضياً يعيد الصيغة النصية عند القراءة بلا كلفة تخزين.

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;

ULID وSnowflake أمام UUIDv7

سبق ULID النسخة السابعة إلى الفكرة نفسها: 48 بت زمن و80 بت عشوائية، بترميز Crockford Base32 في 26 محرفاً، وهو متوافق ثنائياً مع نوع uuid — لكنه ليس معيار RFC. وقد وثّقت Shopify عام 2022 انخفاضاً بنسبة 50% في زمن جمل INSERT بعد انتقالها من UUIDv4 إلى ULID لمفاتيح منع تكرار الطلبات.

أما Snowflake فيسلك طريقاً مختلفاً: 64 بت فقط، منها 41 للزمن و10 لمعرّف الآلة بحد أقصى 1024 آلة و12 لعدّاد يعطي 4096 معرّفاً لكل مللي ثانية لكل آلة. يسكن في bigint فيحتفظ بصغر الحجم مع صلاحية التوزيع، لكنه يتطلب تنسيق معرّفات الآلات مركزياً.

يمنحك UUIDv7 فكرة ULID الزمنية نفسها بمعيار RFC ونوع uuid قياسي ودوال مدمجة، فلا مبرر للبدء بغيره في مشروع جديد — إلا إن كانت منظومتك مقيدة بمفاتيح 8 بايت، وعندها Snowflake هو المرشح.

CHAR(36) وswap_flag وسقف int: ثلاث عثرات موثقة

تخزين UUID نصاً في MySQL. عمود CHAR(36) بترميز utf8mb4 قد يحجز حتى 144 بايت للقيمة الواحدة، مضروباً في كل فهرس ثانوي يحمل المفتاح. الصواب BINARY(16).

تمرير swap_flag = 1 إلى UUID_TO_BIN() مع UUIDv7. يقصر التوثيق تبديل أجزاء الوقت على النسخة الأولى، ويقول صراحة إنه بلا فائدة لغيرها. ومع v7 يزيح التبديل الطرف الأعلى من الطابع الزمني عن موضعه فيفسد الترتيب الذي اخترته من أجله. مرّر 0.

اختيار int بدل bigint. السقف 2147483647، وقد بقيت Basecamp في وضع القراءة فقط نحو 5 ساعات في نوفمبر 2018 حين بلغ عمود من هذا النوع سقفه في جدول الأحداث.

نمط المفتاحين ودرس الدبابات الألمانية

لماذا يستحق إخفاء التسلسل اهتماماً؟ لأن الأرقام المتسلسلة تتكلم. من عينة صغيرة منها يُقدَّر الحد الأقصى بصيغة بسيطة: إذا كان m أكبر رقم مرصود وk عدد العينات، فالتقدير N ≈ m + m/k − 1. في يونيو 1940 طبّق الإحصائيون هذه الفكرة على الأرقام التسلسلية للدبابات الألمانية المغتنمة فقدّروا الإنتاج بـ169 دبابة شهرياً، بينما قالت تقارير الاستخبارات 1000؛ وكان الرقم الفعلي 122. متجر إلكتروني يكشف أرقام طلبات متسلسلة يقع في الفخ نفسه: من يطلب مرتين بفاصل شهر ويطرح الرقمين يعرف حجم مبيعاتك الشهري.

هذا يبرر عموداً عاماً غير متسلسل، لا استبدال المفتاح الداخلي. النمط الأنفع لمعظم التطبيقات مفتاحان: bigint رخيص للربط بين الجداول، وpublic_id فريد يظهر للواجهة والروابط. والقيد الفريد على العمود العام هو الأداة نفسها التي تعتمدها في التحقق من توقيع الويب هوك ومنع تكرار الأحداث لمنع معالجة الحدث مرتين.

CREATE TABLE customers (
    id        bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    public_id uuid NOT NULL DEFAULT uuidv7() UNIQUE
);

سيظهر public_id في الروابط، لكن صعوبة تخمينه ليست تحكّماً في الوصول. من يطلب سجلاً ليس له يُمنع بفحص الصلاحية على الخادم، لا بعشوائية المعرّف. ولك في سرقة جلسة المتصفح مثال حيّ: قيمة عشوائية يستحيل تخمينها، ومع ذلك تفتح الحساب كاملاً لمن وضع يده عليها.

الحسم: bigint أم UUIDv7 أم كلاهما

بنية نظامكمفتاحك
خادم واحد وقاعدة واحدةbigint
توليد من العميل أو دمج قواعد أو مزامنة دون اتصالUUIDv7 لا UUIDv4
معمارية موزعة مقيدة بمفاتيح 8 بايتSnowflake مع تنسيق معرّفات الآلات
إخفاء التسلسل في الروابط فقطbigint داخلي مع public_id بنوع UUIDv7

واختيار المفتاح واحد من قرارات الجدول؛ يجاوره سلوك المعاملات نفسها، وتجده في مستويات العزل في قواعد البيانات: متى ترفعها ومتى تقفل.