प्रतिनिधि इंटरव्यू विषय

SQL इंटरव्यू: डुप्लिकेट पंक्तियों को सुरक्षित रूप से हटाएं और नवीनतम रिकॉर्ड बनाए रखें

डेटाकठिन
Offer.cc संपादकीय टीमप्रकाशित अपडेट किया गया

प्रश्न

एक PostgreSQL customer_contacts टेबल में 200 मिलियन पंक्तियाँ और डुप्लिकेट बिजनेस कीज़ हैं। प्रत्येक (tenant_id, external_id) के लिए नवीनतम पंक्ति को सुरक्षित रूप से बनाए रखें, संदर्भित इवेंट्स को सुरक्षित रखें, लूज़र्स को बैचों में डिलीट करें, सुधार को मान्य करें और नए डुप्लिकेट को रोकें। आप इसे कैसे डिज़ाइन और निष्पादित करेंगे?

प्रॉम्प्ट और लागू संदर्भ

एक PostgreSQL customer_contacts टेबल में 200 मिलियन पंक्तियाँ हैं। एक पुराने इंपोर्टर में पुनः प्रयासों (retries) ने समान (tenant_id, external_id) के लिए कई पंक्तियाँ बना दीं। updated_at के आधार पर नवीनतम पंक्ति बची रहनी चाहिए; यदि टाइमस्टैम्प समान (tie) हैं, तो सबसे बड़ा id विजेता होगा। एक contact_events.contact_id फॉरेन की (foreign key) किसी भी कॉपी की ओर इंगित कर सकती है, इसलिए संदर्भों को रीपॉइंट करने से पहले लूज़र्स को डिलीट करने से विफलता होगी या संबंध समाप्त हो जाएगा।

sql
customer_contacts(
    id bigint primary key,
    tenant_id bigint not null,
    external_id text not null,
    updated_at timestamptz not null,
    payload jsonb not null
)

contact_events(
    id bigint primary key,
    contact_id bigint not null references customer_contacts(id),
    event_type text not null,
    created_at timestamptz not null
)

एक PostgreSQL सुधार डिज़ाइन करें जो प्रति बिजनेस की एक डिटर्मिनिस्टिक कैनोनिकल कॉन्टैक्ट बनाए रखता है, हर इवेंट को सुरक्षित रखता है, विनाशकारी (destructive) कार्य को सीमित लेन-देन (bounded transactions) में संसाधित करता है, जिसे ऑडिट और पुनः प्रयास किया जा सकता है, और जो डेटाबेस द्वारा लागू विशिष्टता के साथ समाप्त होता है। रीड्स (reads) उपलब्ध रहने चाहिए। एक नियंत्रित अंतिम राइट पॉज़ (write pause) की अनुमति है, लेकिन इसकी अवधि को अनुमान लगाने के बजाय मापा जाना चाहिए।

200 मिलियन पंक्तियाँ और अंतिम राइट पॉज़ इंटरव्यू की बाधाएँ हैं। यह एक कठिन डेटा-इंजीनियरिंग और SQL प्रश्न है क्योंकि विंडो फ़ंक्शन केवल चयन तंत्र है; वास्तविक उत्तर को रेफरेंशियल इंटीग्रिटी, कन्करेंसी, रोलबैक, स्टोरेज हेल्थ और भविष्य के राइट्स की भी रक्षा करनी चाहिए। वर्तमान सार्वजनिक SQL इंटरव्यू सामग्री स्पष्ट इंटरव्यू पैटर्न के रूप में ROW_NUMBER डिडुप्लिकेशन और डिटर्मिनिस्टिक टाई-ब्रेकिंग का उपयोग करना जारी रखती है। कोई सत्यापन योग्य कंपनी एट्रिब्यूशन उपलब्ध नहीं है, इसलिए companyName शून्य (null) है।

इंटरव्यूअर क्या मूल्यांकन करता है

पहला संकेत यह है कि क्या उम्मीदवार DELETE लिखने से पहले "डुप्लिकेट" और "नवीनतम" को परिभाषित करता है। बिजनेस की (tenant_id, external_id) है, जबकि id एक भौतिक पंक्ति की पहचान करता है। कुल क्रम (total order) updated_at DESC, id DESC बिल्कुल एक पंक्ति को विजेता बनाता है, तब भी जब टाइमस्टैम्प समान हों। RANK कई टाई वाली पंक्तियों को बनाए रख सकता है; ROW_NUMBER बिल्कुल एक स्थिति 1 निर्दिष्ट करता है।

दूसरा संकेत विनाशकारी सीमा का नियंत्रण है। एक प्रोडक्शन उत्तर काउंट्स और नमूनों का पूर्वावलोकन करता है, एक अपरिवर्तनीय (immutable) लूज़र-टू-विनर मैप को फ्रीज करता है, हारने वाली पंक्तियों या एक पुनर्स्थापन योग्य स्नैपशॉट को सुरक्षित रखता है, और प्राइमरी की द्वारा डिलीट करता है। प्रत्येक बैच के दौरान रैंकिंग की स्वतंत्र रूप से पुनर्गणना करने से राइट्स जारी रहने के दौरान लक्षित सेट बदल सकता है और रन को समझाना कठिन हो जाता है।

तीसरा संकेत रेफरेंशियल और ट्रांजैक्शनल शुद्धता है। इसके पैरेंट के डिलीट होने से पहले प्रत्येक चाइल्ड संदर्भ को लूज़र से विनर पर ले जाया जाना चाहिए। चाइल्ड-टेबल विशिष्टता नियम उस अपडेट को टकराने (collide) का कारण बना सकते हैं, इसलिए इन्वेंट्री और संघर्ष नीति (conflict policy) के बिना "सभी फॉरेन कीज़ को अपडेट करें" अधूरा है। प्रत्येक बैच इडेम्पोटेंट (idempotent), छोटा, अवलोकन योग्य और रोकने के लिए सुरक्षित होना चाहिए।

अंतिम संकेत यह है कि क्या क्लीनअप मूल कारण को समाप्त करता है। एक क्वेरी जो आज के डुप्लिकेट्स को हटाती है, वह कल के इंपोर्टर को उन्हें फिर से बनाने के लिए स्वतंत्र छोड़ देती है। स्थायी इनवेरिएंट एक यूनीक कंस्ट्रेंट या यूनीक इंडेक्स में होता है, जिसमें एक स्पष्ट शून्य (null) और सामान्यीकरण अनुबंध (normalization contract) होता है। उम्मीदवार को शुद्धता के प्रमाण और प्रदर्शन के प्रमाण के बीच भी अंतर करना चाहिए: शून्य डुप्लिकेट कीज़ और शून्य अनाथ संदर्भ (orphan references) सेमेंटिक्स को साबित करते हैं; EXPLAIN, लॉक प्रतीक्षाएं, WAL दर, प्रतिकृति अंतराल (replication lag), डेड टुपल्स और ऑटोवैक्यूम व्यवहार एक स्वीकार्य ऑपरेटिंग दर निर्धारित करते हैं।

उत्तर देने से पहले स्पष्ट करने योग्य प्रश्न

  • कौन से कॉलम बिजनेस एंटिटी को परिभाषित करते हैं? यहाँ यह सटीक युग्म (tenant_id, external_id) है। केस फोल्डिंग, व्हाइटस्पेस ट्रिमिंग, या यूनिकोड सामान्यीकरण एक अलग की को परिभाषित करेगा और सुधार से पहले इस पर सहमति होनी चाहिए।
  • कौन सी पंक्ति जीतती है? सबसे बड़ा updated_at, फिर सबसे बड़ा id। जब दो पंक्तियों में टाई होता है तो केवल एक टाइमस्टैम्प कुल क्रम नहीं होता है।
  • क्या लूज़र पंक्तियों में अद्वितीय डेटा हो सकता है? यदि पेलोड को मर्ज किया जाना चाहिए, तो "नवीनतम रखें" अपर्याप्त है। यह प्रॉम्प्ट नवीनतम पेलोड को आधिकारिक मानता है और समीक्षा के लिए लूज़र्स को संग्रहीत (archive) करता है।
  • कौन सी टेबल customer_contacts.id को संदर्भित करती हैं? घोषित फॉरेन कीज़ और एप्लिकेशन-स्तरीय संदर्भों की सूची बनाएं। contact_events दिखाया गया है, लेकिन एक वास्तविक रन को कैटलॉग और स्वामित्व प्रलेखन की खोज करनी चाहिए।
  • क्या रीपॉइंटिंग चाइल्ड डुप्लिकेट बना सकती है? यदि किसी चाइल्ड के पास UNIQUE(contact_id, event_type, created_at) है, तो कन्वर्जेंस के बाद दो समकक्ष इवेंट्स टकरा सकते हैं। अपडेट से पहले तय करें कि उन्हें मर्ज करना है, बनाए रखना है या अस्वीकार करना है।
  • क्या सुधार के दौरान राइट्स जारी रह सकते हैं? ऐतिहासिक बैच तब चल सकते हैं जब सुरक्षित राइटर्स जारी रहते हैं, लेकिन अंतिम डुप्लिकेट स्कैन और विशिष्टता हैंडऑफ़ के लिए या तो एक सत्यापित डेटाबेस-स्तरीय राइट गार्ड या एक मापा गया राइट पॉज़ की आवश्यकता होती है। एक एप्लिकेशन परिपाटी जिसे कुछ राइटर्स बायपास करते हैं, वह कोई गारंटी नहीं है।
  • रोलबैक की आवश्यकता क्या है? सत्यापन और रोलबैक विंडो समाप्त होने तक एक बैकअप या संग्रहीत लूज़र पंक्तियों के साथ-साथ मूल चाइल्ड मैपिंग को बनाए रखें। अकेले लूज़र-टू-विनर मैप खारिज किए गए पेलोड का पुनर्निर्माण नहीं कर सकता है।
  • कितना लोड स्वीकार्य है? बैच आकार एक नियंत्रण है, स्थिरांक नहीं। छोटा शुरू करें और लॉक प्रतीक्षा, राइट p99, WAL, प्रतिकृति अंतराल, डेड टुपल्स और ऑटोवैक्यूम प्रगति पर थ्रॉटल करें।
  • नल्स (nulls) को कैसा व्यवहार करना चाहिए? यहाँ दोनों की कॉलम नॉन-नल हैं। यदि नल्स की अनुमति है और उन्हें टकराना चाहिए, तो PostgreSQL विशिष्टता को NULLS NOT DISTINCT की आवश्यकता होती है; डिफ़ॉल्ट यूनीक सेमेंटिक्स कई नल्स की अनुमति देते हैं।

30-सेकंड उत्तर ढांचा

"मैं सबसे पहले व्यावसायिक नियम को फ्रीज करूंगा: डुप्लिकेट (tenant_id, external_id) साझा करते हैं, और विजेता सबसे बड़ा updated_at है, फिर सबसे बड़ा id है। मैं रैंक किए गए परिणाम का पूर्वावलोकन करूंगा, लूज़र पंक्तियों को संग्रहीत करूंगा, और प्रत्येक लूज़र से उसके विनर तक एक अपरिवर्तनीय run_id मैप को मटीरियलाइज़ करूंगा। छोटे इडेम्पोटेंट बैचों में मैं सभी चाइल्ड संदर्भों को रिकॉर्ड और रीपॉइंट करूंगा, सत्यापित करूंगा कि कोई भी अभी भी लूज़र को लक्षित नहीं करता है, फिर प्राइमरी की द्वारा लूज़र्स को डिलीट करूंगा। मैं हर चरण के बाद काउंट्स, पेलोड के नमूनों, डुप्लिकेट कीज़ और अनाथ संदर्भों का मिलान करूंगा। अंत में, एक सत्यापित राइटर गार्ड या मापे गए राइट पॉज़ के तहत, मैं एक अंतिम डेल्टा क्लीनअप चलाऊंगा और एक यूनीक इंडेक्स बनाऊंगा, फिर सभी राइट्स को समान की अनुबंध का उपयोग करने के लिए कहूँगा। मैं प्रोडक्शन मेट्रिक्स से थ्रॉटल करूंगा और रोलबैक विंडो समाप्त होने तक आर्काइव को बनाए रखूंगा।"

यह ढांचा चयन, निर्भरता क्रम, विनाशकारी नियंत्रण, कन्वर्जेंस और रोकथाम को बताता है। SQL उन निर्णयों का पालन करता है; यह उन्हें प्रतिस्थापित नहीं करता है।

चरण-दर-चरण गहन उत्तर

चरण 1: इनवेरिएंट्स, स्वामित्व और एक रन सीमा स्थापित करें

डेटा को छूने से पहले पोस्टकंडीशन लिखें:

  • प्रत्येक (tenant_id, external_id) के लिए बिल्कुल एक कॉन्टैक्ट मौजूद है;
  • इसकी ID (updated_at, id) क्रम के तहत अधिकतम है;
  • प्रत्येक पहले से मौजूद contact_events पंक्ति अभी भी मौजूद है और उस विनर को संदर्भित करती है;
  • कोई भी घोषित या एप्लिकेशन-स्तरीय संदर्भ किसी डिलीट की गई ID को इंगित नहीं करता है;
  • पूर्ण किए गए बैच का पुनः प्रयास शून्य पंक्तियों को बदलता है;
  • नए राइट्स एक और बिजनेस-की डुप्लिकेट नहीं बना सकते हैं।

एक run_id, स्वामी, स्रोत स्नैपशॉट या बैकअप, प्रारंभ समय, कोड संस्करण, बैच कर्सर, डैशबोर्ड, पॉज़ थ्रेशोल्ड और रोलबैक समय सीमा असाइन करें। सुधार-पूर्व पंक्ति संख्या, विशिष्ट बिजनेस-की संख्या, डुप्लिकेट-की संख्या, लूज़र संख्या और इवेंट संख्या रिकॉर्ड करें। प्रति टेनेंट नियंत्रण योग (control totals) रखें ताकि वैश्विक कुल किसी टेनेंट-स्तरीय नुकसान को छिपा न सके।

चरण 2: डिटर्मिनिस्टिक रैंकिंग का पूर्वावलोकन करें

केवल-पठन (read-only) क्वेरी से प्रारंभ करें और काउंट्स तथा पेलोड अंतरों का निरीक्षण करें:

sql
WITH ranked AS (
    SELECT
        id,
        tenant_id,
        external_id,
        updated_at,
        ROW_NUMBER() OVER (
            PARTITION BY tenant_id, external_id
            ORDER BY updated_at DESC, id DESC
        ) AS rn
    FROM customer_contacts
)
SELECT tenant_id, external_id, COUNT(*) AS loser_count
FROM ranked
WHERE rn > 1
GROUP BY tenant_id, external_id
ORDER BY loser_count DESC, tenant_id, external_id
LIMIT 100;

ROW_NUMBER उपयुक्त है क्योंकि अनुबंध के लिए एक विजेता की आवश्यकता होती है। RANK या DENSE_RANK समान ऑर्डरिंग मानों को समान रैंक असाइन करेगा जब तक कि id शामिल न हो, और एक गायब डिटर्मिनिस्टिक टाई-ब्रेकर बार-बार चलने वाले रनों को अलग-अलग विजेताओं को चुनने में सक्षम बनाएगा। DISTINCT केवल समान चयनित आउटपुट पंक्तियों को हटाता है; यह व्यावसायिक नियम द्वारा एक पूर्ण पसंदीदा पंक्ति को सुरक्षित नहीं रख सकता है।

चरण 3: एक अपरिवर्तनीय लूज़र-टू-विनर मैप को मटीरियलाइज़ करें

चाइल्ड अपडेट और विलोपन के लिए विजेताओं की अलग से पुनर्गणना न करें। सहमत रन सीमा के तहत निर्णय को एक बार मटीरियलाइज़ करें। स्निपेट्स रिपेयर रनर द्वारा आपूर्ति किए गए बाइंड पैरामीटर को दर्शाने के लिए :name का उपयोग करते हैं:

sql
WITH ranked AS (
    SELECT
        id,
        tenant_id,
        external_id,
        ROW_NUMBER() OVER w AS rn,
        FIRST_VALUE(id) OVER w AS winner_id
    FROM customer_contacts
    WINDOW w AS (
        PARTITION BY tenant_id, external_id
        ORDER BY updated_at DESC, id DESC
        ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
    )
)
INSERT INTO contact_dedup_map (
    run_id, loser_id, winner_id, tenant_id, external_id
)
SELECT :run_id, id, winner_id, tenant_id, external_id
FROM ranked
WHERE rn > 1;

contact_dedup_map में PRIMARY KEY(run_id, loser_id) होना चाहिए, यह जांच होनी चाहिए कि लूज़र और विनर अलग हैं, और (run_id, winner_id) पर एक इंडेक्स होना चाहिए। समान run_id के तहत पूर्ण हारने वाली संपर्क पंक्तियों को संग्रहीत करें, या एक परीक्षण किए गए पॉइंट-इन-टाइम पुनर्स्थापना पथ को बनाए रखें। सत्यापित करें कि प्रत्येक मैप विनर मौजूद है, कोई भी विनर उसी रन में लूज़र के रूप में भी दिखाई नहीं देता है, प्रत्येक लूज़र एक बार मैप करता है, और मैप का आकार मापी गई लूज़र संख्या के बराबर है।

200 मिलियन पंक्तियों के लिए, रैंकिंग में एक बड़े स्कैन और सॉर्ट की आवश्यकता हो सकती है। प्रोडक्शन-आकार के डेटा पर EXPLAIN चलाएं, अस्थायी स्थान और वर्कलोड प्रभाव की पुष्टि करें, और तदनुसार शेड्यूल या थ्रॉटल करें। एक कवरिंग इंडेक्स एक मापी गई योजना में मदद कर सकता है लेकिन इसे बनाना अपने आप में महंगा है और बिना सबूत के इसे निर्धारित नहीं किया जाना चाहिए।

चरण 4: पैरेंट्स को डिलीट करने से पहले संदर्भों को रीपॉइंट करें

पहले प्रत्येक संदर्भ की सूची बनाएं। contact_events के लिए, मूल मैपिंग को एक ऑडिट टेबल में सुरक्षित रखें और फिर सीमित ID श्रेणियों को अपडेट करें:

sql
UPDATE contact_events AS e
SET contact_id = m.winner_id
FROM contact_dedup_map AS m
WHERE m.run_id = :run_id
  AND e.contact_id = m.loser_id
  AND e.id > :after_event_id
  AND e.id <= :batch_end_event_id;

पुनः प्रयास इडेम्पोटेंट है: किसी इवेंट द्वारा विनर की ओर इंगित करने के बाद, यह अब m.loser_id से मेल नहीं खाता है। प्रत्येक सीमित बैच को कमिट करें, कमिट के बाद ही कर्सर को बनाए रखें, और अपडेट की गई पंक्तियों का संदर्भ-ऑडिट पंक्तियों के साथ मिलान करें। यदि कोई चाइल्ड विशिष्टता प्रतिबंध टकराता है, तो पूर्व-सहमति वाली मर्ज या अस्वीकृति नियम लागू करें; प्रतिबंधों को अक्षम न करें और यह उम्मीद न करें कि अंतिम स्थिति मान्य है।

पैरेंट विलोपन से पहले, इस क्वेरी को प्रत्येक चाइल्ड टेबल के लिए शून्य लौटाना होगा:

sql
SELECT COUNT(*) AS remaining_loser_references
FROM contact_events AS e
JOIN contact_dedup_map AS m
  ON m.run_id = :run_id
 AND m.loser_id = e.contact_id;

चरण 5: लूज़र्स को छोटे, पुनरारंभ करने योग्य (Restartable) बैचों में डिलीट करें

केवल फ्रीज किए गए मैप से IDs डिलीट करें:

sql
WITH batch AS (
    SELECT loser_id
    FROM contact_dedup_map
    WHERE run_id = :run_id
      AND loser_id > :after_loser_id
    ORDER BY loser_id
    LIMIT 10000
)
DELETE FROM customer_contacts AS c
USING batch AS b
WHERE c.id = b.loser_id
RETURNING c.id, c.tenant_id, c.external_id;

10000 एक प्रारंभिक इंटरव्यू मान है, कोई सार्वभौमिक इष्टतम नहीं है। मापी गई लॉक अवधि, p99, WAL, प्रतिकृति अंतराल, डेड टुपल्स और ऑटोवैक्यूम से बैच आकार और देरी को समायोजित करें। RETURNING आउटपुट विलोपन का प्रमाण बन जाता है। यदि प्रक्रिया कमिट के बाद लेकिन कर्सर दृढ़ता से पहले क्रैश हो जाती है, तो बैच को फिर से चलाने से केवल पहले से डिलीट की गई IDs मिलती हैं और यह सुरक्षित रहता है।

बड़े PostgreSQL विलोपन डेड टुपल्स और WAL बनाते हैं। सामान्य VACUUM की योजना बनाएं और टेबल तथा इंडेक्स ब्लोट की निगरानी करें; VACUUM FULL पर डिफ़ॉल्ट न करें, जो टेबल को फिर से लिखता है और एक मजबूत लॉक लेता है। रीड्स उपलब्ध रहते हैं, लेकिन संसाधन संतृप्ति अभी भी सेवा SLO का उल्लंघन कर सकती है।

चरण 6: राइट्स को कन्वर्ज करें और स्थायी गार्डरेल स्थापित करें

अकेले बल्क सुधार एक गतिशील लक्ष्य को समाप्त नहीं कर सकता है। अंतिम कन्वर्जेंस से पहले, एक राइट अनुबंध तैनात करें जो समान सामान्यीकृत बिजनेस की के लिए निर्माण को क्रमबद्ध करता है और दूसरी प्रति डालने के बजाय कैनोनिकल पंक्ति को अपडेट करता है। सत्यापित करें कि प्रत्येक API, इंपोर्टर, जॉब और डायरेक्ट डेटाबेस राइटर इसका अनुसरण करता है। यदि वह प्रमाण अनुपलब्ध है, तो अंतिम डेल्टा क्लीनअप और इंडेक्स हैंडऑफ़ के लिए राइट्स को रोकें।

अंतिम डुप्लिकेट स्कैन के शून्य लौटने के बाद, एक ट्रांजैक्शन ब्लॉक के बाहर यूनीक इंडेक्स बनाएं:

sql
CREATE UNIQUE INDEX CONCURRENTLY customer_contacts_business_key_uidx
ON customer_contacts (tenant_id, external_id);

CONCURRENTLY राइट्स को सामान्य इंडेक्स-बिल्ड लॉक द्वारा अवरुद्ध होने से रोकता है, लेकिन यह अधिक काम करता है, एक ट्रांजैक्शन ब्लॉक के अंदर नहीं चल सकता है, और एक अमान्य इंडेक्स छोड़ते हुए विशिष्टता उल्लंघन पर विफल हो सकता है। वैधता का निरीक्षण करें, डेटा या राइटर रेस का निदान करें, रनबुक के अनुसार अमान्य इंडेक्स को ड्रॉप करें, और पुनः प्रयास करें; यह न मानें कि IF NOT EXISTS सफलता साबित करता है। एक बार मान्य होने के बाद, यदि स्कीमा गवर्नेंस को कंस्ट्रेंट सेमेंटिक्स की आवश्यकता होती है, तो इसे एक छोटे नियंत्रित DDL चरण में एक नामित यूनीक कंस्ट्रेंट के रूप में संलग्न करें।

यदि सभी राइट्स रुके हुए हैं और एक मापा गया नियमित निर्माण अनुमत विंडो में फिट बैठता है, तो एक गैर-समवर्ती (non-concurrent) यूनीक इंडेक्स सरल और तेज़ है। समवर्ती रूप सही ट्रेडऑफ़ है जब सत्यापित संरक्षित राइट्स को जारी रखने की आवश्यकता होती है; SLO और पूर्वाभ्यास उनके बीच चयन करते हैं।

अंतिम राइटर को डेटाबेस विशिष्टता इनवेरिएंट और एक स्पष्ट संघर्ष नीति का उपयोग करना चाहिए। क्लीनअप के बाद केवल एक यूनीक उल्लंघन को पकड़ने को ही एकमात्र इडेम्पोटेंसी डिज़ाइन न मानें: तय करें कि क्या समान की कैनोनिकल पंक्ति को अपडेट करती है, परस्पर विरोधी पेलोड को अस्वीकार करती है, या समीक्षा में प्रवेश करती है।

चरण 7: मान्य करें, निरीक्षण करें और रोलबैक विंडो को बंद करें

कम से कम इन जांचों का मिलान करें:

  • सुधार के बाद कुल संपर्क सुधार से पहले के संपर्कों में से संग्रहीत लूज़र्स को घटाकर प्राप्त संख्या के बराबर है;
  • GROUP BY tenant_id, external_id HAVING COUNT(*) > 1 शून्य लौटाता है;
  • प्रत्येक मैप विनर मौजूद है और प्रत्येक लूज़र अनुपस्थित है;
  • चाइल्ड इवेंट संख्या अपरिवर्तित है, शेष लूज़र संदर्भ शून्य हैं, और अनाथ संदर्भ शून्य हैं;
  • नमूना किए गए विनर (updated_at DESC, id DESC) नियम और संग्रहीत पेलोड से मेल खाते हैं;
  • एक डुप्लिकेट इंसर्ट को अस्वीकार कर दिया जाता है या घोषित अपडेट नीति का पालन करता है;
  • पूर्ण मैपिंग, रीपॉइंट और विलोपन बैचों को दोबारा चलाने से शून्य पंक्तियाँ बदलती हैं।

बैच दर, त्रुटियों, पंक्ति-लॉक प्रतीक्षाओं, राइट p50/p95/p99, WAL बाइट्स, प्रतिकृति अंतराल, डेड टुपल्स, ऑटोवैक्यूम प्रगति, मैप/आर्काइव वृद्धि, शेष लूज़र संदर्भों, शेष डुप्लिकेट कीज़ और यूनीक-इंडेक्स निर्माण प्रगति की निगरानी करें। परीक्षण की गई रोलबैक विंडो के माध्यम से आर्काइव और संदर्भ-ऑडिट डेटा रखें। उसके बाद ही अस्थायी राइट गार्ड्स और रिपेयर टेबल्स को रिटेंशन नीति के अनुसार हटाएं।

उच्च-गुणवत्ता वाला नमूना उत्तर

"मैं डुप्लिकेट्स को समान (tenant_id, external_id) के रूप में परिभाषित करूंगा और updated_at DESC, id DESC द्वारा कैनोनिकल पंक्ति का चयन करूंगा; यूनीक ID संबंधों को डिटर्मिनिस्टिक बनाती है। विलोपन से पहले मैं बेसलाइन काउंट्स रिकॉर्ड करूंगा, पेलोड अंतरों का निरीक्षण करूंगा, प्रत्येक संदर्भ की सूची बनाऊंगा, एक पुनर्स्थापन योग्य बैकअप लूंगा, और ROW_NUMBER और FIRST_VALUE का उपयोग करके प्रत्येक लूज़र ID से उसके विनर तक एक अपरिवर्तनीय run_id मैप बनाऊंगा।

मैं हारने वाली पंक्तियों और मूल चाइल्ड मैपिंग को संग्रहीत करूंगा। फिर मैं छोटी प्राइमरी-की श्रेणियों में contact_events को रीपॉइंट करूंगा। प्रत्येक बैच अपने कर्सर के आगे बढ़ने से पहले कमिट करता है, और एक पुनः प्रयास इडेम्पोटेंट होता है क्योंकि पहले से स्थानांतरित इवेंट्स अब किसी लूज़र ID से मेल नहीं खाते हैं। चाइल्ड कंस्ट्रेंट्स सक्षम रहते हैं; कोई भी टकराव एक स्पष्ट मर्ज या अस्वीकृति नियम का पालन करता है। प्रत्येक चाइल्ड टेबल द्वारा शून्य लूज़र संदर्भों की रिपोर्ट करने के बाद ही मैं सीमित लेन-देन और RETURNING को सबूत के रूप में उपयोग करते हुए फ्रीज किए गए मैप से IDs द्वारा संपर्कों को डिलीट करूंगा।

ऑपरेटिंग दर एक निश्चित बैच संख्या के बजाय लॉक समय, अनुरोध p99, WAL, प्रतिकृति अंतराल, डेड टुपल्स और ऑटोवैक्यूम का पालन करती है। मैं पंक्ति-संख्या संरक्षण, प्रति बिजनेस की एक पंक्ति, कोई लूज़र नहीं, कोई अनाथ नहीं, अपरिवर्तित इवेंट संख्या, डिटर्मिनिस्टिक सर्वाइवर नमूने और शून्य-परिवर्तन पुनः प्रयासों को मान्य करूंगा।

दोबारा ऐसा होने से रोकने के लिए, सभी राइटर्स को समान सामान्यीकृत बिजनेस की पर कन्वर्ज होना चाहिए। एक सत्यापित राइटर गार्ड या मापे गए पॉज़ के तहत, मैं एक अंतिम डेल्टा क्लीनअप करूंगा और एक ट्रांजैक्शन के बाहर (tenant_id, external_id) पर एक समवर्ती यूनीक इंडेक्स बनाऊंगा। मैं सत्यापित करूंगा कि इंडेक्स मान्य है क्योंकि एक विफल समवर्ती निर्माण एक अमान्य इंडेक्स छोड़ सकता है। स्थायी राइट पथ फिर घोषित अनुबंध के अनुसार संघर्षों को अपडेट, अस्वीकार या समीक्षा करता है। आर्काइव तब तक बना रहता है जब तक रोलबैक साक्ष्य और अवलोकन विंडो पूर्ण नहीं हो जाते।"

यह उत्तर अपरिवर्तनीय संचालन को स्पष्ट नियंत्रणों पर निर्भर बनाता है और SQL, फॉरेन कीज़, राइटर व्यवहार और डेटाबेस कंस्ट्रेंट को एक बिजनेस-की अनुबंध के तहत रखता है।

सामान्य गलतियाँ

  • डुप्लिकेट मिलने के तुरंत बाद DELETE चलाना → संदर्भ, पेलोड और रोलबैक साक्ष्य खो सकते हैं → पूर्वावलोकन करें, संग्रहीत करें, मैप करें, रीपॉइंट करें, मान्य करें, फिर डिलीट करें।
  • केवल external_id द्वारा विभाजन करना → विभिन्न टेनेंट्स में समान बाहरी IDs एक साथ मिल जाती हैं → पूर्ण बिजनेस की (tenant_id, external_id) का उपयोग करें।
  • केवल updated_at द्वारा ऑर्डर करना → टाई हुए टाइमस्टैम्प किसी अन्य रन पर एक अलग विजेता की अनुमति देते हैं → अंतिम टाई-ब्रेकर के रूप में अद्वितीय id जोड़ें।
  • rn > 1 के साथ RANK का उपयोग करना → टाई हुई नवीनतम पंक्तियाँ दोनों रैंक 1 प्राप्त कर सकती हैं → जब ठीक एक सर्वाइवर की आवश्यकता हो तो कुल क्रम के साथ ROW_NUMBER का उपयोग करें।
  • प्रत्येक बैच में विजेताओं की पुनर्गणना करना → समवर्ती परिवर्तन लक्ष्य को स्थानांतरित कर सकते हैं और ऑडिटेबिलिटी को नष्ट कर सकते हैं → एक संस्करणित लूज़र-टू-विनर मैप को फ्रीज करें।
  • बच्चों को रीपॉइंट करने से पहले पैरेंट्स को डिलीट करना → फॉरेन कीज़ डिलीट को अस्वीकार कर देती हैं या कैस्केड मान्य इतिहास को हटा देते हैं → पहले सभी संदर्भों की सूची बनाएं और स्थानांतरित करें।
  • गति के लिए कंस्ट्रेंट्स को अक्षम करना → सुधार मूक अनाथ या चाइल्ड टकराव पैदा कर सकता है → कंस्ट्रेंट्स को सक्रिय रखें और संघर्ष से निपटने को परिभाषित करें।
  • 10,000 को सही बैच आकार कहना → हार्डवेयर और वर्कलोड सुरक्षित थ्रूपुट तय करते हैं → छोटा शुरू करें और SLO, WAL, अंतराल और वैक्यूम मेट्रिक्स से अनुकूलित करें।
  • क्लीनअप के बाद रुक जाना → दोषपूर्ण राइटर डुप्लिकेट्स को फिर से बनाता है → राइटर्स को कन्वर्ज करें और PostgreSQL में विशिष्टता लागू करें।
  • यह मानना कि CREATE UNIQUE INDEX CONCURRENTLY हमेशा सफल होता है → एक रेस या डुप्लिकेट एक अमान्य इंडेक्स छोड़ सकता है → वैधता का निरीक्षण करें और एक स्पष्ट पुनर्प्राप्ति प्रक्रिया का पालन करें।
  • नियमित क्लीनअप के रूप में VACUUM FULL का उपयोग करना → यह टेबल को फिर से लिखता है और दृढ़ता से लॉक करता है → सामान्य वैक्यूम और ब्लोट की निगरानी करें, फिर असाधारण पुनर्लेखन को अलग से शेड्यूल करें।

अनुवर्ती प्रश्न और उत्तर

अनुवर्ती 1: एक CTE के साथ डिलीट करके एकल लेन-देन में समाप्त क्यों नहीं करते?

बिना किसी संदर्भ और रुके हुए राइटर्स वाली एक छोटी पृथक टेबल के लिए, एक CTE DELETE पर्याप्त हो सकता है। 200 मिलियन पंक्तियों पर, एक लेन-देन लॉक और पुराने पंक्ति संस्करणों को बनाए रख सकता है, एक बड़ा WAL बर्स्ट उत्पन्न कर सकता है, प्रतिकृतियों में देरी कर सकता है, वैक्यूम को जटिल बना सकता है, और सब-कुछ-या-कुछ-नहीं पुनर्प्राप्ति घटना बना सकता है। फ्रीज किया गया मैप और सीमित बैच प्रगति, ऑडिटेबिलिटी, थ्रॉटलिंग और पुनरारंभता प्रदान करते हैं। इसके पैमाने और निर्भरता मान्यताओं को साबित करने के बाद ही सरल क्वेरी का उपयोग करें।

अनुवर्ती 2: क्या होगा यदि दो पंक्तियाँ समान टाइमस्टैम्प साझा करती हैं लेकिन उनके पेलोड अलग-अलग हैं?

प्रॉम्प्ट का नियम सबसे बड़ा id चुनता है, इसलिए परिणाम डिटर्मिनिस्टिक है, लेकिन डिटर्मिनिज्म यह साबित नहीं करता है कि पेलोड सेमेंटिक रूप से सही है। सुधार से पहले परस्पर विरोधी पेलोड को मापें और उच्च जोखिम वाले फ़ील्ड को समीक्षा या डोमेन-विशिष्ट मर्ज नियम पर रूट करें। दोनों पंक्तियों को संग्रहीत करें। विलोपन शुरू होने के बाद फ़ील्ड-दर-फ़ील्ड मर्ज का आविष्कार न करें।

अनुवर्ती 3: क्या होगा यदि external_id शून्य (null) हो सकता है?

पहले परिभाषित करें कि क्या null का अर्थ "अज्ञात और स्वतंत्र" है या एक डुप्लिकेट मान है। PostgreSQL यूनीक कंस्ट्रेंट्स और इंडेक्स डिफ़ॉल्ट रूप से नल्स को अलग मानते हैं, इसलिए कई null कीज़ की अनुमति है। यदि नल्स को टकराना चाहिए, तो NULLS NOT DISTINCT का उपयोग करें और समान सेमेंटिक्स के साथ रैंक करें; यदि अज्ञात संपर्क स्वतंत्र हैं, तो डिफ़ॉल्ट व्यवहार बनाए रखें या गैर-नल IDs के लिए आंशिक विशिष्टता नियम का उपयोग करें। क्लीनअप और कंस्ट्रेंट को सहमत होना चाहिए।

अनुवर्ती 4: क्या सुधार बिना किसी राइट पॉज़ के पूरी तरह से ऑनलाइन हो सकता है?

हाँ केवल तभी जब प्रत्येक राइटर अंतिम स्कैन से पहले बिजनेस की के लिए डेटाबेस-समर्थित क्रमांकन (serialization) या आरक्षण नियम का उपयोग करने के लिए सिद्ध हो। तब ऐतिहासिक क्लीनअप कन्वर्ज हो सकता है और एक समवर्ती यूनीक इंडेक्स बनाया जा सकता है। यदि कोई लीगेसी इंपोर्टर या डायरेक्ट राइटर गार्ड को बायपास करता है, तो निर्माण के दौरान एक नया डुप्लिकेट दिखाई दे सकता है और इसे विफल कर सकता है। एक शैडो टेबल और नियंत्रित कटओवर एक अन्य विकल्प है, लेकिन यह डुअल-राइट, बैकफिल, फॉरेन-की और रोलबैक जटिलता जोड़ता है और इसे डाउनटाइम SLO द्वारा उचित ठहराया जाना चाहिए।

अनुवर्ती 5: आप प्रत्येक फॉरेन की और छिपे हुए संदर्भ को कैसे ढूंढते हैं?

उन फॉरेन कीज़ के लिए PostgreSQL कैटलॉग को क्वेरी करें जिनका संदर्भित संबंध customer_contacts है, फिर संग्रहीत संपर्क IDs के लिए व्यूज़, ट्रिगर्स, CDC कंज्यूमर्स, सर्च इंडेक्स, डेटा एक्सपोर्ट्स और एप्लिकेशन स्कीमा का निरीक्षण करें। घोषित फॉरेन कीज़ लागू करने योग्य साक्ष्य प्रदान करती हैं; स्वामित्व प्रलेखन और कोड खोज एप्लिकेशन-स्तरीय संदर्भों को कवर करते हैं। डिलीट गेट प्रत्येक खोजे गए उपभोक्ता और उसके मिलान जांच को सूचीबद्ध करता है।

अनुवर्ती 6: क्या होगा यदि रीपॉइंटिंग इवेंट्स चाइल्ड यूनीक कंस्ट्रेंट का उल्लंघन करते हैं?

अपडेट करने से पहले रोकें। टकराव का मतलब है कि उनके पैरेंट IDs के कन्वर्ज होने के बाद दो चाइल्ड पंक्तियाँ समान हो जाती हैं। परिभाषित करें कि क्या वे डुप्लिकेट इवेंट्स हैं, विशिष्ट अवलोकन हैं जिन्हें एक नई की की आवश्यकता है, या एक डेटा संघर्ष है। टकराव समूहों को मटीरियलाइज़ करें, उन्हें संग्रहीत करें, और रीपॉइंट करने से पहले एक डिटर्मिनिस्टिक मर्ज या अस्वीकृति नियम लागू करें। चाइल्ड कंस्ट्रेंट को हटाना व्यावसायिक सेमेंटिक्स को बदलता है और यह कोई सुधार योजना नहीं है।

अनुवर्ती 7: आप कैसे साबित करते हैं कि लंबे रन के दौरान टेबल बदलने के बाद चयनित विजेता नवीनतम था?

मैप अपने रिकॉर्ड किए गए स्नैपशॉट या रन सीमा के तहत विजेता को साबित करता है, असीमित भविष्य के राइट्स के तहत नहीं। राइटर्स को सुरक्षित करें ताकि बाद के संचालन मैप की गई कैनोनिकल पंक्ति को अपडेट करें, एक स्रोत संस्करण रिकॉर्ड करें, और विशिष्टता हैंडऑफ़ से पहले एक अंतिम डेल्टा स्कैन करें। यदि व्यावसायिक नियमों को पेलोड को बदलने के लिए बाद के अपडेट की आवश्यकता होती है, तो विजेता को अपडेट करें; दूसरा भौतिक संपर्क न बनाएं। ऑडिट रिकॉर्ड में अस्थायी गारंटी बताएं।

सार्वजनिक स्रोत

संबंधित प्रश्न