DanLevy.net

Foreign Keys: ये तेज़ हैं या नहीं, ये सवाल मत पूछो

सवाल यह है कि तुम असल में किस चीज़ को ऑप्टिमाइज़ कर रहे हो।

मैंने जो सबसे महंगा डेटाबेस ऑप्टिमाइज़ेशन देखा, वो किसी ने सारे Foreign Keys हटाकर शुरू किया था।

इसलिए नहीं कि उन्होंने कोई बॉटलनेक मेज़र किया था। इसलिए नहीं कि writes सच में स्लो थे। बल्कि इसलिए कि उन्होंने कहीं पढ़ लिया कि “Foreign Keys स्केल नहीं करते।” छह महीने बाद, उनके पास 2 अरब orphaned records थे, billing system deleted users को चार्ज कर रहा था, और analytics 40% तक गलत थी।

जब उन्होंने constraints वापस जोड़ने की कोशिश की? डेटाबेस पहले से ही corrupt हो चुके data को validate करते-करते रुक गया।

वेब डेवलपमेंट में ये बहुत आम सोच है कि Foreign Keys inherently स्लो होते हैं, कि ये training wheels हैं जो तुम “real” systems पर पहुंचकर हटा देते हो। लेकिन इससे constraint का असली मकसद ही छूट जाता है। तुम fast और slow के बीच चुनाव नहीं कर रहे। तुम अलग-अलग failure modes के बीच चुनाव कर रहे हो।

ऐसे सोचो: safety glass, seatbelts, और airbags सब तुम्हारी कार में वज़न बढ़ाते हैं। वो ज़रूर तुम्हारी गाड़ी को स्लो और कम fuel-efficient बनाते हैं। लेकिन तुम अपना 0-60 टाइम ऑप्टिमाइज़ करने के लिए उन्हें नहीं निकालते, क्योंकि तुम किसी और चीज़ के लिए ऑप्टिमाइज़ कर रहे हो।

सवाल ये नहीं कि Foreign Keys तुम्हें स्लो करते हैं या नहीं। ज़ाहिर है करते हैं। सवाल ये है कि बदले में तुम्हें क्या मिलता है, और क्या तुम्हें सच में उसकी ज़रूरत है।

तुम असल में क्या ट्रेड कर रहे हो

एक concrete example देता हूं। तुम एक weather monitoring system बना रहे हो — weather stations, sensor devices, sensor readings, और US states के लिए tables।

क्या तुम सब को Foreign Key से जोड़ दोगे? सोचते हैं कि असल में क्या बदलता है और परिणाम क्या होते हैं:

US States शायद नहीं बदलेंगे। Wyoming का नाम कभी नहीं बदलेगा। तुम्हें हर insert पर state codes validate करने के लिए Foreign Key की ज़रूरत नहीं जब तुम जानते हो कि reference data static है। वो बेकार overhead है।

Weather stations add, move, और decommission होते हैं। लेकिन एक सवाल: क्या तुम चाहते हो कि historical readings अपना station “खो दें” अगर कोई गलती से station record delete कर दे? शायद तुम चाहते हो कि वो data intact रहे भले ही station गया ना रहे। इसका मतलब है कि तुम readings को historical snapshot मान रहे हो, live reference नहीं — जो बदल देता है कि Foreign Key का मतलब ही बनता है या नहीं।

Sensor readings हज़ारों बार प्रति मिनट insert हो रहे हैं। हर Foreign Key check का मतलब एक lookup। हर lookup तुम्हारी tables पर contention बढ़ाता है। अगर slow validation का मतलब है कि तुम्हारा insert queue भर जाता है और real-time data खत्म हो जाता है, तो वो orphaned record से अलग तरह का data loss है।

तुम देख सकते हो ये कहाँ जा रहा है। चुनाव performance बनाम correctness के बारे में abstract concepts के रूप में नहीं है। ये इस बारे में है कि तुम्हारी actual constraints और actual consequences को देखते हुए किस specific failure को तुम ज़्यादा सहन कर सकते हो।

अगर गलत references का मतलब corrupted billing data या regulatory violations है, तुम शायद performance cost की परवाह ना करते हुए Foreign Keys चाहते हो। अगर slow validation का मतलब है कि real-time sensor data हमेशा के लिए खत्म हो जाता है क्योंकि queue overflow हो गया, तो शायद validation गलत tradeoff है।

जब Fast Writes सच में मायने रखते हैं

तो तुमने तय किया कि तुम्हें maximum write speed चाहिए। Queue भर रही है, transactions timeout हो रहे हैं, और Foreign Key checks वाकई समस्याएं पैदा कर रहे हैं — समस्याएं जो तुमने वाकई measure की हैं (सिर्फ theory में नहीं सोची हैं)।

तुम्हारे पास कुछ options हैं। तुम अपना transaction isolation level SERIALIZABLE से READ COMMITTED में बदल सकते हो, जो तेज़ है लेकिन कुछ consistency guarantees छीन लेता है। तुम अपने commits batch कर सकते हो, एक-एक करके की जगह प्रति transaction 1000 rows insert करके FK overhead को amortize कर सकते हो। या तुम एक append-only log structure में denormalize कर सकते हो जहाँ तुम references validate करने की कोशिश ही नहीं कर रहे।

तीसरा option cheating नहीं है। वो बस एक अलग design है:

CREATE TABLE sensor_log (
id BIGSERIAL PRIMARY KEY,
recorded_at TIMESTAMPTZ NOT NULL,
data JSONB NOT NULL -- { station_id, sensor_id, temp, humidity, ... }
);
CREATE INDEX ON sensor_log USING GIN (data);
CREATE INDEX ON sensor_log (recorded_at);

कोई joins नहीं। कोई Foreign Key checks नहीं। बस data append करो और time range या JSONB blob पर GIN index से query करो। क्या ये “best practice” है? शायद उस sense में नहीं जिसमें database textbooks सिखाते हैं। क्या ये एक Raspberry Pi पर 50,000 rows प्रति मिनट insert करने पर काम करता है? बिलकुल।

Disconnect तब आता है जब लोग “best practice” को moral imperative मानते हैं बजाय एक pattern के जो common scenarios में अच्छी तरह काम करता है लेकिन शायद तुम्हारे लिए सही नहीं है।

Normalization का Trap

Database courses normalization सिखाना पसंद करते हैं। हर कीमत पर duplication से बचो। Third Normal Form या कुछ नहीं।

तो तुम्हारे पास कुछ ऐसा बनता है: OrdersOrderItemsProductsVariantsColorsSizes

छह table joins सिर्फ ये जवाब देने के लिए कि “क्या मैंने पिछले क्रिसमस पर red shirt order की थी या blue?” और अगर तुम्हें product name शामिल करना है, तो वो catalog hierarchy में तीन और joins दूर है।

लेकिन रुको। इसका justification आमतौर पर ये होता है कि “क्या होगा अगर brand ने Blue का label बदल दिया?” अगर ऐसा होता है, तो क्या तुम सच में चाहते हो कि historical orders retroactively color बदल दें? ज़ाहिर है नहीं। जब किसी ने order किया, उन्होंने “Blue T-Shirt, Size M” उसी moment में जैसा वो था, खरीदा — किसी abstract reference के रूप में नहीं जो बाद में update हो सकता है।

इस पर विचार करना ज़रूरी है क्योंकि ये subtle है। कुछ data fundamentally snapshot है, reference नहीं। जब तुम snapshot data को live reference की तरह treat करते हो, तुम इन absurd joins का जाल बुनते हो जो कुछ ऐसा reconstruct करने के लिए हैं जिसे write time पर ही denormalize कर देना चाहिए था।

{"color": "blue", "size": "M"} सीधे order पर store कर दो। बस, हो गया।

Snapshot Data पहचानना

तुम कैसे जानते हो कि कुछ snapshot होना चाहिए? अपने से पूछो कि क्या ये point-in-time record है:

Orders product details को उसी रूप में capture करते हैं जैसे purchase time पर थे। Audit logs user state record करते हैं जब उन्होंने action perform किया। History tables update से पहले record state preserve करते हैं। Event streams capture करते हैं कि क्या हुआ, कब, किस data के साथ।

अगर जवाब “हाँ, ये किसी moment को record कर रहा है” है, तो इसे normalize करना बंद करो। Snapshot करना शुरू करो।

Opaque Blobs

Snapshot से परे एक और category है: data जिसमें तुम कभी query नहीं करते। तुम बस इसे store करते हो और पूरा retrieve करते हो।

LLM model configurations जैसे {"model": "gpt-4", "temperature": 0.7, "max_tokens": 2000} — तुम इन्हें temperature से query नहीं करते। तुम पूरा config request ID से fetch करते हो जब ज़रूरत हो। JWT payloads decoding के बाद, debugging के लिए API request/response logs, theme settings और notification flags वाले user preference objects। ये सब opaque blobs हैं। इन्हें normalization नहीं चाहिए। Foreign Keys नहीं चाहिए। इन्हें JSONB में डालो और आगे बढ़ो।

6-table join से पता लगाना कि किस color की shirt order की गई? वो proper normalization नहीं है। वो confused thinking है कि तुम reference store कर रहे हो या value।

(हालांकि सावधान रहो: ये बाद में भयानक तरीके से backfire कर सकता है अगर तुम्हें उस data को query करना पड़े। देखो The JSONB Seduction कि ये approach कब अपना खुद का nightmare बना देती है।)

Scale Context है

तुम सुनोगे लोग कहते हैं “Foreign Keys स्केल नहीं करते।” लेकिन scale तुम्हारे hardware और architecture के सापेक्ष पूरी तरह relative है।

Raspberry Pi पर 10,000 sensor readings प्रति मिनट log करना microSD card पर? वो उस hardware के लिए वाकई high scale है। AWS Aurora provisioned IOPS के साथ billions of rows handle करना? तुम Foreign Keys के साथ आराम से कर सकते हो बिना पसीना आए।

Actual hard limit row count या write volume के बारे में नहीं है। वो sharding है।

जब तुम्हारी Users table Server A पर है और Orders table Server B पर, तो Foreign Keys physically काम नहीं कर सकते। डेटाबेस के पास network boundaries पर constraint enforce करने का कोई mechanism नहीं है। उस point पर, तुम पहले से background jobs चला रहे हो orphans ढूंढने के लिए और eventual consistency patterns implement कर रहे हो।

ये multi-tenant SaaS में होता है जहाँ हर tenant को compliance के लिए अपना isolated database मिलता है, या IoT deployments में जहाँ तुम्हारे पास 50,000 edge devices हैं हर एक locally SQLite चला रही है। एक बार वहाँ पहुँचकर, Foreign Keys table से उतर चुके हैं (शाब्दिक रूप से) performance considerations की परवाह किए बिना।

लेकिन जब तक तुम उस architectural boundary तक नहीं पहुँचे, शायद Netflix की problems के लिए prematurely optimize मत करो जब तुम एक 10-user internal tool बना रहे हो।

Practice में ये असल में कैसा दिखता है

“क्या मुझे Foreign Keys use करने चाहिए” पूछने की जगह, ये तीन सवाल पूछो:

अगर ये reference गलत है तो क्या टूट जाएगा? क्या ये lawsuit है, corrupted billing, regulatory violation? या सिर्फ एक missing join जो तुम्हारे analytics dashboard में null return करता है?

अगर validation स्लो है तो क्या टूट जाएगा? क्या तुम irreplaceable real-time data खो दोगे? या तुम्हारे queries सिर्फ 50 milliseconds ज़्यादा लेंगे?

क्या ये data snapshot है या reference? क्या तुम record कर रहे हो कि कुछ किसी specific moment पर कैसा दिखता था, या तुम authoritative current value की ओर point कर रहे हो?

वहाँ से, patterns काफी naturally निकलते हैं:

Financial transactions, authentication sessions — कुछ भी जहाँ data corruption का मतलब legal liability, वो शायद performance overhead की परवाह किए बिना Foreign Keys चाहिए।

High-volume logs, append-only time series data — कुछ भी जहाँ तुम प्रति मिनट एक million events लिख रहे हो, उसे शायद हर write पर validation overhead नहीं चाहिए।

Historical snapshots जैसे orders और audit logs, data जिसे तुम हमेशा complete blob के रूप में fetch करते हो जैसे user preferences, schemas जो तुम control नहीं करते जैसे external APIs से webhook payloads… ये अक्सर denormalized बेहतर काम करते हैं।

लेकिन ध्यान दो कि मैंने “शायद” और “अक्सर” कहा। क्योंकि context मायने रखता है, और तुम्हारा context मेरे से अलग है।

अंतिम विचार

Foreign Keys performance problem नहीं हैं। वो write speed और data integrity के बीच tradeoff हैं, और क्या वो tradeoff सही बैठता है ये पूरी तरह तुम्हारी specific bottlenecks और specific consequences पर निर्भर करता है।

असली issue तब होता है जब लोग Foreign Keys हटाते हैं क्योंकि उन्होंने कहीं “web scale” के बारे में पढ़ा बिना वाकई measure किए कि उन्हें write performance problem है भी या नहीं, या सोचे बिना कि वो क्या छोड़ रहे हैं। तुम Netflix की architecture को एक greenfield project पर cargo-cult कर रहे हो जो प्रति दिन 100 transactions process करता है।

शायद performance cost तुम्हारे use case के लिए इसके लायक है। शायद नहीं है। लेकिन कम से कम वो decision इस बात पर करो कि तुम वाकई किसके लिए ऑप्टिमाइज़ कर रहे हो, न कि जो तुम्हें लगता है कि तुम्हें ऑप्टिमाइज़ करना चाहिए।

तुम किसके लिए ऑप्टिमाइज़ कर रहे हो?

संसाधन