Postgres 18 UUIDv7 Primary Keys: دليل عملي (2026)
١٧ يونيو ٢٠٢٦
يضيف PostgreSQL 18 وظيفة uuidv7() مدمجة، لذا يمكنك استخدام uuid PRIMARY KEY DEFAULT uuidv7() بدون أي إضافات (extensions). يدمج UUIDv7 طابعًا زمنيًا بالملي ثانية في بتاته الأولى، لذا يتم فرز القيم حسب وقت الإنشاء وتتصرف بشكل أقرب بكثير إلى المفتاح التسلسلي مقارنة بـ UUIDv4 العشوائي.12
ملخص
يأتي PostgreSQL 18 (الإصدار المستقر الحالي 18.4)3 مزودًا بدعم أصلي لـ uuidv7()، و uuidv4() (وهو اسم مستعار لـ gen_random_uuid())، بالإضافة إلى أدوات مساعدة محدثة مثل uuid_extract_timestamp() و uuid_extract_version().1 لإنشاء جدول جديد، ما عليك سوى كتابة id uuid PRIMARY KEY DEFAULT uuidv7(). تمنح البادئة المرتبة زمنيًا في UUIDv7 فهارس B-tree أصغر وأقل تشتتًا من UUIDv4 — في أحد الاختبارات المرجعية المنشورة، كان فهرس المفتاح الأساسي أصغر بنسبة 26-27% تقريبًا، وكانت عمليات المسح المرتبة أسرع بثلاث مرات تقريبًا.2 المقايضة الحقيقية الوحيدة هي أن UUIDv7 يكشف عن وقت الإنشاء التقريبي للصف. تم تشغيل كل كود SQL أدناه على PostgreSQL 18.3 قبل النشر.
ما ستتعلمه
- هل يحتوي PostgreSQL 18 على وظيفة UUIDv7 أصلية وما الذي تم إصداره معها
- كيفية تعيين
uuidv7()كمفتاح أساسي افتراضي باستخدام DDL جاهز للنسخ واللصق - كيف يتقارن UUIDv7 مع UUIDv4 من حيث حجم الفهرس، التشتت، والسرعة
- متى تختار UUIDv7 بدلاً من عمود هوية
bigintعادي - كيفية ترحيل جدول UUIDv4 موجود إلى UUIDv7 دون فقدان الترتيب
- كيفية استخراج وقت الإنشاء من UUIDv7
- لماذا يكشف UUIDv7 عن وقت الإنشاء وكيفية التخفيف من ذلك
هل يحتوي PostgreSQL 18 على وظيفة UUIDv7 مدمجة؟
نعم. أضاف PostgreSQL 18 وظيفة uuidv7() كوظيفة أساسية — لا يلزم وجود إضافة uuid-ossp أو pgcrypto.1 وهي تولد UUID من الإصدار 7 (مرتب زمنيًا) وفقًا لـ RFC 9562، مع طابع زمني مبني من قيمة UNIX بالملي ثانية بالإضافة إلى جزء من الملي ثانية وبتات عشوائية.1
أكمل هذا الإصدار مجموعة أدوات UUID. لا تزال وظيفة gen_random_uuid() (الإصدار 4، عشوائي تمامًا) موجودة، وتمت إضافة uuidv4() كاسم مستعار مدمج لها ليتطابق التسمية مع uuidv7().1 كما تم تحديث أداتين للاستخراج لفهم الإصدار 7: uuid_extract_version(uuid) التي تعيد رقم الإصدار، و uuid_extract_timestamp(uuid) التي تعيد timestamp with time zone لـ UUID من الإصدار 1 أو 7 (وتعيد null للإصدارات الأخرى).1
SELECT
uuid_extract_version(gen_random_uuid()) AS v4, -- 4
uuid_extract_version(uuidv7()) AS v7; -- 7
تمت إضافة UUIDv7 بشكل أصلي في الإصدار 18؛ أما في PostgreSQL 17 والإصدارات الأقدم، فلا تزال بحاجة إلى إضافة أو مكتبة من جانب التطبيق لتوليده.1
كيف أستخدم uuidv7() كمفتاح أساسي افتراضي؟
قم بتعيين القيمة الافتراضية للعمود إلى uuidv7(). نظرًا لأن الوظيفة تعمل على جانب الخادم مع كل عملية إدخال، فلن تضطر أبدًا للمس الـ id في كود التطبيق:
CREATE TABLE invoices (
id uuid PRIMARY KEY DEFAULT uuidv7(),
amount numeric,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO invoices (amount)
SELECT g FROM generate_series(1, 5) AS g;
SELECT id, amount FROM invoices ORDER BY id;
عند التشغيل على PostgreSQL 18.3، تعود الصفوف بترتيب الإدخال لأن المعرفات متسلسلة (monotonic):
id | amount
--------------------------------------+--------
019ed46f-1d7f-7000-9fd1-6978e2d1412f | 1
019ed46f-1d7f-7001-924f-a07704cd74c2 | 2
019ed46f-1d7f-7002-817a-caaa62db219e | 3
019ed46f-1d7f-7003-b9c9-fc99d1d64969 | 4
019ed46f-1d7f-7004-abf1-966ff34e4974 | 5
هذا الترتيب ليس وليد الصدفة. يخزن PostgreSQL جزءًا من الملي ثانية بطول 12 بت مباشرة بعد الطابع الزمني المكون من 48 بت، ويستخدم طريقة RFC 9562 "استبدال البتات العشوائية في أقصى اليسار بدقة ساعة متزايدة"، مما يضمن قيمًا متزايدة باستمرار لكل UUIDv7 يتم إنشاؤه داخل نفس الجلسة (عملية الخلفية). الترتيب غير مضمون عبر العمليات الخلفية المختلفة.4
هل UUIDv7 أفضل من UUIDv4 للمفاتيح الأساسية؟
بالنسبة لصحة الفهرس، نعم — وهذا هو الهدف الأساسي. UUIDv4 عشوائي، لذا فإن كل عملية إدخال تهبط في مكان عشوائي في فهرس B-tree للمفتاح الأساسي، مما يتسبب في انقسام الصفحات (page splits)، وانخفاض كثافة الصفحات، والتشتت. أما إدخالات UUIDv7 فتهبط عند الحافة اليمنى للفهرس، تمامًا مثل المفتاح التسلسلي.
قام اختبار مرجعي مفصل لـ PostgreSQL 18 بواسطة credativ (على مليون و 50 مليون صف) بقياس الفرق على هياكل التخزين الفعلية:2
| المقياس (مليون صف) | UUIDv4 | UUIDv7 |
|---|---|---|
| حجم فهرس المفتاح الأساسي | ~40 ميجابايت | ~31.6 ميجابايت (~26–27% أصغر) |
| صفحات الأوراق (Leaf pages) | 4861 | 3832 |
| متوسط كثافة الأوراق | 71% | ~90% |
| تشتت الأوراق | ~50% | 0% |
ارتباط id بترتيب الكومة (heap-order) | -0.0025 | 1.0 |
ORDER BY id عبر مليون صف | ~318 مللي ثانية | ~113 مللي ثانية |
في نفس الاختبار، استغرق إدخال 50 مليون صف في جدول فارغ حوالي 20 دقيقة لـ UUIDv4 مقابل أقل من دقيقتين لـ UUIDv7، واتسعت الفجوة في الدفعة الثانية.2 تعتمد الأرقام الدقيقة على الأجهزة، والتخزين المؤقت، وحجم العمل، لكن الاتجاه ثابت: البادئة المرتبة زمنيًا تحافظ على ضغط الفهرس وتجعل عمليات الإدخال غير مكلفة. لاحظ أن حجم جدول الكومة (heap table) متطابق — فـ UUID يبلغ 16 بايت في كلتا الحالتين؛ فقط تخطيط الفهرس هو ما يتغير.2
UUIDv7 مقابل bigint: أي مفتاح أساسي يجب استخدامه؟
استخدم عمود هوية bigint عندما تكون على عقدة واحدة وتريد أصغر وأبسط مفتاح؛ واستخدم UUIDv7 عندما تحتاج إلى معرفات فريدة عالميًا، يمكن توليدها من جانب العميل أو في نظام موزع دون التخلي عن محلية الفهرس (index locality).5
يبلغ حجم bigint 8 بايت مقابل 16 بايت لـ UUID، لذا فإن إدخال فهرس bigint يكون نصف الحجم — ولأن هذا المفتاح يتم نسخه في كل فهرس ثانوي ومفتاح خارجي، فإن التوفير يتضاعف عبر المخطط (schema) — بينما لا يزال bigint الموقع يصل إلى حوالي 9.2 × 10¹⁸ قبل أن يتجاوز السعة.2 ما لا تستطيع تسلسلات bigint فعله هو السماح للعديد من الكتاب (الخدمات المصغرة، عملاء الهاتف المحمول، العقد المجزأة) بصك مفاتيح غير متصادمة بشكل مستقل، أو إخفاء عدد صفوف الجدول — فقيمة التسلسل قابلة للتخمين وتكشف عن حجم البيانات.5
UUIDv7 هو الحل الوسط: فهو يحافظ على مساحة المفاتيح الضخمة المكونة من 128 بت وخاصية التوليد الموزع لـ UUIDs، بينما يتصرف الفهرس الخاص به بشكل قريب من bigint المتسلسل بدلاً من الفوضى العشوائية التي ينتجها UUIDv4.2 بالنسبة لتطبيق CRUD جديد يعمل على خادم واحد، لا تزال هوية bigint خياراً افتراضياً ممتازاً. في اللحظة التي تتوقع فيها عمليات كتابة موزعة، أو دمج بيانات، أو كشف المعرفات عبر الخدمات، فإن UUIDv7 هو المفتاح الأساسي الأقوى.
كيف يمكنني نقل جدول UUIDv4 الحالي إلى UUIDv7؟
تغيير مفتاح أساسي مباشر هو عملية ثقيلة — فأنت تضيف عموداً جديداً، وتملأه بالبيانات القديمة، ثم تنقل القيود والمفاتيح الخارجية — لذا خطط لها كأي عملية ترحيل للمخطط (schema migration). الجزء الميكانيكي هو إضافة عمود v7 يكون افتراضياً uuidv7() للصفوف الجديدة وملء الصفوف القديمة بطريقة تحافظ على ترتيبها الأصلي:
-- New rows get a real UUIDv7; existing rows are filled next.
ALTER TABLE legacy
ADD COLUMN id_v7 uuid NOT NULL DEFAULT uuidv7();
-- Backfill old rows so the embedded timestamp matches each row's created_at.
UPDATE legacy
SET id_v7 = uuidv7(created_at - now());
التفصيل الرئيسي هو وسيط uuidv7(shift interval). دالة uuidv7() بدون وسيط تدمج الوقت الحالي؛ أما وسيط shift الاختياري فيقوم بإزاحة ذلك الوقت بفترة زمنية معينة.1 لجعل UUID المملوء قديماً يحمل قيمة created_at الأصلية للصف، يجب أن تكون الإزاحة هي created_at - now(). بالتحقق من ذلك في PostgreSQL 18.3، كانت النتيجة دقيقة تماماً حتى الملي ثانية:
SELECT created_at,
uuid_extract_timestamp(uuidv7(created_at - now())) AS embedded
FROM legacy ORDER BY created_at;
created_at | embedded
------------------------+------------------------
2024-03-01 12:00:00+00 | 2024-03-01 12:00:00+00
2024-06-15 08:30:00+00 | 2024-06-15 08:30:00+00
2025-01-20 23:15:00+00 | 2025-01-20 23:15:00+00
احذر من صيغة منتشرة على الويب: uuidv7((created_at - '2025-01-01')::interval). تبدو منطقية ولكنها لا تدمج created_at — بل تزيح الوقت الحالي بالمسافة عن تاريخ مرجعي ثابت، مما يجعل الطابع الزمني بعيداً بسنوات عن القيمة الحقيقية. نفس الاختبار الموثق يظهر صفاً بتاريخ 2024-03-01 يعود كـ 2025-08-15. قم دائماً بالإزاحة بالنسبة لـ now(). تذكر أيضاً أن أي معرف تم ملؤه قديماً هو معرف اصطناعي: فهو يعيد إنتاج الترتيب، وليس لحظة التوليد الحقيقية، التي لم تكن موجودة أصلاً لتلك الصفوف القديمة.
هل يمكنني استخراج وقت الإنشاء من UUIDv7؟
نعم — تقرأ دالة uuid_extract_timestamp() الطابع الزمني المدمج وتعيده كـ timestamp with time zone:
SELECT id, uuid_extract_timestamp(id) AS created_at_from_uuid
FROM invoices ORDER BY id LIMIT 1;
بالنسبة لـ UUID من الإصدار 4، تعيد نفس الدالة null، لأنه لا يوجد طابع زمني لاستعادته.1 تشير الوثائق إلى أن القيمة المستخرجة "ليست بالضرورة مساوية تماماً للوقت الذي تم فيه إنشاء UUID" — فهي تعكس ما كتبه المولد — ولكن بالنسبة لدالة uuidv7() الخاصة بـ Postgres، فهي قراءة الساعة وقت الإدراج.1 وهذا يجعل UUIDv7 مفيداً لتقسيم الصفحات بناءً على المفاتيح (keyset pagination)، وربط السجلات (log correlation)، وعمليات المسح بنمط السلاسل الزمنية حيث يخبرك المفتاح نفسه تقريباً متى تم إنشاء الصف.2
هل يسرب UUIDv7 وقت إنشاء الصف؟
نعم، وهذا هو التحذير الوحيد الذي يستحق التصميم حوله. لأن أول 48 بت هي طابع زمني بالملي ثانية قابل للقراءة، يمكن لأي شخص يمتلك UUIDv7 استدعاء uuid_extract_timestamp() (أو فك تشفيره من جهة العميل) لمعرفة وقت إنشاء الصف تقريباً — ومن خلال عدة معرفات، يمكنه استنتاج معدل الإنشاء لديك.2 بالنسبة للعديد من الجداول الداخلية، هذا الأمر غير ضار أو حتى مفيد؛ أما بالنسبة للمعرفات الموجهة للجمهور، فقد يكشف عن إشارات تجارية مثل توقيت التسجيل أو حجم الطلبات.
الحل الشائع هو الاحتفاظ بـ UUIDv7 كمفتاح أساسي داخلي وكشف UUIDv4 عشوائي تماماً منفصل (gen_random_uuid()) كمعرف موجه للجمهور، بحيث لا ترى الأطراف الخارجية القيمة المرتبة زمنياً.5 إذا كان حتى الطابع الزمني الداخلي حساساً، فإن وسيط shift يسمح لك بإزاحة الوقت المدمج بقيمة ثابتة لإخفاء تاريخ الإنشاء الحقيقي — على حساب أن الطابع الزمني لن يعود ذا معنى.1
الخلاصة والخطوات التالية
أخيراً، تجعل دالة uuidv7() الأصلية في PostgreSQL 18 المفاتيح الأساسية UUID المرتبة زمنياً خياراً افتراضياً بسطر واحد دون الحاجة لإضافات، مما يمنحك مساحة مفاتيح UUID الصديقة للتوزيع مع سلوك فهرسة قريب من bigint.12 الجأ إليها عندما تحتاج إلى معرفات فريدة عالمياً أو معرفات يتم إنشاؤها من جهة العميل؛ والتزم بهوية bigint عندما يحتاج تطبيق العقدة الواحدة فقط إلى أصغر مفتاح؛ وقم بعزل تسريب الطابع الزمني خلف معرف عام منفصل عندما يكون وقت الإنشاء حساساً.
بما أن مفاتيح UUIDv7 مرتبة بشكل طبيعي، فهي تتوافق تماماً مع تقسيم الصفحات باستخدام المؤشر (keyset pagination) في Postgres، والذي يعتمد على مفتاح قابل للفرز. إذا كنت تنتقل إلى PostgreSQL 18 للحصول على uuidv7()، فراجع دليل ترقية Postgres 18 بدون وقت توقف، وبمجرد وصولك إلى هناك، فإن أتمتة التقسيم باستخدام pg_partman + pg_cron على Postgres 18 تعمل جنباً إلى جنب مع المفاتيح المرتبة زمنياً للجداول الكبيرة.
Footnotes
-
PostgreSQL 18 Documentation — 9.14. UUID Functions. https://www.postgresql.org/docs/18/functions-uuid.html ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12 ↩13 ↩14 ↩15 ↩16
-
Josef Machytka, credativ — "A deeper look at old UUIDv4 vs new UUIDv7 in PostgreSQL 18" (2025-12-05). https://www.credativ.de/en/blog/postgresql-en/a-deeper-look-at-old-uuidv4-vs-new-uuidv7-in-postgresql-18/ ↩ ↩2 ↩3 ↩4 ↩5 ↩6 ↩7 ↩8 ↩9 ↩10 ↩11 ↩12
-
PostgreSQL Global Development Group — "PostgreSQL 18.4, 17.10, 16.14, 15.18, and 14.23 Released!" (2026-05-14). https://www.postgresql.org/about/news/postgresql-184-1710-1614-1518-and-1423-released-3297/ ↩
-
Masahiko Sawada (PostgreSQL committer) — "PostgreSQL 18 supports UUIDv7" (2025-09-04). https://masahikosawada.GitHub.io/en/2025/09/04/UUIDv7-in-PostgreSQL/ ↩
-
CYBERTEC — "UUID, serial or identity columns for PostgreSQL auto-generated primary keys?" https://www.cybertec-postgresql.com/en/uuid-serial-or-identity-columns-for-postgresql-auto-generated-primary-keys/ ↩ ↩2 ↩3 ↩4 ↩5