UUID v1·v4·v7 तुलना और DB प्राइमरी-की डिज़ाइन
सारांश (TL;DR)
यह कहने से थोड़ी बहस होगी, पर मैं कहता हूँ — उच्च-वॉल्यूम इंसर्ट टेबल की प्राइमरी की के तौर पर v4 UUID चुनना ख़ुद अपने पैर पर कुल्हाड़ी मारने जैसा है। मैंने एक PostgreSQL 16 वर्कलोड देखा जिसका events टेबल प्राइमरी की uuid v4 (gen_random_uuid()) पर था। हर INSERT B-tree के रैंडम लीफ़ पेज पर गिरती थी, shared_buffers ठंडा कर देती थी, और इंडेक्स फ़्रैग्मेंटेशन एक अंकों की बड़ी प्रतिशत में जमा हो रहा था। कॉलम टाइप v7 करते ही — वही 16 बाइट uuid टाइप, वही इंडेक्स — औसत INSERT लेटेंसी लगभग एक-तिहाई पर आ गई और pg_stat_user_indexes के फ़्रैग्मेंटेशन मैट्रिक्स स्थिर हो गए। UUID वर्ज़न चुनना “क्रिप्टोग्राफ़ी का निर्णय” नहीं, डेटाबेस डिज़ाइन का निर्णय है। UUID 128-बिट मान है, 8-4-4-4-12 हेक्स में लिखा जाता है, जहाँ 4 बिट वर्ज़न को और 2–3 बिट variant को नियत हैं। v1 समय और node identifier (ऐतिहासिक रूप से MAC पता) एम्बेड करता है, लगभग समय-क्रमबद्ध होता है पर होस्ट उजागर करता है। v4 122 बिट रैंडमनेस से अप्रत्याशितता लाता है पर सॉर्ट नहीं होता। v7 RFC 9562 (2024) में मानकीकृत है, ऊपर 48 बिट Unix मिलीसेकंड टाइमस्टैम्प रखता है और बाक़ी रैंडम भरता है — v4 की सुरक्षा और v1 की इंडेक्स स्थानीयता का संयोजन। पब्लिक API ID जैसी जगह जहाँ “कब बना” छुपाना वाक़ई मायने रखता है, वहाँ v4 अब भी डिफ़ॉल्ट है। बाक़ी लगभग सब जगह v7 बेहतर विकल्प है, और v1 लेगेसी है।
पृष्ठभूमि
UUID 128 बिट है। पारंपरिक रूप से हाइफ़न-विभाजित 32 हेक्स अंकों के रूप में लिखा जाता है: xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx। M स्थान पर वर्ज़न (1, 4, 7 आदि) है, और N के ऊपरी बिट variant को दर्शाते हैं। बाक़ी वर्ज़न-विशेष अर्थ रखते हैं।
वर्ज़न 1 मशीन और समय में अद्वितीयता के लिए डिज़ाइन हुआ था। 60-बिट टाइमस्टैम्प 15 अक्टूबर 1582 से 100ns अंतराल गिनता है, clock sequence फ़ील्ड clock rewinding संभालता है, और 48-बिट node ID मूलतः नेटवर्क MAC पता था। गोपनीयता पर असर सीधा है: लैपटॉप पर जारी v1 UUID में उस लैपटॉप का MAC एन्कोडेड है, और uuid CLI की एक पंक्ति उसे निकाल लेती है।
वर्ज़न 4 122 बिट रैंडमनेस है, जिसमें वर्ज़न·variant के लिए 6 बिट स्थिर। मज़बूत RNG माना जाता है (ब्राउज़र का crypto.randomUUID() या PostgreSQL का gen_random_uuid())। असली पैमाने पर टकराव व्यावहारिक रूप से असंभव हैं — birthday paradox सीमा से 1 अरब जनरेट करने पर टकराव प्रायिकता लगभग एक ट्रिलियन में एक। नुक़सान यह है कि लगातार जारी दो v4 परस्पर असंबद्ध हैं, इसलिए इंडेक्स इंसर्ट पर रैंडम पेज छूते हैं — कैश स्थानीयता और राइट एम्प्लिफ़िकेशन (WAL, full page writes) दोनों के लिए ख़राब।
वर्ज़न 7 2024 में RFC 9562 का हिस्सा बना (यह RFC 4122 को अप्रचलित कर v6·v7·v8 जोड़ता है)। v7 ऊपरी 48 बिट Unix मिलीसेकंड टाइमस्टैम्प, फिर वर्ज़न टैग, छोटा rand_a, variant टैग, और 62-बिट rand_b पूँछ रखता है। व्यावहारिक प्रभाव — एक ही मिलीसेकंड में जारी v7 सॉर्ट में निकट हैं, और मिलीसेकंड्स के बीच समय-क्रम में आते हैं। PostgreSQL 18 में नेटिव uuidv7() आ चुका है; पुराने वर्ज़न pg_uuidv7 एक्सटेंशन या एप्लिकेशन स्तर लाइब्रेरी (Node uuid 9.x, Python uuid6) से वही परिणाम देते हैं।
स्टोरेज फ़ॉर्मैट अलग मसला है। PostgreSQL 16 का नेटिव uuid टाइप मान 16 बाइट में रखता है। MySQL अक्सर BINARY(16) या CHAR(36) इस्तेमाल करता है; बाद वाला स्टोरेज दुगना करता है और तुलना अक्षर-अक्षर बनाता है।
तुलना और डेटा
| गुण | v1 | v4 | v7 |
|---|---|---|---|
| जनरेशन इनपुट | टाइमस्टैम्प + clock sequence + node ID | 122 बिट रैंडम | 48-बिट Unix ms + रैंडम पूँछ |
| गोपनीयता | node ID (अक्सर MAC) उजागर | कोई होस्ट/समय जानकारी नहीं | जनरेशन समय (ms) दिखता है, होस्ट नहीं |
| समय-क्रम सॉर्ट | संभव (बाइट क्रम ≠ समय क्रम, पुनर्व्यवस्था चाहिए) | नहीं | हाँ — lexicographic क्रम = समय-क्रम |
| इंडेक्स स्थानीयता | मध्यम | ख़राब (B-tree में रैंडम इंसर्ट) | अच्छी (लगभग monotonic) |
| आम उपयोग | लेगेसी सिस्टम, कुछ COM/Windows ID | पब्लिक API ID, सेशन टोकन, salts | ईवेंट लॉग, उच्च-वॉल्यूम इंसर्ट, समय पेजिनेशन |
| एंट्रॉपी | कम (ज़्यादातर समय·node) | उच्च (लगभग 122 बिट) | पूँछ उच्च (लगभग 74 बिट), ms के अंदर टकराव दुर्लभ |
मोटा मानसिक मॉडल: v4 अप्रत्याशितता अधिकतम करता है और इंडेक्स प्रदर्शन बलिदान करता है; v7 अधिकांश अनुप्रयोगों में पर्याप्त अप्रत्याशितता बनाए रखते हुए “अंत में इंसर्ट” पैटर्न बहाल करता है जो DB को पसंद है; v1 ऐतिहासिक विरासत है।
वास्तविक परिदृश्य
परिदृश्य 1 — अप्पेंड-केंद्रित ईवेंट लॉग। शुरुआत वाला वर्कलोड बिल्कुल ऐसा था। रोज़ लाखों पंक्तियाँ आती हैं और अक्सर “पिछले 24 घंटे, समय-क्रम में” पूछी जाती हैं — v7 सीधा फ़ायदा है। नई पंक्तियाँ इंडेक्स के अंत में जुड़ती हैं, हॉट पेज गर्म रहते हैं, और समय रेंज क्वेरी निकट इंडेक्स सेगमेंट पर मैप होती हैं।
परिदृश्य 2 — पब्लिक यूज़र-फ़ेसिंग ID। /orders/{id} जैसे शेयर लिंक को दूसरे यूज़र के ऑर्डर enumerate होने से बचाने के लिए अप्रत्याशित होना चाहिए। v4 सुरक्षित डिफ़ॉल्ट है। v7 के फ़ायदे भी चाहिए, तो याद रखें कि v7 मिलीसेकंड-स्तरीय जनरेशन समय उजागर करता है। ऑर्डर के लिए यह स्वीकार्य हो सकता है, लेकिन संवेदनशील संदर्भ (प्रति-मिनट पेमेंट्स जैसे बिज़नेस सिग्नल) में यह लीक है। एक दोहरी ID पैटर्न यह है कि आंतरिक प्राइमरी की पर v7 रखें और बाहर अलग v4 या छोटा random slug दिखाएँ।
परिदृश्य 3 — मल्टी-रीजन·शार्डेड सिस्टम। v7 का टाइमस्टैम्प प्रिफ़िक्स अलग-अलग रीजन द्वारा एक ही ms में जारी UUID को समय-क्रम में मिलाने में मदद करता है, लेकिन एक ही ms के अंदर रीजन-पार क्रम की गारंटी नहीं। सख़्त क्रम चाहिए तो ULID (48-बिट टाइमस्टैम्प + 80-बिट रैंडम Crockford Base32 में) लगभग वही गुण छोटे 26-अक्षर टेक्स्ट में देता है। उससे भी सख़्त गारंटी चाहिए तो Snowflake-स्टाइल ID (स्पष्ट machine ID के साथ 64-बिट) — पर उसके लिए machine-ID आवंटन का समन्वय ज़रूरी है।
आम ग़लतफ़हमियाँ
“UUID DB में हमेशा धीमा है।” 4-बाइट int से धीमा ज़रूर है, पर बड़ी लागत B-tree इंडेक्स के रैंडम इंसर्ट फ़्रैग्मेंटेशन की है, जिसे v7 लगभग ख़त्म कर देता है। 36-अक्षर स्ट्रिंग की बजाय 16 बाइट में स्टोर करने से इंडेक्स आधा हो जाता है और तुलना तेज़।
“केवल v4 सुरक्षित है।” v7 की रैंडम पूँछ भी अधिकांश अनुप्रयोगों के लिए पर्याप्त एंट्रॉपी पूल है — सेशन संदर्भ, API ID — हमलावर के लिए enumerate करना अव्यावहारिक है। असली मुद्दा अप्रत्याशितता नहीं, टाइमस्टैम्प लीक है।
“UUID स्ट्रिंग के रूप में स्टोर करें।” स्ट्रिंग रूप 36 अक्षर है, पर बाइनरी 16 बाइट है और बाइट सॉर्ट सही सॉर्ट बन जाता है।
“v1 का MAC लीक कोई नहीं देखता।” v1 से MAC निकालना सार्वजनिक रूपांतरण है, और फ़ॉरेंसिक टूल (uuid -d आदि) डिफ़ॉल्ट रूप से यह करते हैं। अगर UUID URL, सपोर्ट टिकट या बाहरी लॉग में दिखे, तो यह असली जानकारी लीक है।
चेकलिस्ट
- बार-बार इंसर्ट होने वाले इंडेक्स की key? डिफ़ॉल्ट v7। केवल तब v4 जब जनरेशन समय की अप्रत्याशितता वाक़ई ज़रूरी हो।
- क्या UUID यूज़र·पार्टनर को दिखेगा? दोनों चल सकते हैं। बस v7 के टाइमस्टैम्प लीक की स्वीकार्यता तय करें।
- Postgres है? नेटिव
uuid(16 बाइट) स्टोर करें। MySQL में स्ट्रिंग संगतता आवश्यक न हो तोBINARY(16)। - कई जनरेटर्स के बीच क्रम की गारंटी? अकेला v7 काफ़ी नहीं। ULID या स्पष्ट machine ID वाला Snowflake।
- कोड में v1 बचा है? MAC लीक दस्तावेज़ करें, स्कीमा की अनुमति आने पर माइग्रेशन प्लान।
- क्लाइंट जनरेशन? क्रिप्टोग्राफ़िक RNG वाली लाइब्रेरी इस्तेमाल करें।
संबंधित टूल
Patrache Studio का UUID जनरेटर v4 और v7 लोकल बनाता है, इसलिए मान थर्ड-पार्टी सर्विस लॉग में नहीं जाते। UUID लगभग हमेशा JSON पेलोड में जाते हैं — JSON फ़ॉर्मैटिंग·वैलिडेशन·JSON Schema में उन्हें सेवाओं के बीच भी टाइप-सुरक्षित रखने के स्कीमा पैटर्न हैं। 16-बाइट UUID से व्युत्पन्न 22-अक्षर छोटे ID जैसे संकुचित टेक्स्ट रूप चाहिए तो Base64 और URL एन्कोडिंग बताता है मानक Base64 नहीं, Base64URL क्यों चाहिए।
संदर्भ
- IETF RFC 9562, “Universally Unique IDentifiers (UUIDs)” — https://datatracker.ietf.org/doc/html/rfc9562
- IETF RFC 4122, “A Universally Unique IDentifier (UUID) URN Namespace” (obsoleted by RFC 9562) — https://datatracker.ietf.org/doc/html/rfc4122
- PostgreSQL documentation, “UUID Type” — https://www.postgresql.org/docs/current/datatype-uuid.html
- ULID specification — https://github.com/ulid/spec