> ## Content Index
> Fetch the complete content index at: https://arabroot.io/llms.txt
> Use this file to discover other available public pages before exploring further.

# الرقم المتسلسل أم UUID؟ كيف تختار المعرف الفريد
- URL: https://arabroot.io/articles/المعرفات-الفريدة-uuid-و-ulid-و-snowflake/
- Published: 2026-10-05T12:00:00.000Z
- Updated: 2026-10-09T09:42:12.000Z
- Description: لماذا يفشل الرقم المتسلسل في الروابط وعند تعدد السيرفرات، وكيف يؤثر نوع المعرف الذي تختاره على فهرس قاعدة البيانات.
- Author: فريق عرب رووت
- Tags: الأدلة التقنية, أساسيات الحاسب والبرمجة, قواعد البيانات, DevOps وCI/CD, الاستضافة الذاتية

كل سجل تحفظه في قاعدة البيانات يحتاج إلى رقم يميزه عن غيره، فالفاتورة والمستخدم والطلب والرسالة كلها تحتاج إلى معرف فريد Unique Identifier تشير إليه الجداول الأخرى ويظهر في الروابط وفي ال API وفي السجلات Logs، والحل الأول الذي يأتي مع كل قاعدة بيانات هو الرقم المتسلسل 1 ثم 2 ثم 3، وهو حل ممتاز ما دامت قاعدة البيانات واحدة والرقم لا يخرج منها، ولكن ما إن يظهر الرقم في رابط يراه المستخدم، أو يصبح عندك أكثر من سيرفر يكتب البيانات، أو تطبيق جوال يعمل دون اتصال ثم يرسل ما حفظه لاحقاً، حتى تبدأ المشاكل التي سوف نراها في هذا المقال.

وقد شرحنا تصميم الجداول والعلاقات بينها في دليل [تصميم قواعد البيانات: التطبيع والتوسع](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/)، أما هذا المقال فيتعمق في سؤال واحد من أسئلة التصميم وهو: ما نوع المفتاح الأساسي Primary Key الذي تختاره، ولماذا، وسوف نناقش في هذا المقال ما يلي:

- المشكلة التي يحلها المعرف الفريد، ولماذا يكفي الرقم المتسلسل في حالات ويفشل في أخرى.
- ال UUID من الداخل: البتات 128 ورقم الإصدار Version والنوع Variant، والفرق بين الإصدارات 1 و3 و4 و5 و6 و7 و8 بحسب المواصفة الجديدة RFC 9562.
- احتمال التكرار Collision في UUIDv4 بالأرقام، ولماذا لا يقلق منه أحد.
- لماذا يتعب UUIDv4 فهرس قاعدة البيانات، مع تجربة حقيقية على PostgreSQL 18 الذي أضاف الدالة `uuidv7()`.
- البدائل: ULID وKSUID وNanoID وSnowflake وطرق تويتر وDiscord وInstagram، ثم Sqids لإخفاء الأرقام.
- جدول يساعدك على الاختيار حسب حالتك، وملاحظات أمنية لا يجب أن تنساها.

## ما المشكلة التي يحلها المعرف الفريد؟

المعرف الفريد هو قيمة تشير إلى سجل واحد فقط، ولا تتكرر لسجل آخر داخل النطاق الذي تهتم به، والكلمة المهمة هنا هي «النطاق»، لأن الرقم 501 فريد داخل جدول الطلبات في قاعدة بيانات واحدة، ولكنه ليس فريداً إذا جمعت طلبات فرعين في قاعدة بيانات واحدة، وليس فريداً بين جدول الطلبات وجدول الفواتير. والحالات التي تحتاج فيها إلى معرف فريد خارج حدود الجدول الواحد كثيرة ويعرفها كل من عمل على نظام حقيقي:

- **بين الجداول:** عندما يكون عندك جدول أحداث Events أو سجل تدقيق Audit Log يشير إلى سجلات من جداول مختلفة، فالرقم 7 وحده لا يكفي لأنه قد يكون مستخدماً أو طلباً أو فاتورة.
- **بين السيرفرات:** عندما يكتب أكثر من سيرفر في الوقت نفسه، كقاعدتين في مدينتين أو خدمات مصغرة Microservices لكل منها قاعدة بياناتها، فكل واحدة تعد من 1 بشكل مستقل.
- **الكلاينت الذي يعمل دون اتصال Offline:** تطبيق الجوال الذي ينشئ طلباً وهو في الطائرة يحتاج أن يعطيه معرفاً الآن، وليس بعد أن يصل إلى السيرفر ويسأله عن الرقم التالي.
- **دمج قواعد البيانات Merge:** عند استحواذ شركة على أخرى، أو نقل بيانات من نظام قديم، أو دمج نسخة اختبار مع نسخة الإنتاج Production، تتصادم الأرقام المتسلسلة مباشرة.
- **الروابط العامة Public URLs:** الرقم الذي يظهر في الرابط يقرؤه كل مستخدم، وسوف نرى أنه يكشف أكثر مما تتوقع.

والصورة التالية تبين أشهر مشكلتين للرقم المتسلسل: مستخدم يغير رقماً واحداً في الرابط فيصل إلى فواتير غيره، وسيرفران يعطي كل منهما الرقم 501 لطلب مختلف ثم يتصادمان عند الدمج:

![مخطط من جزأين: في الأول عميل يفتح الرابط shop.example/invoices/1041 فيرى فاتورته، ثم شخص آخر يغير الرقم إلى 1042 فيرى فواتير عملاء آخرين 1042 و1043 و1044، مع ملاحظة أن الرقم كان القفل الوحيد وأنه يكشف عدد الفواتير. وفي الثاني سيرفر في الرياض وسيرفر في جدة لكل منهما عداد مستقل فيعطي كل منهما الطلب رقم 501، ثم يتصادمان عند الدمج في قاعدة بيانات مركزية بخطأ مفتاح مكرر](https://arabroot.io/content/images/2026/10/unique-ids-01-problem.webp)

الرقم المتسلسل سهل التخمين في الرابط، ويتكرر عندما يكتب أكثر من طرف

## الرقم المتسلسل: البداية الطبيعية ونقاط ضعفه

الرقم المتسلسل Auto-increment هو أبسط معرف على الإطلاق، فقاعدة البيانات تحتفظ بعداد تسميه PostgreSQL التسلسل Sequence، وتأخذ منه الرقم التالي عند كل إدراج Insert، والطريقة الحديثة في PostgreSQL هي عمود الهوية Identity Column المعرف في [معيار SQL](https://www.postgresql.org/docs/18/sql-createtable.html?ref=arabroot.io) بدلاً من النوع القديم `serial`، والمثال التالي يبين ذلك:

```sql
CREATE TABLE invoices (
    id    bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    total numeric(12,2) NOT NULL
);
```

ونقاط القوة في هذا الحل حقيقية ولا يجب أن تتجاهلها، فالرقم صغير يأخذ 8 بايتات فقط في النوع `bigint`، ومرتب لأن كل رقم جديد أكبر من الذي قبله، وبالتالي يضاف دائماً في آخر الفهرس كما سوف نرى، وهو سهل القراءة في الدعم الفني «الفاتورة 1041»، ولا يحتاج إلى أي مكتبة. لذلك فهو الخيار الصحيح لكثير من الجداول الداخلية التي لا يراها أحد ولا يكتب فيها إلا قاعدة بيانات واحدة. أما نقاط الضعف فتظهر في الحالات التالية:

- **سهل التخمين Guessable:** إذا كان رابط فاتورتك ينتهي بالرقم 1041 فأنت تعرف أن الفاتورة 1042 موجودة، وإذا نسي المطور أن يتحقق في الكود من أن الفاتورة تخص المستخدم الذي طلبها، فكل ما يحتاجه المخترق هو حلقة تكرار Loop تزيد الرقم. وهذا النوع من الثغرات له اسم معروف هو الإشارة المباشرة غير الآمنة إلى الكائن Insecure Direct Object Reference واختصاراً IDOR، وقد خصصت له OWASP [صفحة كاملة](https://cheatsheetseries.owasp.org/cheatsheets/Insecure%5FDirect%5FObject%5FReference%5FPrevention%5FCheat%5FSheet.html?ref=arabroot.io).
- **يكشف حجم عملك:** عميل يشتري منك اليوم فيحصل على الطلب 1041، ثم يشتري بعد شهر فيحصل على الطلب 1388، فهو يعرف الآن أنك تبيع قرابة 350 طلباً في الشهر، والمنافس يستطيع أن يفعل الشيء نفسه بحسابين وهميين. وهذه الفكرة قديمة ولها اسم في الإحصاء هو [مسألة الدبابات الألمانية German Tank Problem](https://en.wikipedia.org/wiki/German%5Ftank%5Fproblem?ref=arabroot.io)، حيث قدر الحلفاء في الحرب العالمية الثانية إنتاج المصانع الألمانية من الأرقام التسلسلية على الدبابات التي غنموها.
- **يتكرر عند أكثر من كاتب:** كل قاعدة بيانات تعد من 1، فإذا كان عندك سيرفران يكتبان أو دمجت قاعدتين فسوف تحصل على خطأ المفتاح المكرر Duplicate Key، أو ما هو أسوأ إذا لم يكن هناك قيد Constraint يكشفه.
- **مصدر واحد للحقيقة Single Source of Truth:** لا تعرف رقم السجل إلا بعد أن تكتبه في قاعدة البيانات، فالتطبيق الذي يعمل دون اتصال لا يستطيع أن ينشئ معرفاً، والخدمة التي تريد أن ترسل حدثاً قبل الكتابة يجب أن تنتظر.

📌

في مايو 2019 نشر الصحفي Brian Krebs أن موقع شركة التأمين العقاري الأمريكية First American Financial كان يعرض قرابة 885 مليون مستند لعملائها تعود إلى سنة 2003، فيها أرقام حسابات بنكية وأرقام ضمان اجتماعي وصور رخص قيادة، والسبب أن روابط المستندات كانت تحمل رقماً متسلسلاً ولا يوجد أي تحقق من هوية من يطلبها، فكان يكفي أن [تغير رقماً واحداً في الرابط](https://krebsonsecurity.com/2019/05/first-american-financial-corp-leaked-hundreds-of-millions-of-title-insurance-records/?ref=arabroot.io) لترى مستند شخص آخر. والدرس أن المشكلة الأساسية هنا هي غياب التحقق من الصلاحية، وأن الرقم المتسلسل حول هذا الخطأ من ثغرة في مستند واحد إلى تسريب لكل المستندات.

وقد يتساءل البعض: لماذا لا نحل مشكلة الكتاب المتعددين بعدادات منفصلة، فيعد السيرفر الأول بالأرقام الفردية والثاني بالأرقام الزوجية؟ وهذا ممكن فعلاً في PostgreSQL بالخيارين `START WITH` و`INCREMENT BY` عند إنشاء ال Sequence، ولكنه حل هش لأن عدد السيرفرات يجب أن تعرفه من البداية، فإذا أضفت سيرفراً ثالثاً بعد سنة فعليك أن تعيد تقسيم الأرقام، ثم إنه لا يحل مشكلة التخمين ولا مشكلة الكلاينت الذي يعمل دون اتصال. والحل الآخر الذي يخطر على البال هو رقم عشوائي كبير، وهذه بالضبط فكرة ال UUID.

## ال UUID: 128 بت لا تحتاج أن تسأل أحداً

المعرف الفريد العالمي Universally Unique Identifier واختصاراً UUID هو رقم طوله 128 بت يكتب عادة بالنظام الست عشري Hex في 32 خانة مقسمة بأربع شرطات بالشكل `8-4-4-4-12`، والفكرة الأساسية فيه أن أي جهاز يستطيع أن ينشئه وحده دون أن يسأل قاعدة البيانات أو أي سيرفر آخر، ومع ذلك يكون احتمال أن ينشئ جهاز آخر الرقم نفسه ضئيلاً لدرجة أنه يهمل عملياً. وكانت مواصفة ال UUID لسنوات طويلة هي RFC 4122، ثم صدرت في مايو 2024 المواصفة [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562.html?ref=arabroot.io) التي حلت محلها Obsoletes RFC 4122، وأضافت الإصدارات 6 و7 و8 وال UUID الأقصى Max UUID.

والشيء الذي لا يعرفه كثير من المطورين أن ال UUID ليس 128 بتاً عشوائية بالكامل، وإنما فيه حقلان ثابتان يخبرانك بنوعه: حقل الإصدار Version في أول 4 بتات من الخانة 13، وحقل النوع Variant في أول بتين من الخانة 17 وقيمتهما `10` لكل UUID يتبع هذه المواصفة، ولذلك تجد الخانة 17 دائماً أحد الحروف `8` أو `9` أو `a` أو `b`. والصورة التالية تبين ذلك على قيمتين حقيقيتين من التجربة التي سوف نجريها بعد قليل:

![مخطط يعرض UUIDv7 قيمته 01a10e17-db10-75d5-8941-89473c30a15a مقسمة إلى أول 12 خانة للوقت unix_ts_ms بطول 48 بت وقيمته 2026-10-05 22:03:26.864 UTC، ثم خانة الإصدار 7، ثم 12 بت عشوائية rand_a، ثم خانة النوع 8 وأول بتين منها 10، ثم 62 بت عشوائية rand_b. وتحته UUIDv4 قيمته 094691a5-657f-4400-89f0-ab7fc433cf6e كله عشوائي ما عدا خانة الإصدار 4 وخانة النوع، بمجموع 122 بت عشوائية](https://arabroot.io/content/images/2026/10/unique-ids-02-uuid-anatomy.webp)

رقم الإصدار في الخانة 13، والنوع في الخانة 17، والباقي يحدده الإصدار

في الصورة أعلاه لاحظ التالي:

- في UUIDv4 تبقى 122 بتاً عشوائية بعد حجز 4 بتات للإصدار وبتين للنوع، وهذا الرقم 122 هو الذي سوف نحسب منه احتمال التكرار.
- في UUIDv7 أول 48 بتاً هي الوقت بالمللي ثانية منذ بداية عصر Unix، وهذا يعني أن القيم التي تنشأ لاحقاً تكون أكبر، وأنك تستطيع أن تقرأ وقت الإنشاء من المعرف نفسه. وإذا أردت أن تعرف أكثر عن وقت Unix والمناطق الزمنية فقد شرحناها في دليل [التاريخ والوقت وUTC والمناطق الزمنية](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%A7%D8%B1%D9%8A%D8%AE-%D9%88%D8%A7%D9%84%D9%88%D9%82%D8%AA-utc-%D9%88%D8%A7%D9%84%D9%85%D9%86%D8%A7%D8%B7%D9%82-%D8%A7%D9%84%D8%B2%D9%85%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D8%A7%D9%84%D9%87%D8%AC%D8%B1%D9%8A/).
- الخانات مكتوبة بال Hex، وكل خانة 4 بتات، وقد شرحنا ال Hex وغيره من الترميزات في [مقال الترميز](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/#hex).

### إصدارات ال UUID وما يناسب كل واحد

المواصفة RFC 9562 تعرف ثمانية إصدارات، وكل إصدار طريقة مختلفة لملء البتات، والجدول التالي يلخصها:

| الإصدار | ما بداخله                                                            | متى يستخدم                                                                       |
| ------- | -------------------------------------------------------------------- | -------------------------------------------------------------------------------- |
| v1      | وقت بدقة 100 نانو ثانية منذ 15 أكتوبر 1582، وعداد، وعنوان MAC للجهاز | أنظمة قديمة، ولا ينصح به في الأنظمة الجديدة لأنه يكشف عنوان الجهاز               |
| v3 وv5  | هاش Hash لاسم داخل نطاق Namespace، بخوارزمية MD5 في v3 وSHA-1 في v5  | عندما تريد أن يعطيك الاسم نفسه المعرف نفسه دائماً، والمواصفة تفضل v5 على v3      |
| v4      | 122 بتاً عشوائية                                                     | المعرف العام الذي لا يكشف شيئاً، وهو الأكثر انتشاراً                             |
| v6      | محتوى v1 نفسه مع إعادة ترتيب حقول الوقت حتى يترتب المعرف بالوقت      | للأنظمة التي تستخدم v1 وتريد ترتيباً أفضل، والمواصفة توصي ب v7 بدلاً منه إن أمكن |
| v7      | وقت Unix بالمللي ثانية في 48 بتاً، والباقي عشوائي أو عداد            | المفتاح الأساسي في قواعد البيانات، والأحداث والسجلات                             |
| v8      | مخصص، والمواصفة تحدد الإصدار والنوع فقط                              | صيغة خاصة بنظامك، وفي حالات نادرة                                                |

وبالإضافة إلى ذلك تعرف المواصفة قيمتين خاصتين: ال UUID الفارغ Nil UUID وكل بتاته أصفار `00000000-0000-0000-0000-000000000000`، وال UUID الأقصى Max UUID وكل بتاته واحد `ffffffff-ffff-ffff-ffff-ffffffffffff`، وتستخدمان علامة على «لا قيمة» أو حداً في المقارنات، ولا يجب أن تعطى أي منهما لسجل حقيقي.

والمواصفة نفسها واضحة في موقفها من v1، فهي تقول في [قسم الاعتبارات الأمنية](https://www.rfc-editor.org/rfc/rfc9562.html?ref=arabroot.io#section-8) إن عناوين MAC فيها مخاطر على الخصوصية ولا ينبغي أن توضع في ال UUID، وتوصي في [تعريف الإصدار 7](https://www.rfc-editor.org/rfc/rfc9562.html?ref=arabroot.io#section-5.7) باستخدامه بدلاً من v1 وv6 متى أمكن. لذلك فالاختيار العملي اليوم ينحصر غالباً بين v4 وv7، ومعهما v5 للحالات التي تحتاج فيها إلى معرف ثابت من اسم.

### هل يمكن أن يتكرر UUIDv4؟

الإجابة النظرية نعم، والإجابة العملية لا، والتفصيل فيما يلي. عدد القيم الممكنة في UUIDv4 هو 2 مرفوعة للقوة 122، أي قرابة 5.3 × 1036، وقد يظن البعض أن التكرار لا يحدث إلا بعد أن تنشئ عدداً قريباً من هذا، ولكن هنا تدخل [مسألة يوم الميلاد Birthday Problem](https://en.wikipedia.org/wiki/Birthday%5Fproblem?ref=arabroot.io): في غرفة فيها 23 شخصاً فقط يكون احتمال أن يشترك اثنان في يوم الميلاد أكثر من 50%، مع أن عدد أيام السنة 365، والسبب أن المقارنة تكون بين كل زوج من الأشخاص وليس بينك وبين الآخرين فقط. والقاعدة التقريبية لهذه المسألة أن احتمال التكرار بعد إنشاء n معرف من N قيمة ممكنة يساوي تقريباً:

```text
p ≈ 1 - e^(-n² / 2N)
```

وبالتالي يصل الاحتمال إلى 50% عندما يكون n قريباً من `sqrt(2N × ln 2)`. ولن نترك هذا للتخمين، فالسكربت `ids.py` الذي سوف نشغله في التجربة يحسب هذه الأرقام، وهذا الجزء منه:

```python
# birthday bound for UUIDv4: 122 random bits
N = 2 ** 122
n50 = math.sqrt(2 * N * math.log(2))
print(f"\n2^122 = {N:.3e}")
print(f"IDs for a 50% chance of one collision: {n50:.3e}")
n = 1_000_000_000 * 86400 * 365          # one billion per second for a year
p = -math.expm1(-n * n / (2 * N))
print(f"one billion per second for a year = {n:.3e} IDs -> p(collision) = {p:.2e}")
```

والمخرج سوف يكون كما يلي:

```text
2^122 = 5.317e+36
IDs for a 50% chance of one collision: 2.715e+18
one billion per second for a year = 3.154e+16 IDs -> p(collision) = 9.35e-05
```

أي أنك تحتاج إلى قرابة 2.7 مليار مليار معرف حتى يصبح احتمال تكرار واحد 50%، وإذا أنشأت مليار معرف في كل ثانية لمدة سنة كاملة فاحتمال أن يتكرر معرف واحد أقل من واحد في عشرة آلاف، وهذا يتفق مع ما تذكره [صفحة UUID في Wikipedia](https://en.wikipedia.org/wiki/Universally%5Funique%5Fidentifier?ref=arabroot.io). ولكن هذه الأرقام كلها تفترض شرطاً واحداً مهماً وهو أن العشوائية حقيقية، أي من مولد أرقام عشوائية آمن تشفيرياً Cryptographically Secure، فإذا استخدمت مكتبة تعتمد على `Math.random()` أو مولداً بدأ من البذرة Seed نفسها في كل Container، فالحساب أعلاه لا قيمة له. ومع ذلك يبقى المفتاح الأساسي وقيد التفرد Unique Constraint في قاعدة البيانات، فهو شبكة الأمان الأخيرة التي ترفض أي تكرار مهما كان احتماله.

## تجربة عملية: ننشئ المعرفات ونفككها

الآن سوف نقوم بإنشاء كل نوع ونقرأ ما بداخله، وكل التجارب تعمل داخل Containers مؤقتة تحذف بعد التشغيل، فلا تحتاج أن تثبت Node.js أو Python على جهازك. ونبدأ بالملف `ids.mjs` الذي يستخدم مكتبة `crypto` المدمجة في Node.js، حيث الدالة [crypto.randomUUID()](https://nodejs.org/api/crypto.html?ref=arabroot.io) موجودة منذ الإصدار 14.17 وتنشئ UUIDv4، والدالة `crypto.randomUUIDv7()` أضيفت في الإصدار 26.1.0 وتنشئ UUIDv7:

```javascript
import { randomUUID, randomUUIDv7 } from 'node:crypto';

console.log('v4:', randomUUID());
console.log('v4:', randomUUID());
const v7 = [randomUUIDv7(), randomUUIDv7(), randomUUIDv7()];
for (const u of v7) console.log('v7:', u);

// decode a v7: the first 48 bits (12 hex digits) are Unix time in milliseconds
const u = v7[0];
const hex = u.replaceAll('-', '');
const ms = parseInt(hex.slice(0, 12), 16);
console.log('\ndecode', u);
console.log('  timestamp (48 bits):', hex.slice(0, 12), '=', ms, 'ms =', new Date(ms).toISOString());
console.log('  version digit      :', hex[12]);
console.log('  variant (bits 64-65):', (parseInt(hex[16], 16) >> 2).toString(2));
```

ونشغله في Container من Node.js 26:

```bash
docker run --rm --name lab-ids-node -v "$PWD":/lab -w /lab node:26-alpine node ids.mjs
```

والمخرج سوف يكون كما يلي:

```text
v4: 094691a5-657f-4400-89f0-ab7fc433cf6e
v4: bb36fcef-fb89-4ea2-844c-221195951173
v7: 01a10e17-db10-75d5-8941-89473c30a15a
v7: 01a10e17-db10-705c-acee-e6c832b3ce1e
v7: 01a10e17-db10-7ae1-acb9-40f56348133b

decode 01a10e17-db10-75d5-8941-89473c30a15a
  timestamp (48 bits): 01a10e17db10 = 1791237806864 ms = 2026-10-05T22:03:26.864Z
  version digit      : 7
  variant (bits 64-65): 10
```

في المخرج أعلاه لاحظ التالي:

- قيمتا v4 لا يجمعهما شيء، أما قيم v7 الثلاث فتبدأ كلها بـ `01a10e17-db10` لأنها أنشئت في المللي ثانية نفسها، وفك أول 12 خانة أعطانا وقت الإنشاء بالضبط.
- القيم الثلاث نفسها ليست مرتبة فيما بينها: الثانية `705c` أصغر من الأولى `75d5`، والسبب أن توثيق Node.js يذكر صراحة أن الوقت في `randomUUIDv7()` يعتمد على ساعة غير رتيبة Non-monotonic وأن القيم ليست مضمونة التزايد، والبتات بعد الوقت عشوائية بالكامل. وهذا لا يهم في أغلب الحالات، ولكن إذا كان ترتيب الأحداث داخل المللي ثانية الواحدة مهماً لك فاعتمد على عمود وقت أو عداد، أو على مكتبة تضمن الرتابة Monotonicity كما سوف نرى في Python.
- الخانة 17 هي `8`، أي `1000` بالثنائي، وأول بتين منها `10` وهما علامة النوع.

ننتقل الآن إلى Python، حيث أضاف الإصدار 3.14 إلى [الوحدة uuid](https://docs.python.org/3.14/library/uuid.html?ref=arabroot.io) الدوال `uuid6()` و`uuid7()` و`uuid8()` والثابتين `NIL` و`MAX`، والملف `ids.py` كما يلي:

```python
import uuid, sys, datetime, math

print(sys.version.split()[0])
print("uuid4:", uuid.uuid4())
a, b, c = uuid.uuid7(), uuid.uuid7(), uuid.uuid7()
print("uuid7:", a); print("uuid7:", b); print("uuid7:", c)
print("sorted in creation order:", [a, b, c] == sorted([a, b, c]))

ms = a.int >> 80
print("uuid7 time:", datetime.datetime.fromtimestamp(ms / 1000, datetime.UTC).isoformat(), "| version:", a.version, "| variant:", a.variant)

# name-based: same namespace + same name -> the same UUID every time
print("uuid5:", uuid.uuid5(uuid.NAMESPACE_DNS, "arabroot.io"))
print("uuid5:", uuid.uuid5(uuid.NAMESPACE_DNS, "arabroot.io"))
print("uuid3:", uuid.uuid3(uuid.NAMESPACE_DNS, "arabroot.io"))

v1 = uuid.uuid1()
print("uuid1:", v1, "| node (MAC) =", ":".join(f"{(v1.node >> s) & 0xff:02x}" for s in range(40, -1, -8)))
print("uuid6:", uuid.uuid6())
print("NIL  :", uuid.NIL); print("MAX  :", uuid.MAX)

# birthday bound for UUIDv4: 122 random bits
N = 2 ** 122
n50 = math.sqrt(2 * N * math.log(2))
print(f"\n2^122 = {N:.3e}")
print(f"IDs for a 50% chance of one collision: {n50:.3e}")
n = 1_000_000_000 * 86400 * 365          # one billion per second for a year
p = -math.expm1(-n * n / (2 * N))
print(f"one billion per second for a year = {n:.3e} IDs -> p(collision) = {p:.2e}")
```

```bash
docker run --rm --name lab-ids-py -v "$PWD":/lab -w /lab python:3.14-alpine python ids.py
```

والمخرج سوف يكون كما يلي:

```text
3.14.8
uuid4: 29e32724-b34c-4e7f-b9ea-aef9fe17bb6a
uuid7: 01a10e18-d8c8-73bc-9187-9ee921ce4092
uuid7: 01a10e18-d8c8-73bc-9187-9eeaf95235e9
uuid7: 01a10e18-d8c8-73bc-9187-9eeb5ce9f871
sorted in creation order: True
uuid7 time: 2026-10-05T22:04:31.816000+00:00 | version: 7 | variant: specified in RFC 4122
uuid5: 595fe484-c908-54f9-8e8b-4b790423d2af
uuid5: 595fe484-c908-54f9-8e8b-4b790423d2af
uuid3: cc61b52e-bf2a-3294-97ca-c08302768142
uuid1: be210496-c108-11f1-8422-b2b210ec2f86 | node (MAC) = b2:b2:10:ec:2f:86
uuid6: 1f1c108b-e210-6f66-80c6-b2b210ec2f86
NIL  : 00000000-0000-0000-0000-000000000000
MAX  : ffffffff-ffff-ffff-ffff-ffffffffffff

2^122 = 5.317e+36
IDs for a 50% chance of one collision: 2.715e+18
one billion per second for a year = 3.154e+16 IDs -> p(collision) = 9.35e-05
```

في المخرج أعلاه لاحظ التالي:

- قيم `uuid7` الثلاث مرتبة هذه المرة، والفرق بينها في آخر الحقل الثالث، والسبب أن Python كما في توثيقها تستخدم بعد الوقت عداداً طوله 42 بتاً يضمن الرتابة داخل المللي ثانية الواحدة، وهذا هو الفرق العملي بين مكتبة وأخرى رغم أن الاثنتين تتبعان المواصفة نفسها.
- `uuid5` أعطى القيمة نفسها مرتين للاسم `arabroot.io`، وهذا هو الغرض منه: إذا استوردت بيانات من نظام خارجي مرتين فإن المعرف المحسوب من المفتاح الخارجي يبقى نفسه، فلا تتكرر السجلات. ولكن تذكر أنه ليس سراً، فكل من يعرف الاسم والنطاق يستطيع أن يحسب المعرف نفسه.
- آخر 12 خانة في `uuid1` و`uuid6` متطابقة `b2b210ec2f86`، وهي عنوان الجهاز كما سوف نتأكد الآن.
- Python ما زالت تطبع النوع بالاسم القديم «specified in RFC 4122»، وهو النوع نفسه الذي تسميه RFC 9562 بالبتين `10`.

وحتى نرى بعيننا أن v1 يكشف عنوان الجهاز، نقرأ عنوان MAC لبطاقة الشبكة داخل Container جديد ثم ننشئ UUIDv1 في الـ Container نفسه:

```bash
docker run --rm --name lab-ids-py python:3.14-alpine sh -c 'cat /sys/class/net/eth0/address; python -c "import uuid; print(uuid.uuid1())"; python -m uuid -u uuid7'
```

```text
52:2d:bb:84:7b:63
d703a478-c108-11f1-83ea-522dbb847b63
01a10e19-8654-75fa-9fab-e38bf0ae05ca
```

آخر 12 خانة في ال UUID هي `522dbb847b63`، أي عنوان MAC الذي في السطر الأول حرفاً بحرف، فكل من يرى هذا المعرف في رابط أو مستند يعرف عنوان الجهاز الذي أنشأه ووقت إنشائه، وفي الأجهزة الحقيقية خارج Docker يكون هذا العنوان ثابتاً ومرتبطاً بالجهاز نفسه، لذلك لا تقم باستخدام v1 في أي معرف يخرج من نظامك. والسطر الأخير يبين أن Python 3.14 تنشئ UUIDv7 من سطر الأوامر مباشرة بالخيار `-u uuid7`، وهو مفيد في السكربتات.

## لماذا يتعب UUIDv4 فهرس قاعدة البيانات؟

المفتاح الأساسي في PostgreSQL وMySQL وأغلب قواعد البيانات له فهرس من نوع B-tree، وهو شجرة من الصفحات Pages حجم كل منها 8 كيلوبايت في PostgreSQL، والقيم في صفحاتها السفلى Leaf Pages مرتبة. فعندما تضيف رقماً متسلسلاً أو UUIDv7 فالقيمة الجديدة أكبر من كل ما قبلها، وبالتالي تذهب دائماً إلى آخر صفحة، وهذه الصفحة موجودة في الذاكرة لأنها استخدمت قبل لحظة، وعندما تمتلئ تبدأ صفحة جديدة وتبقى القديمة ممتلئة كما هي. أما UUIDv4 فقيمته عشوائية، فكل إدراج يذهب إلى صفحة عشوائية في الفهرس، وهذا يعني أن كل صفحات الفهرس يجب أن تكون في الذاكرة حتى تكون الكتابة سريعة، وعندما تمتلئ صفحة في المنتصف تنقسم Page Split إلى صفحتين نصف ممتلئتين، فيكبر الفهرس ويزيد ما يكتب في سجل WAL. والمواصفة RFC 9562 نفسها تذكر في [قسم الدوافع](https://www.rfc-editor.org/rfc/rfc9562.html?ref=arabroot.io#section-2.1) أن الإصدارات غير المرتبة بالوقت مثل v4 لها موقعية Locality ضعيفة في فهارس قواعد البيانات، وأن أثرها على الأداء قد يكون كبيراً. والصورة التالية تبين الفرق:

![مخطط يقارن فهرس B-tree مع UUIDv7 وUUIDv4. مع UUIDv7 الصفحات القديمة ممتلئة بنسبة 90% وكل معرف جديد يضاف إلى آخر صفحة التي تبقى في الذاكرة، والنتيجة فهرس حجمه 60 ميجابايت لمليوني صف مقابل 43 ميجابايت للرقم bigint. ومع UUIDv4 كل معرف جديد يقع في صفحة عشوائية، فتبقى كل الصفحات في الذاكرة وتنقسم الصفحات الممتلئة إلى صفحتين نصف ممتلئتين، والنتيجة فهرس حجمه 76 ميجابايت وصفحات ممتلئة بنسبة 71% في المتوسط](https://arabroot.io/content/images/2026/10/unique-ids-03-btree.webp)

UUIDv7 يضاف في آخر الفهرس، وUUIDv4 يتوزع على كل صفحاته

### تجربة على PostgreSQL 18: v4 مقابل v7

أضاف PostgreSQL 18 الصادر في سبتمبر 2025 الدالة [uuidv7()](https://www.postgresql.org/docs/18/functions-uuid.html?ref=arabroot.io) التي تنشئ UUIDv7 من وقت Unix بالمللي ثانية مع جزء من المللي ثانية والباقي عشوائي، والاسم المستعار `uuidv4()` للدالة القديمة `gen_random_uuid()`، بالإضافة إلى `uuid_extract_timestamp()` و`uuid_extract_version()`. والآن سوف نقوم بإنشاء أربعة جداول متطابقة لا يختلف بينها إلا نوع المفتاح الأساسي، ثم ندرج مليوني صف في كل منها، ونقيس حجم الفهرس ومتوسط امتلاء صفحاته بالامتداد [pgstattuple](https://www.postgresql.org/docs/18/pgstattuple.html?ref=arabroot.io). والملف `bench.sql` كما يلي:

```sql
\timing on
CREATE EXTENSION pgstattuple;
CREATE TABLE ids_bigint (id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, n int);
CREATE TABLE ids_v4     (id uuid PRIMARY KEY DEFAULT uuidv4(), n int);
CREATE TABLE ids_v7     (id uuid PRIMARY KEY DEFAULT uuidv7(), n int);
CREATE TABLE ids_text   (id text PRIMARY KEY DEFAULT uuidv4()::text, n int);

INSERT INTO ids_bigint (n) SELECT g FROM generate_series(1, 2000000) g;
INSERT INTO ids_v7     (n) SELECT g FROM generate_series(1, 2000000) g;
INSERT INTO ids_v4     (n) SELECT g FROM generate_series(1, 2000000) g;
INSERT INTO ids_text   (n) SELECT g FROM generate_series(1, 2000000) g;
\timing off
SELECT c.relname AS index, pg_size_pretty(pg_relation_size(c.oid)) AS size,
       round((pgstatindex(c.oid::regclass)).avg_leaf_density::numeric, 1) AS leaf_density_pct
FROM pg_class c WHERE c.relname IN ('ids_bigint_pkey','ids_v7_pkey','ids_v4_pkey','ids_text_pkey')
ORDER BY pg_relation_size(c.oid);
```

ونشغله في Container من PostgreSQL 18 دون أن ننشر أي منفذ، لأن كل الأوامر تعمل من داخله:

```bash
docker run -d --name lab-ids-pg -e POSTGRES_PASSWORD=lab --shm-size=256m postgres:18
until docker exec lab-ids-pg pg_isready -U postgres -q; do sleep 1; done
docker cp bench.sql lab-ids-pg:/bench.sql
docker exec lab-ids-pg psql -U postgres -f /bench.sql
```

والمخرج سوف يكون كما يلي:

```text
Timing is on.
CREATE EXTENSION
Time: 5046.321 ms (00:05.046)
CREATE TABLE
Time: 2918.261 ms (00:02.918)
CREATE TABLE
Time: 1173.312 ms (00:01.173)
CREATE TABLE
Time: 1264.814 ms (00:01.265)
CREATE TABLE
Time: 508.689 ms
INSERT 0 2000000
Time: 148085.376 ms (02:28.085)
INSERT 0 2000000
Time: 268736.718 ms (04:28.737)
INSERT 0 2000000
Time: 315521.396 ms (05:15.521)
INSERT 0 2000000
Time: 362911.564 ms (06:02.912)
Timing is off.
      index      |  size  | leaf_density_pct
-----------------+--------+------------------
 ids_bigint_pkey | 43 MB  |             90.0
 ids_v7_pkey     | 60 MB  |             90.0
 ids_v4_pkey     | 76 MB  |             71.4
 ids_text_pkey   | 146 MB |             69.5
(4 rows)
```

في المخرج أعلاه لاحظ التالي:

- فهرس v7 صفحاته ممتلئة بنسبة 90% مثل الرقم المتسلسل تماماً، أما فهرس v4 فمتوسط امتلاء صفحاته 71.4%، وهذا أثر انقسام الصفحات في المنتصف، ولذلك كان حجمه 76 ميجابايت مقابل 60 لـ v7 مع أن القيم بالحجم نفسه 16 بايتاً.
- الفهرس على ال UUID المحفوظ نصاً `text` حجمه 146 ميجابايت، أي قرابة ضعف فهرس v4 بالنوع `uuid`، فلا تقم بحفظ ال UUID في عمود نصي أبداً، والسبب أن النص 36 حرفاً بالإضافة إلى ترويسة الطول، أما النوع `uuid` فهو 16 بايتاً كما سوف نرى بعد قليل، ومقارنته أسرع لأنها مقارنة بايتات وليست مقارنة نصوص حسب ترتيب اللغة Collation.
- الأوقات بطيئة كلها لأن التجربة تعمل على Docker Desktop في Windows على جهاز تطوير عادي، والمهم فيها الترتيب: الرقم المتسلسل أسرع، ثم v7، ثم v4، ثم النص. وهذه الأرقام للتوضيح فقط وتختلف حسب الجهاز والإعدادات، ثم إن ترتيب التشغيل نفسه يؤثر فيها لأن كل جدول يضاف بعد الذي قبله.

ولأن الأوقات وحدها تتأثر بأشياء كثيرة، نعيد التجربة على مليون صف لكل من v7 وv4 بعد أن نفرغ الذاكرة إلى القرص بالأمر `CHECKPOINT`، ونطلب من PostgreSQL أن يخبرنا بعدد الصفحات التي لمسها وحجم ما كتبه في سجل WAL، والملف `bench2.sql` كما يلي:

```sql
DROP TABLE IF EXISTS ids_bigint, ids_v4, ids_v7, ids_text;
CREATE TABLE ids_v4 (id uuid PRIMARY KEY DEFAULT uuidv4(), n int);
CREATE TABLE ids_v7 (id uuid PRIMARY KEY DEFAULT uuidv7(), n int);
CHECKPOINT;
EXPLAIN (ANALYZE, BUFFERS, WAL, COSTS OFF)
INSERT INTO ids_v7 (n) SELECT g FROM generate_series(1, 1000000) g;
CHECKPOINT;
EXPLAIN (ANALYZE, BUFFERS, WAL, COSTS OFF)
INSERT INTO ids_v4 (n) SELECT g FROM generate_series(1, 1000000) g;
```

```bash
docker cp bench2.sql lab-ids-pg:/bench2.sql
docker exec lab-ids-pg psql -U postgres -f /bench2.sql
```

والمخرج سوف يكون كما يلي:

```text
DROP TABLE
CREATE TABLE
CREATE TABLE
CHECKPOINT
                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Insert on ids_v7 (actual time=96446.603..96446.604 rows=0.00 loops=1)
   Buffers: shared hit=2109994 read=1 dirtied=10224 written=10226, temp read=1709 written=1709
   WAL: records=2003851 fpi=1 bytes=150264015 buffers full=2278
   ->  Function Scan on generate_series g (actual time=345.861..5857.081 rows=1000000.00 loops=1)
         Buffers: temp read=1709 written=1709
 Planning Time: 0.053 ms
 Execution Time: 96479.204 ms
(7 rows)

CHECKPOINT
                                            QUERY PLAN
---------------------------------------------------------------------------------------------------
 Insert on ids_v4 (actual time=145026.546..145026.547 rows=0.00 loops=1)
   Buffers: shared hit=3960684 read=45 dirtied=11249 written=11200, temp read=1709 written=1709
   WAL: records=2004820 fpi=1 bytes=164547039 buffers full=294
   ->  Function Scan on generate_series g (actual time=725.354..14960.446 rows=1000000.00 loops=1)
         Buffers: temp read=1709 written=1709
 Planning Time: 0.077 ms
 Execution Time: 145029.759 ms
(7 rows)
```

في المخرج أعلاه لاحظ التالي:

- `shared hit` هو عدد مرات قراءة صفحة من ذاكرة PostgreSQL المشتركة، وكان قرابة 3.96 مليون مع v4 مقابل 2.11 مليون مع v7 للعدد نفسه من الصفوف، أي أن كل إدراج عشوائي احتاج إلى المرور بصفحات أكثر في الشجرة.
- حجم ما كتب في WAL كان قرابة 164.5 ميجابايت مع v4 مقابل 150.3 مع v7، والفرق يأتي من انقسام الصفحات الذي يكتب صفحات جديدة.
- الفهرس هنا صغير ويتسع بالكامل في الذاكرة، لذلك الفرق معتدل. والفرق الحقيقي يظهر عندما يصبح الجدول أكبر من الذاكرة، فعندها يحتاج كل إدراج في v4 إلى قراءة صفحة عشوائية من القرص، أما v7 فيبقى يكتب في آخر الفهرس الموجود في الذاكرة. وإذا كانت جداولك تتجه إلى هذا الحجم فقد تحدثنا عنها في دليل [PostgreSQL للجداول الضخمة](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/).

وأخيراً نتأكد من حجم كل نوع، ونقرأ الوقت والإصدار من قيم v7 المحفوظة، ثم نحذف ال Container مع ال Volume الذي أنشأه:

```bash
docker exec lab-ids-pg psql -U postgres \
  -c "SELECT relname AS index, pg_size_pretty(pg_relation_size(oid)) AS size, round((pgstatindex(oid::regclass)).avg_leaf_density::numeric,1) AS leaf_density_pct FROM pg_class WHERE relname IN ('ids_v4_pkey','ids_v7_pkey') ORDER BY 1;" \
  -c "SELECT id, uuid_extract_version(id) AS ver, uuid_extract_timestamp(id) AS created FROM ids_v7 ORDER BY id LIMIT 3;" \
  -c "SELECT uuidv7();" \
  -c "SELECT pg_column_size('01a10e17-db10-75d5-8941-89473c30a15a'::uuid) AS uuid_bytes, pg_column_size('01a10e17-db10-75d5-8941-89473c30a15a'::text) AS text_bytes, pg_column_size(1::bigint) AS bigint_bytes;"
docker rm -f -v lab-ids-pg
```

```text
    index    | size  | leaf_density_pct
-------------+-------+------------------
 ids_v4_pkey | 38 MB |             72.0
 ids_v7_pkey | 30 MB |             90.0
(2 rows)

                  id                  | ver |          created
--------------------------------------+-----+----------------------------
 01a10e32-50f2-7f79-8a8a-25a9d84cd1b4 |   7 | 2026-10-05 22:32:20.978+00
 01a10e32-50f3-7697-bd35-42c9ce8911c8 |   7 | 2026-10-05 22:32:20.979+00
 01a10e32-50f3-76df-83b9-a3a500ceb086 |   7 | 2026-10-05 22:32:20.979+00
(3 rows)

                uuidv7
--------------------------------------
 01a10e37-5cff-7a21-81bb-2ac02b55fe06
(1 row)

 uuid_bytes | text_bytes | bigint_bytes
------------+------------+--------------
         16 |         40 |            8
(1 row)
```

النتيجة نفسها على مليون صف: فهرس v7 أصغر بربع تقريباً وصفحاته ممتلئة بنسبة 90% مقابل 72% لـ v4، وال UUID يأخذ 16 بايتاً في النوع `uuid` مقابل 40 بايتاً إذا حفظته نصاً، والرقم `bigint` يبقى الأصغر بـ 8 بايتات. ولاحظ أيضاً أن `uuid_extract_timestamp()` قرأت وقت الإنشاء من المعرف نفسه، فلا تحتاج إلى عمود `created_at` لترتيب الصفوف، ولكن احتفظ به إذا كنت تحتاج دقة أعلى أو تريد أن تخفي الوقت عن المستخدم كما سوف نرى في الملاحظات الأمنية. والخيار `-v` في الأمر الأخير يحذف ال Volume المجهول Anonymous Volume الذي ينشئه Docker Image الخاص بـ PostgreSQL، وإذا نسيته يبقى على القرص بعد حذف ال Container.

## ULID وKSUID وNanoID: المعرفات النصية

قبل أن تصدر RFC 9562 كانت مشكلة ترتيب ال UUID معروفة، فظهرت صيغ أخرى تحلها، وما زالت هذه الصيغ منتشرة ولها مكتبات في أغلب اللغات:

- [**ULID**](https://github.com/ulid/spec?ref=arabroot.io)**:** اختصار Universally Unique Lexicographically Sortable Identifier، وهو 128 بتاً مثل ال UUID، أولها 48 بتاً للوقت بالمللي ثانية ثم 80 بتاً عشوائية، أي أنه قريب جداً في فكرته من UUIDv7، ولكنه يكتب في 26 حرفاً بترميز [Crockford Base32](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/#crockford) بدلاً من 36 حرفاً، ولا يحتوي على شرطات، ولا يفرق بين الحروف الكبيرة والصغيرة، والمواصفة تشرح طريقة رتيبة Monotonic تزيد الجزء العشوائي بواحد إذا أنشئ أكثر من معرف في المللي ثانية نفسها.
- [**KSUID**](https://github.com/segmentio/ksuid?ref=arabroot.io)**:** من شركة Segment، وطوله 20 بايتاً: 32 بتاً للوقت بالثواني من بداية عصر خاص هو 13 مايو 2014، ثم 128 بتاً عشوائية، ويكتب دائماً في 27 حرفاً بترميز Base62\. وعدد البتات العشوائية فيه أكبر من UUIDv4، ولكن دقة الوقت بالثانية فقط، ولا يتسع في عمود `uuid` لأنه 160 بتاً.
- [**NanoID**](https://github.com/ai/nanoid?ref=arabroot.io)**:** مكتبة لمعرف عشوائي قصير مناسب للروابط، طوله الافتراضي 21 حرفاً من الأبجدية `A-Za-z0-9_-`، أي 64 رمزاً كل منها 6 بتات، وبالتالي 126 بتاً عشوائية وهو قريب من UUIDv4 في احتمال التكرار. وتستطيع أن تغير الطول والأبجدية بالدالة `customAlphabet`، ولكن كلما قصرت المعرف زاد احتمال التكرار، فاحسبه بالقاعدة السابقة واعتمد على قيد التفرد في قاعدة البيانات.

والمثال التالي `others.mjs` ينشئ كل نوع، ويستخدم معه Sqids الذي سوف نشرحه بعد قليل:

```javascript
import { ulid, decodeTime, monotonicFactory } from 'ulid';
import { nanoid, customAlphabet } from 'nanoid';
import Sqids from 'sqids';

const u = ulid();
console.log('ULID   :', u, `(${u.length} chars)`, '->', new Date(decodeTime(u)).toISOString());
const mono = monotonicFactory();
console.log('ULID x3 in one ms (monotonic):', mono(), mono(), mono());
const n = nanoid();
console.log('NanoID :', n, `(${n.length} chars)`);
const short = customAlphabet('0123456789abcdefghijklmnopqrstuvwxyz', 8);
console.log('NanoID custom (8 chars, a-z0-9):', short());

const sqids = new Sqids({ minLength: 8 });
for (const id of [1, 2, 3, 1000]) console.log(`Sqids  : ${id} ->`, sqids.encode([id]));
console.log('decode back:', sqids.decode(sqids.encode([1000])));
```

```bash
docker run --rm --name lab-ids-node -v "$PWD":/lab -w /lab node:26-alpine sh -c 'npm i --silent ulid nanoid sqids >/dev/null 2>&1; npm ls --depth=0; node others.mjs'
```

والمخرج سوف يكون كما يلي:

```text
lab@ /lab
+-- nanoid@6.0.2
+-- sqids@0.3.0
`-- ulid@3.0.2

ULID   : 01M471GYFFE12DN1S15DEVK9VS (26 chars) -> 2026-10-05T22:04:07.535Z
ULID x3 in one ms (monotonic): 01M471GYG4DAV6S3CPGK1PTKY4 01M471GYG4DAV6S3CPGK1PTKY5 01M471GYG5YRCS24TP7V74P84A
NanoID : Uv1AY8wikMaVvXwP42H8D (21 chars)
NanoID custom (8 chars, a-z0-9): dpw0m5jr
Sqids  : 1 -> UkLWZg9D
Sqids  : 2 -> gbHJdmfr
Sqids  : 3 -> EfhxLZ9c
Sqids  : 1000 -> pndklVeM
decode back: [ 1000 ]
```

في المخرج أعلاه لاحظ التالي:

- أول معرفين من ULID الرتيب يختلفان في الحرف الأخير فقط `Y4` ثم `Y5`، لأنهما في المللي ثانية نفسها فزادت المكتبة الجزء العشوائي بواحد، ثم بدأ الثالث بمللي ثانية جديدة `G5` وجزء عشوائي جديد.
- ال NanoID المخصص بثمانية حروف من 36 رمزاً فيه قرابة 41 بتاً فقط، وهذا يكفي لرابط قصير في نظام صغير مع قيد تفرد، ولا يكفي لمعرف يحمي شيئاً.
- ULID يحفظ في قاعدة البيانات في عمود `uuid` لأنه 128 بتاً، ولكن بعد تحويله من Base32 إلى بايتات، وإذا كنت تبدأ مشروعاً جديداً على PostgreSQL 18 فال UUIDv7 يعطيك الشيء نفسه دون مكتبة إضافية.

## Snowflake: معرف بحجم bigint من تويتر

في يونيو 2010 كانت تويتر تنقل جزءاً من بياناتها من MySQL إلى Cassandra، وCassandra لا تملك طريقة مدمجة لإنشاء أرقام متسلسلة، وكانت الشركة تحتاج إلى عشرات الآلاف من المعرفات في الثانية دون أن تنتظر الأجهزة بعضها، وبمعرف يترتب بالوقت تقريباً ولا يزيد عن 64 بتاً حتى يبقى رقماً عادياً في قواعد البيانات. فأعلنت عن خدمة Snowflake ونشرت [الكود الأصلي](https://github.com/twitter-archive/snowflake/tree/snowflake-2010?ref=arabroot.io)، ثم بدأت استخدامها لمعرفات التغريدات في أكتوبر 2010، وكتبت TechCrunch يومها أن [أرقام التغريدات لن تعكس بعد ذلك عدد التغريدات](https://techcrunch.com/2010/10/12/twitter-snowflake/?ref=arabroot.io)، أي أن تويتر أغلقت بالمناسبة باب حساب حجم نشاطها من الأرقام.

والمعرف 64 بتاً، أولها صفر حتى يبقى الرقم موجباً، ثم 41 بتاً للوقت بالمللي ثانية من بداية عصر خاص بتويتر، ثم 10 بتات لرقم الجهاز مقسمة في الكود الأصلي [IdWorker.scala](https://github.com/twitter-archive/snowflake/blob/snowflake-2010/src/main/scala/com/twitter/service/snowflake/IdWorker.scala?ref=arabroot.io) إلى 5 لمركز البيانات Datacenter و5 للعامل Worker، ثم 12 بتاً لعداد يبدأ من الصفر في كل مللي ثانية، أي 4096 معرفاً لكل جهاز في المللي ثانية. والفكرة أن كل جهاز يملك رقمه الخاص فلا يمكن أن يتصادم مع غيره، والوقت يعطي الترتيب، والعداد يفرق بين المعرفات داخل المللي ثانية الواحدة. والصورة التالية تبين ذلك مع الطرق المشابهة في شركات أخرى:

![مخطط معرف Snowflake من تويتر 2010 بطول 64 بت: بت صفر، ثم 41 بت للوقت بالمللي ثانية منذ 2010-11-04 01:42:54.657 UTC، ثم 5 بت لمركز البيانات و5 للعامل، ثم 12 بت للعداد من 0 إلى 4095 في كل مللي ثانية، مع فك المعرف 2107230213409763330 من التجربة إلى الوقت 2026-10-05 22:03:27.978 UTC ومركز البيانات 1 والعامل 7 والعداد 2. وتحته تقسيمات مشابهة: Discord بوقت 42 بت من بداية 2015 ثم worker وprocess بخمس بتات لكل منهما ثم عداد 12 بت، وInstagram بوقت 41 بت ثم رقم الجزء Shard ID بطول 13 بت ثم عداد 10 بت، وSonyflake بوقت 39 بت بوحدات 10 مللي ثانية ثم عداد 8 بت ثم رقم الجهاز 16 بت](https://arabroot.io/content/images/2026/10/unique-ids-04-snowflake.webp)

Snowflake يجمع الوقت ورقم الجهاز وعداداً في رقم واحد من 64 بت

والمثال التالي `snowflake.mjs` يبني مولداً بسيطاً بتقسيم تويتر نفسه وبدايته الزمنية من الكود الأصلي، ثم يفكك المعرف، ويفكك أيضاً المثال المنشور في [توثيق Discord](https://docs.discord.com/developers/reference?ref=arabroot.io#snowflakes):

```javascript
// A minimal Snowflake generator with Twitter's 2010 layout:
// 1 unused bit | 41 bits ms since custom epoch | 5 datacenter | 5 worker | 12 sequence
const TWEPOCH = 1288834974657n;            // from IdWorker.scala (snowflake-2010)
function makeGenerator(datacenterId, workerId) {
    let lastMs = -1n, seq = 0n;
    return () => {
        let now = BigInt(Date.now());
        if (now === lastMs) {
            seq = (seq + 1n) & 4095n;
            if (seq === 0n) while ((now = BigInt(Date.now())) <= lastMs) {}   // 4096 used: wait for the next ms
        } else seq = 0n;
        lastMs = now;
        return ((now - TWEPOCH) << 22n) | (BigInt(datacenterId) << 17n) | (BigInt(workerId) << 12n) | seq;
    };
}
function decode(id, epoch, layout) {
    id = BigInt(id);
    const ms = (id >> 22n) + epoch;
    return { id: id.toString(), time: new Date(Number(ms)).toISOString(),
        [layout[0]]: Number((id >> 17n) & 31n), [layout[1]]: Number((id >> 12n) & 31n), sequence: Number(id & 4095n) };
}
const next = makeGenerator(1, 7);
const ids = [next(), next(), next()];
for (const id of ids) console.log('snowflake:', id.toString(), ' bits:', id.toString(2).length);
console.log(decode(ids[2], TWEPOCH, ['datacenter', 'worker']));

// Discord uses the same split with its own epoch (first second of 2015); this is the example from its docs
console.log(decode('175928847299117063', 1420070400000n, ['worker', 'process']));

// why APIs send Snowflakes as strings: JavaScript numbers are exact only up to 2^53 - 1
console.log('\nNumber.MAX_SAFE_INTEGER =', Number.MAX_SAFE_INTEGER);
console.log('as Number:', Number(ids[0]), ' as BigInt:', ids[0].toString());
```

```bash
docker run --rm --name lab-ids-node -v "$PWD":/lab -w /lab node:26-alpine node snowflake.mjs
```

والمخرج سوف يكون كما يلي:

```text
snowflake: 2107230213409763328  bits: 61
snowflake: 2107230213409763329  bits: 61
snowflake: 2107230213409763330  bits: 61
{
  id: '2107230213409763330',
  time: '2026-10-05T22:03:27.978Z',
  datacenter: 1,
  worker: 7,
  sequence: 2
}
{
  id: '175928847299117063',
  time: '2016-04-30T11:18:25.796Z',
  worker: 1,
  process: 0,
  sequence: 7
}

Number.MAX_SAFE_INTEGER = 9007199254740991
as Number: 2107230213409763300  as BigInt: 2107230213409763328
```

في المخرج أعلاه لاحظ التالي:

- المعرفات الثلاثة متتالية لأنها في المللي ثانية نفسها، فلم يتغير إلا العداد 0 ثم 1 ثم 2، وطولها 61 بتاً لأن الوقت منذ 2010 لم يملأ بعد كل البتات الـ 41.
- فك مثال Discord أعطى الوقت `2016-04-30T11:18:25.796Z`، وهو الوقت نفسه المكتوب في توثيقهم، مع أن Discord تستخدم بداية زمنية مختلفة هي أول ثانية من 2015 وتسمي حقلي الجهاز worker وprocess.
- السطر الأخير هو أشهر خطأ مع Snowflake: الرقم في JavaScript دقيق فقط حتى [2 مرفوعة للقوة 53 ناقص واحد](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global%5FObjects/Number/MAX%5FSAFE%5FINTEGER?ref=arabroot.io)، فعندما تحول المعرف إلى `Number` تفقد آخر خانتين ويصبح `...300` بدلاً من `...328`، أي معرفاً لسجل آخر. لذلك تقول Discord في توثيقها إنها ترسل المعرفات في ال API نصوصاً دائماً حتى لا يحدث فيض Overflow في بعض اللغات، وعليك أن تفعل الشيء نفسه في أي API ترسل فيه أرقام 64 بت.

والمولد أعلاه مختصر للشرح، أما الكود الأصلي من تويتر فيرفض أن ينشئ أي معرف إذا رجعت ساعة الجهاز إلى الخلف، ويرمي الخطأ «Clock moved backwards»، والسبب أن الساعة الراجعة قد تعيد وقتاً وعداداً استخدما من قبل فيتكرر المعرف. وهذه هي نقطة الضعف الأساسية في Snowflake: أنت تحتاج إلى ساعة مضبوطة بـ NTP، وتحتاج إلى نظام يوزع أرقام الأجهزة دون أن يعطي الرقم نفسه لجهازين، وهذا عمل إداري لا يحتاجه UUIDv7.

وقد أخذت شركات أخرى الفكرة نفسها بتقسيمات مختلفة، ومنها:

- **Discord:** 42 بتاً للوقت من بداية 2015، ثم 5 بتات worker و5 بتات process، ثم 12 بتاً للعداد، كما في [توثيقها](https://docs.discord.com/developers/reference?ref=arabroot.io#snowflakes).
- **Instagram:** كانت تنشئ المعرف داخل PostgreSQL نفسه بدالة PL/pgSQL، بتقسيم 41 بتاً للوقت ثم 13 بتاً لرقم الجزء Shard ID ثم 10 بتات لعداد مأخوذ من Sequence بعد قسمته على 1024، فيعرف التطبيق من المعرف نفسه أي جزء من قاعدة البيانات يحمل السجل، كما في [صفحة Snowflake ID في Wikipedia](https://en.wikipedia.org/wiki/Snowflake%5FID?ref=arabroot.io).
- [**Sonyflake**](https://github.com/sony/sonyflake?ref=arabroot.io)**:** مكتبة Go من Sony، تجعل الوقت 39 بتاً بوحدات 10 مللي ثانية فيكفي قرابة 174 سنة، ثم 8 بتات للعداد و16 بتاً لرقم الجهاز، أي أنها تقبل أجهزة أكثر ومعرفات أقل في كل وحدة زمن، والإصدار الثاني منها يجعل هذه الأطوال قابلة للتعديل.

## Sqids: إخفاء الرقم المتسلسل دون تغييره

أحياناً يكون عندك نظام قائم كل مفاتيحه أرقام متسلسلة، ولا تريد أن تغير قاعدة البيانات، ولكنك لا تريد أن يظهر الرقم 1041 في الرابط. وهنا تأتي مكتبة [Sqids](https://sqids.org/?ref=arabroot.io) التي كان اسمها سابقاً Hashids، وهي تحول الرقم إلى نص قصير ثم تعيده رقماً، كما رأيت في المخرج السابق حيث أصبح الرقم 1 هو `UkLWZg9D` والرقم 1000 هو `pndklVeM` ثم رجع 1000 عند فك الترميز. والمكتبة تمنع ظهور كلمات بذيئة في الناتج، وتسمح لك بتحديد أبجدية خاصة وطول أدنى.

⚠️

Sqids ليس تشفيراً، والمشروع نفسه يقول في [صفحة الأسئلة الشائعة](https://sqids.org/faq?ref=arabroot.io) إن المعرفات ليست هاش ويمكن فكها إلى الأرقام، وإنه أزال الخيار الذي كان يسمى Salt لأنه كان يوحي بحماية غير موجودة، وينصح بعدم استخدامه لمعرفات المستخدمين لأنه قد يكشف عددهم إذا عرف أحد الأبجدية. لذلك استخدمه لتجميل الروابط فقط، ولا تعتمد عليه لإخفاء حجم عملك أو لمنع الوصول.

## مقارنة الأطوال والترميزات

كل معرف من هذه المعرفات هو في النهاية مجموعة بتات، والفرق في عددها وفي طريقة كتابتها نصاً، وهذا يحدد طول الرابط وحجم الفهرس. والصورة التالية تبين كل نوع بمثال حقيقي من التجربة:

![جدول مرئي يقارن طول المعرفات نصاً وحجمها وترميزها: bigint مثل 1041 بثمانية بايتات وترميز عشري، وSnowflake مثل 2107230213409763330 بتسعة عشر رقماً وثمانية بايتات، وSqids مثل UkLWZg9D بثمانية حروف من رقم صحيح بأبجدية خاصة، وNanoID مثل Uv1AY8wikMaVvXwP42H8D بواحد وعشرين حرفاً و126 بت عشوائية بالأبجدية A-Za-z0-9_-، وULID مثل 01M471GYFFE12DN1S15DEVK9VS بستة وعشرين حرفاً و16 بايتاً بترميز Crockford Base32، وKSUID مثل 0ujtsYcgvSTl8PAuAdqWYSMnLOv بسبعة وعشرين حرفاً و20 بايتاً بترميز Base62، وUUID نصاً بستة وثلاثين حرفاً و16 بايتاً في النوع uuid بترميز Hex مع أربع شرطات](https://arabroot.io/content/images/2026/10/unique-ids-05-lengths.webp)

كم حرفاً يأخذ كل معرف في الرابط، وكم بايتاً في قاعدة البيانات

والجدول التالي يجمع الخصائص التي تهمك عند الاختيار:

| المعرف          | الحجم                      | النص                      | مرتب بالوقت   | يحتاج تنسيقاً بين الأجهزة | يكشف                         |
| --------------- | -------------------------- | ------------------------- | ------------- | ------------------------- | ---------------------------- |
| bigint identity | 8 بايتات                   | حتى 19 رقماً              | نعم           | نعم، قاعدة بيانات واحدة   | العدد والترتيب               |
| UUIDv4          | 16 بايتاً                  | 36 حرفاً Hex              | لا            | لا                        | لا شيء                       |
| UUIDv7          | 16 بايتاً                  | 36 حرفاً Hex              | نعم           | لا                        | وقت الإنشاء                  |
| ULID            | 16 بايتاً                  | 26 حرفاً Crockford Base32 | نعم           | لا                        | وقت الإنشاء                  |
| KSUID           | 20 بايتاً                  | 27 حرفاً Base62           | نعم، بالثانية | لا                        | وقت الإنشاء                  |
| NanoID          | 126 بتاً عشوائية افتراضياً | 21 حرفاً                  | لا            | لا                        | لا شيء                       |
| Snowflake       | 8 بايتات                   | حتى 19 رقماً              | نعم           | نعم، رقم لكل جهاز         | الوقت ورقم الجهاز            |
| Sqids           | حسب الرقم الأصلي           | قصير ويحدد طوله الأدنى    | لا            | حسب الرقم الأصلي          | الرقم نفسه لمن يعرف الأبجدية |

## أي معرف تختار؟

في أغلب الحالات يكفيك سؤالان: هل سوف يرى المستخدم هذا المعرف؟ وهل تنشئه قاعدة بيانات واحدة؟ والصورة التالية تبين طريق الاختيار، ثم الجدول بعدها يفصل الحالات الشائعة:

![مخطط قرار: السؤال الأول هل سيرى المستخدمون المعرف في رابط أو API أو QR. إذا كانت الإجابة لا فالسؤال هل تنشئ قاعدة بيانات واحدة كل المعرفات، فإن كانت نعم فالجواب bigint identity البسيط الصغير المرتب، وإن كانت لا فالجواب UUIDv7 للكتاب المتعددين والعمل دون اتصال والدمج. وإذا كانت الإجابة نعم فالسؤال هل يجب أن يكون قصيراً أو يكتبه الناس، فإن كانت لا فالجواب UUIDv7 أو v4 إذا كان يجب إخفاء الوقت، وإن كانت نعم فالجواب NanoID عشوائي مع فهرس فريد. وفي الأسفل حالات خاصة: UUIDv5 عندما يعطي الاسم نفسه المعرف نفسه للاستيراد ومنع التكرار، وSnowflake عندما تحتاج 64 بت مرتبة على نطاق ضخم مع أرقام للأجهزة، وSqids لإخفاء رقم داخلي وهو ليس حماية أمنية، وbigint داخلي مع عمود UUID عام](https://arabroot.io/content/images/2026/10/unique-ids-06-decision.webp)

سؤالان يحددان نوع المعرف في أغلب الحالات

| الحالة                                  | المعرف المناسب                                 | والسبب                                                               |
| --------------------------------------- | ---------------------------------------------- | -------------------------------------------------------------------- |
| مفتاح أساسي داخلي في قاعدة بيانات واحدة | bigint بعمود Identity                          | الأصغر والأسرع والأسهل في القراءة، ولا يخرج من النظام                |
| معرف يظهر في الرابط أو ال API           | UUIDv7، أو v4 إذا كان وقت الإنشاء سراً         | لا يمكن تخمين ما بعده، ولا يكشف عدد السجلات                          |
| نظام قائم بأرقام متسلسلة تريد أن تخفيها | عمود UUID إضافي للروابط مع بقاء bigint داخلياً | لا تغير المفاتيح والعلاقات، وتتوقف عن نشر الرقم الداخلي              |
| أكثر من سيرفر أو خدمة تكتب              | UUIDv7                                         | كل جهاز ينشئ معرفه وحده دون تنسيق، ويبقى الفهرس مرتباً               |
| تطبيق جوال أو كلاينت يعمل دون اتصال     | UUIDv7 أو v4 ينشئه الكلاينت                    | يعرف الكلاينت المعرف قبل الإرسال، فلا تتكرر السجلات إذا أعاد الإرسال |
| السجلات والأحداث Logs وEvents           | UUIDv7 أو ULID                                 | الترتيب بالوقت يأتي مع المعرف، وتستطيع أن تقرأ الوقت منه             |
| الروابط القصيرة ورموز الدعوة            | NanoID بطول وأبجدية تناسب الحجم، مع قيد تفرد   | قصير وسهل في الكتابة، والقيد يرفض التكرار النادر                     |
| استيراد من نظام خارجي أكثر من مرة       | UUIDv5 من المفتاح الخارجي                      | المدخل نفسه يعطي المعرف نفسه، فلا تتكرر السجلات                      |
| حجم ضخم مع حاجة إلى 64 بت فقط           | Snowflake أو أحد أشكاله                        | نصف حجم ال UUID، ولكن مع إدارة أرقام الأجهزة وضبط الساعة             |

## ملاحظات أمنية

المعرف الذي لا يمكن تخمينه يجعل استغلال الأخطاء أصعب، ولكنه لا يمنعها، وفيما يلي القواعد التي لا يجب أن تنساها:

- **المعرف ليس صلاحية:** المواصفة RFC 9562 تقول في [قسم الاعتبارات الأمنية](https://www.rfc-editor.org/rfc/rfc9562.html?ref=arabroot.io#section-8) إنه لا ينبغي أن تفترض أن ال UUID صعب التخمين، وإنه لا يجب أن يستخدم أداة أمنية Security Capability يكفي امتلاكها للوصول. والمعرف يظهر في سجلات ال Reverse Proxy وفي سجل المتصفح وفي الروابط التي يتشاركها الناس، لذلك تحقق في كل طلب من أن المستخدم يملك صلاحية الوصول إلى هذا السجل، وقد شرحنا كيف يعرف السيرفر من هو المستخدم في دليل [الجلسات وملفات تعريف الارتباط وJWT](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)، وكيف تبني الصلاحيات في دليل [إدارة الهوية والوصول IAM](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/).
- **التوكنات شيء آخر:** رابط إعادة تعيين كلمة المرور أو رابط المشاركة السري أو معرف الجلسة Session ID هي أسرار وليست معرفات، فأنشئها من مولد عشوائي آمن بطول كاف، واحفظ الهاش الخاص بها، واجعل لها مدة انتهاء، ولا تقم باستخدام UUIDv7 لها لأن نصفه وقت معروف.
- **v1 يكشف الجهاز:** كما رأيت في التجربة، آخر 12 خانة هي عنوان MAC، لذلك لا تقم باستخدامه في أي معرف يخرج من نظامك.
- **v7 وULID وSnowflake تكشف الوقت:** كل من يرى المعرف يعرف متى أنشئ السجل بالمللي ثانية، وهذا لا يهم في أغلب الحالات، ولكنه يهم في معرف حساب مستخدم إذا كان تاريخ تسجيله معلومة حساسة، فاستخدم v4 هناك.
- **العشوائية الآمنة:** استخدم مولدات النظام مثل `crypto.randomUUID()` في Node.js والمتصفح و`uuid.uuid4()` في Python و`uuidv4()` في PostgreSQL، ولا تقم بكتابة مولد UUID بنفسك من `Math.random()`، والسبب أن هذه الدالة [ليست آمنة تشفيرياً](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/randomUUID?ref=arabroot.io) وقد تتكرر قيمها أو يمكن توقعها.
- **تحقق من المدخلات:** عندما يصلك معرف من المستخدم فتحقق من صيغته قبل أن تمرره إلى قاعدة البيانات، فعمود `uuid` في PostgreSQL يرفض أي نص ليس UUID صالحاً، وهذه حماية مجانية لا تحصل عليها إذا حفظته نصاً.

## الخلاصة

وصلنا لنهاية الموضوع، وأهم ما فيه:

- الرقم المتسلسل `bigint` ما زال الأفضل للمفاتيح الداخلية في قاعدة بيانات واحدة، ومشكلته تبدأ عندما يظهر في الرابط فيصبح سهل التخمين ويكشف حجم عملك، أو عندما يكتب أكثر من طرف فتتكرر الأرقام.
- ال UUID رقم من 128 بتاً تحدده الآن RFC 9562 الصادرة في مايو 2024، وفيه حقل للإصدار في الخانة 13 وحقل للنوع في الخانة 17، والإصداران العمليان اليوم هما v4 العشوائي وv7 المرتب بالوقت، ومعهما v5 للمعرف المحسوب من اسم.
- احتمال تكرار UUIDv4 يصل إلى 50% بعد قرابة 2.7 مليار مليار معرف، بشرط أن يأتي من مولد عشوائي آمن، وقيد التفرد في قاعدة البيانات يبقى شبكة الأمان.
- UUIDv4 يتوزع على كل صفحات الفهرس فيكبره ويبطئ الإدراج، وفي تجربتنا على PostgreSQL 18 كان فهرس v4 أكبر بقرابة الربع من v7 وصفحاته ممتلئة بنسبة 71% مقابل 90%، والنص أسوأ من الاثنين، لذلك احفظ ال UUID دائماً في النوع `uuid`.
- ULID وKSUID سبقا UUIDv7 بالفكرة نفسها بترميز أقصر، وNanoID للمعرفات القصيرة العشوائية، وSnowflake للمعرفات المرتبة بحجم 64 بت مع ثمن إداري هو أرقام الأجهزة وضبط الساعة، وأرسله في ال API نصاً.
- Sqids يجمل الروابط ولا يحمي شيئاً، وأي معرف مهما كان عشوائياً ليس بديلاً عن التحقق من الصلاحية في كل طلب.

[![](https://arabroot.io/content/images/2026/10/db-design-cover-cover.webp)دليلتصميم قواعد البيانات: التطبيع Normalization ومتى تكسرهلماذا يفشل الجدول الواحد الكبير، وكيف تقسمه إلى جداول لا تناقض نفسها، وما الذي يتغير عندما تكبر البيانات.](https://arabroot.io/articles/%D8%AA%D8%B5%D9%85%D9%8A%D9%85-%D9%82%D9%88%D8%A7%D8%B9%D8%AF-%D8%A7%D9%84%D8%A8%D9%8A%D8%A7%D9%86%D8%A7%D8%AA-%D8%A7%D9%84%D8%AA%D8%B7%D8%A8%D9%8A%D8%B9-%D9%88%D8%A7%D9%84%D8%AA%D9%88%D8%B3%D8%B9/)[![](https://arabroot.io/content/images/2026/10/encodings-cover-cover.webp)دليلالترميز Encoding للمطورين: Base64 وأخواتهلماذا نحول البايتات إلى حروف حتى تمر في البريد والروابط، وكيف تفك أي سلسلة منها، ولماذا لا يحمي Base64 أي سر.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%B1%D9%85%D9%8A%D8%B2-base64-%D9%88-base32-%D9%88-base58/)[![](https://arabroot.io/content/images/2026/10/sessions-cookies-jwt-cover-cover.webp)دليلكيف يتذكرك الموقع بعد تسجيل الدخول؟ الجلسات وJWTبروتوكول HTTP لا يتذكر أحداً، فكيف يبقى المستخدم مسجلاً بين الصفحات، ومتى تختار الجلسة على السيرفر ومتى تختار JWT.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AC%D9%84%D8%B3%D8%A7%D8%AA-%D9%88%D9%85%D9%84%D9%81%D8%A7%D8%AA-%D8%AA%D8%B9%D8%B1%D9%8A%D9%81-%D8%A7%D9%84%D8%A7%D8%B1%D8%AA%D8%A8%D8%A7%D8%B7-%D9%88-jwt-%D9%84%D9%84%D9%85%D8%A8%D8%AA%D8%AF%D8%A6%D9%8A%D9%86/)[![](https://arabroot.io/content/images/2026/10/iam-pam-cover-cover.webp)دليلإدارة الهوية والوصول IAM والوصول المميز PAMمن يملك صلاحية root على سيرفراتك ومنذ متى، وكيف تضبط حسابات المديرين بصلاحيات مؤقتة وجلسات مسجلة.](https://arabroot.io/articles/%D8%A7%D8%AF%D8%A7%D8%B1%D8%A9-%D8%A7%D9%84%D9%87%D9%88%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-iam-%D9%88%D8%A7%D9%84%D9%88%D8%B5%D9%88%D9%84-%D8%A7%D9%84%D9%85%D9%85%D9%8A%D8%B2-pam/)[![](https://arabroot.io/content/images/2026/10/postgres-large-tables-cover-cover.webp)دليلPostgreSQL للجداول الضخمة: عشرات الملايين من الصفوفماذا تفعل عندما يصل جدول الطلبات إلى عشرات الملايين من الصفوف وتبطئ لوحة التحكم ويمتلئ القرص، وكيف تقيس قبل أن تصلح.](https://arabroot.io/articles/postgresql-%D9%84%D9%84%D8%AC%D8%AF%D8%A7%D9%88%D9%84-%D8%A7%D9%84%D8%B6%D8%AE%D9%85%D8%A9/)[![](https://arabroot.io/content/images/2026/10/date-time-cover-cover.webp)دليلالتاريخ والوقت للمطورين ومديري الأنظمةلماذا يتأخر التقرير ثلاث ساعات ولا تعمل المهمة المجدولة ليلة تغيير التوقيت، وكيف تضبط الوقت في السيرفر والكود.](https://arabroot.io/articles/%D8%A7%D9%84%D8%AA%D8%A7%D8%B1%D9%8A%D8%AE-%D9%88%D8%A7%D9%84%D9%88%D9%82%D8%AA-utc-%D9%88%D8%A7%D9%84%D9%85%D9%86%D8%A7%D8%B7%D9%82-%D8%A7%D9%84%D8%B2%D9%85%D9%86%D9%8A%D8%A9-%D9%88%D8%A7%D9%84%D8%AA%D9%82%D9%88%D9%8A%D9%85-%D8%A7%D9%84%D9%87%D8%AC%D8%B1%D9%8A/)

## سجل التحديثات

- أكتوبر 2026: كتابة المقال، وتنفيذ التجارب على Node.js 26.10.0 وPython 3.14.8 وPostgreSQL 18.6 مع المكتبات ulid 3.0.2 وnanoid 6.0.2 وsqids 0.3.0.