SQLite को भी Rust-स्टाइल editions की ज़रूरत है
(mort.coffee)- SQLite स्थानीय स्टोरेज और embedded projects में व्यापक रूप से इस्तेमाल होने वाला self-contained RDBMS है, लेकिन backward compatibility के कारण यह सुरक्षा और performance के लिए नुकसानदेह पुराने default settings बनाए रखता है
- डिफ़ॉल्ट रूप से यह foreign key constraints को enforce नहीं करता और
ROWIDको दोबारा इस्तेमाल कर सकता है, जिससे data integrity की समस्या पैदा हो सकती है जहाँ हटाए गए user की posts उसी ID पाने वाले किसी दूसरे user से जुड़ जाती हैं - सामान्य tables घोषित columns से अलग data types भी स्टोर कर लेते हैं, और STRICT tables इसे रोकते हैं, लेकिन हर table पर अलग से
strictलिखना पड़ता है और यह custom type names इस्तेमाल करने वाले तरीकों से टकराता है - concurrent writes में तुरंत आने वाला
SQLITE_BUSY, बंद पड़ा WAL, और conservative synchronization settings को क्रमशःbusy_timeout,journal_mode,synchronouspragma से सुधारा जा सकता है - अगर
PRAGMA edition = 2026जैसी year-based editions के जरिए सुरक्षित default settings के bundle चुने जा सकें, तो SQLite बिना पुराने behavior को तोड़े अपने defaults को आगे भी बेहतर बना सकता है
SQLite की ताकत और पुराने defaults
- SQLite embedded projects के लिए database और local data storage का लगभग industry standard है, और कुछ server software जैसे lobste.rs भी SQLite पर चल रहे हैं
- पारंपरिक RDBMS की तरह अलग process के रूप में चलने के बजाय यह library-form RDBMS है, इसलिए software को self-contained रखा जा सकता है
- सामान्य file format की तरह custom serializer और parser लिखने की ज़रूरत नहीं पड़ती, इसलिए यह RDBMS और file-based storage दोनों के फायदे देता है
- लेकिन data integrity, concurrency, और performance को प्रभावित करने वाले कई defaults अनुपयुक्त बने हुए हैं
foreign keys को enforce न करने से पैदा होने वाली integrity problems
- foreign keys database consistency बनाए रखने और broken references रोकने का मुख्य तरीका हैं, लेकिन SQLite इन्हें डिफ़ॉल्ट रूप से enforce नहीं करता
- भले ही
posts.user_idकोusers.idrefer करने के लिए declare किया गया हो, default setting में यह निम्न behavior को नहीं रोकता- valid user ID के बिना post बनाना
- उस user की posts छोड़े रखते हुए user को delete करना
INTEGER PRIMARY KEYtable केROWIDका alias बन जाता है, और SQLite काROWIDallocation algorithm कुछ स्थितियों में IDs को reuse करता है- अगर Bob ने ID
1से post लिखी और बाद में सिर्फ account delete हो गया, तब भी foreign key error नहीं आता - बाद में Alice को वही reused ID
1मिल जाए, तो Bob की पुरानी posts Alice से जुड़ जाती हैं - query सामान्य रूप से सफल हो जाती है, इसलिए यह साधारण broken reference की तुलना में पकड़ना और मुश्किल होता है
- अगर Bob ने ID
- हर connection पर नीचे वाला pragma चालू करने से foreign keys enforce किए जा सकते हैं
PRAGMA foreign_keys = ON;
- अगर यह शुरुआत से लागू होता, तो posts छोड़कर user delete करने की कोशिश
FOREIGN KEY constraint failederror के साथ रुक जाती
घोषित type से अलग data types की अनुमति देने वाली सामान्य tables
- SQLite values
NULL,INTEGER,REAL,TEXT,BLOBमें से एक होती हैं, लेकिन सामान्य tables में column declaration स्टोर होने वाले type को सख्ती से सीमित नहीं करती, बल्कि type affinity तय करती है INTEGERaffinity वाले column में नीचे वाले storage rules लागू होते हैं- ऐसा
TEXTजिसे integer की तरह समझा जा सके,INTEGERमें convert हो जाता है - ऐसा
TEXTजिसे floating-point number की तरह समझा जा सके, double-precisionREALमें convert हो जाता है - बाकी values अपनी मूल type में ही store होती हैं
- ऐसा
- दूसरी affinities के लिए भी अलग conversion rules हैं
BLOBaffinity value को ज्यों का त्यों store करती हैTEXTaffinityBLOB,TEXT,NULLको वैसे ही रखती है और numbers कोTEXTमें convert करती हैREALaffinityINTEGERaffinity की तरह काम करती है, लेकिन integers कोREALमें बदल देती है
- इसी वजह से
duration_sec INTEGERcolumn में'Way too long, I mean come on'जैसी non-numeric string भी store की जा सकती है - वास्तविक projects में boolean के लिए numeric
1,0की जगह string'1','0'लिखे जाने के मामले भी मिले, जिनके कारण data cleanup और debugging करनी पड़ी
STRICT tables और custom types का टकराव
- STRICT tables में अगर column में गलत data type डालें, तो
cannot store TEXT value in INTEGER columnजैसी type error आती है
CREATE TABLE music (
id INTEGER PRIMARY KEY,
name TEXT,
duration_sec INTEGER
) strict;
-
सभी tables को globally strict बनाने के लिए कोई pragma नहीं है, इसलिए हर
CREATE TABLEstatement मेंstrictजोड़ना पड़ता है -
SQLite का रुख है कि flexible typing उपयोगी है, लेकिन STRICT tables में भी
ANYdata type लिखकर हर तरह की values रखने वाला column बनाया जा सकता है- यानी किन columns में arbitrary types चलेंगे और किनमें specific type enforce होगा, यह साफ़ तौर पर अलग किया जा सकता है
-
ज़्यादा व्यावहारिक compatibility problem यह है कि STRICT tables सिर्फ types enforce नहीं करतीं, बल्कि type specifier interpretation rules भी बदल देती हैं
-
सामान्य tables type names को कैसे interpret करती हैं
- सामान्य SQLite tables declared type string के आधार पर affinity तय करती हैं
- अगर
"INT"शामिल है, तो उसेINTEGERमाना जाता है - अगर
"CHAR","CLOB","TEXT"में से कुछ शामिल है, तो वहTEXTबनता है - अगर
"BLOB"शामिल है या type छोड़ा गया है, तो उसेBLOBमाना जाता है - अगर
"REAL","FLOA","DOUB"में से कुछ शामिल है, तो वहREALबनता है - अगर कोई शर्त लागू न हो, तो उसे
NUMERICमाना जाता है - loose typing और इन rules को मिलाकर columns पर
DATETIME,KEY_VALUE_SET,COLORजैसे custom type names लगाए जा सकते हैं - database connectors या wrappers type name देखकर serialization और deserialization अपने-आप कर सकते हैं
- भले ही अलग processing feature न हो, type name अपने आप में column के expected data को document करता है
- अगर STRICT को उसी रूप में default बना दिया जाए, तो यह उपयोग का तरीका छोड़ना पड़ेगा, इसलिए explicit custom type aliases की ज़रूरत होगी
CREATE TYPE KEY_VALUE_SET = TEXT;- अगर strict table में
KEY_VALUE_SETजैसा alias इस्तेमाल किया जा सके, तो schema में यह भी दर्ज किया जा सकेगा किTEXTcolumn में application द्वारा parse किया जाने वाला कौन-सा expected data pattern है - custom types से
CHECKconstraints जोड़ने की सुविधा भी उपयोगी होगी - SQL:1999 standard में constraints तक समर्थित type alias syntax
CREATE DOMAINपहले से मौजूद है, इसलिए SQLite इसे इसी standard statement के रूप में implement कर सकता है
concurrent writes और SQLITE_BUSY
- SQLite कई concurrent reads की अनुमति देता है, लेकिन writes एक समय में सिर्फ एक ही संभालता है
- default setting में जब दो processes एक साथ write lock लेने की कोशिश करते हैं, तो उनमें से एक बिना इंतज़ार किए तुरंत
SQLITE_BUSYerror पाता है- disk I/O करने वाली write मूल रूप से धीमी हो सकती है, इसलिए कुछ समय lock release होने का इंतज़ार करना ज़्यादा स्वाभाविक है
- ऐसे मामले भी रहे हैं जहाँ production system बीच-बीच में रुक जाता था और manually retry loop लिखना पड़ा
- नीचे वाली setting अधिकतम 5 seconds तक lock लेने की retry करती है, और उसके बाद ही
SQLITE_BUSYलौटाती है
PRAGMA busy_timeout = 5000;
- सामान्य संचालन में सभी writes को एक ही process, और संभव हो तो एक ही thread में समेटना बेहतर है; concurrent writes तेज़ नहीं हो सकतीं
- फिर भी concurrent writes की ज़रूरत वाले वैध operational cases मौजूद हैं
sqlite3command-line tool से database को interactively साफ़-सुधारना- अलग frontend के बिना rare admin tasks के लिए scripts चलाना
- ऐसी operations default settings की वजह से अनपेक्षित
SQLITE_BUSYपैदा कर सकती हैं और चल रहे software को रोक सकती हैं
WAL और performance defaults
- SQLite सही configuration के साथ इतना तेज़ हो सकता है कि PostgreSQL या MySQL जैसे बड़े servers के कुछ काम भी संभाल सके, लेकिन इसका default performance अच्छा नहीं है
- server environment की detailed tuning के लिए Optimizing SQLite for servers देखा जा सकता है
- सबसे महत्वपूर्ण default problem यह है कि Write-Ahead Log, यानी WAL बंद रहता है
PRAGMA journal_mode = WAL;
- WAL अधिकांश स्थितियों में write speed को काफी बढ़ाता है
- नीचे वाली setting को साथ में इस्तेमाल करने पर data corruption का जोखिम बढ़ाए बिना disk sync की संख्या बहुत कम की जा सकती है
PRAGMA synchronous = NORMAL;
synchronousके सटीक behavior का सार SQLite pragma documentation में दिया गया है
year-based SQLite editions
- अगर मौजूदा defaults को अभी सीधे बदल दिया जाए, तो पुराना software टूट सकता है और users आगे के SQLite upgrades से डरने लगेंगे
- backward compatibility बनाए रखते हुए बेहतर defaults चुनने के लिए एक super pragma जोड़ा जा सकता है
PRAGMA edition = 2026;
2026edition कम-से-कम नीचे वाली settings के alias की तरह काम करेगा
PRAGMA foreign_keys = ON;
PRAGMA busy_timeout = 5000;
PRAGMA journal_mode = WAL;
PRAGMA synchronous = NORMAL;
- इस edition में नए tables के लिए
strictmode भी default के रूप में लागू होगा - यह Rust editions की तरह है, जहाँ पुराने code का behavior बचाए रखते हुए नए default behavior को explicit तरीके से चुना जाता है
- JavaScript के
"use strict";जैसे single mode के विपरीत, year-based editions समय के साथ sensible default bundles बदलने की सुविधा देती हैं - उदाहरण के लिए अगर Hctree की WAL2 2034 में main branch में आ जाए, तो भविष्य का
PRAGMA edition = 2034इस तरह configure किया जा सकता है कि वहjournal_mode = WAL2चुने - edition system ऐसा समझौता बन सकता है जिसमें पुराने programs का default behavior टूटे बिना SQLite अपने safety और performance defaults को आगे बढ़ा सके
2 टिप्पणियां
सिद्धांत रूप में यह अच्छा लगता है, लेकिन compatibility conflicts और overhead को देखते हुए मुझे लगता है कि इससे नुकसान ही होगा।
SQLite तो ऐसा DBMS है जो embedded environments, जैसे Aegis, में चलने के लिए पैदा हुआ था।
void**को धड़ाधड़ इस्तेमाल करना और__weak__घोषित करने के बाद functions को override करना—यही embedded दुनिया है। ऐसी दुनिया में इसे जानबूझकर क्यों लाना? यही ख्याल आता है।Lobste.rs की रायें
कल यह खबर देखकर कि lobste.rs ने SQLite पर स्विच किया है (https://lobste.rs/s/ko1ji1/lobste_rs_is_now_running_on_sqlite), मैंने यह पोस्ट लिखी
उस समय की चर्चा पढ़ते हुए SQLite के defaults की छोटी-छोटी असुविधाएँ याद आईं, इसलिए उन्हें ब्लॉग पोस्ट में समेट दिया
अगर क्षमता हो तो यह पोस्ट SQLite forum पर भी साझा करने लायक है: https://sqlite.org/forum/forum
SQL में
CREATE TYPEका उपयोग composite type बनाने के लिए होता है, और Postgres इसमें range, enum, array, यहाँ तक कि काफ़ी जटिल base type भी support करता हैजो फ़ीचर चाहिए वह
CREATE DOMAINके ज़्यादा क़रीब है। इससे base concrete type पर newtype बनाया जा सकता है, और default value तथा constraints भी तय किए जा सकते हैं, जिससे कई tables में एक हीCHECKconstraint दोहराने वाली schema काफ़ी संक्षिप्त हो जाती है। उदाहरण के लिए,create domain mything as text;याcreate domain mything as integer not null check (VALUE < 5);जैसा लिखा जा सकता हैयह काफ़ी ख़तरनाक trap भी कवर किया जाना चाहिए कि सिर्फ
busy_timeoutसे तुरंत आने वालीSQLITE_BUSYerror नहीं रुकतीअगर read-only query से transaction शुरू करने के बाद lock को upgrade करने के लिए write query चलाई जाए और उसी समय कोई दूसरी write operation मौजूद हो, तो
busy_timeoutsetting की परवाह किए बिना वह तुरंत fail हो जाती है। इसका समाधानbegin immediateका उपयोग करना हैयह भी ध्यान में रखना चाहिए कि प्रस्तावित edition से बँधी कुछ settings, SQLite model में connection properties हैं, जबकि बाकी database file या individual table की properties हैं
PRAGMA journal_mode = WALdatabase file में बना रहता है, लेकिनPRAGMA foreign_keys = ONहर connection पर अलग से सेट करना पड़ता है।STRICTindividual table की property है, इसलिए एक ही database में strict tables और non-strict tables को मिलाकर रखा जा सकता है। इसलिए तय करना होगा कि इस unified PRAGMA को database file की property माना जाए या connection की। यह भी जानने की जिज्ञासा है कि क्या कोई built-in mechanism है जो पुरानी library को ऐसी file बदलकर खराब करने से चेतावनी दे, जिसमें ऐसे features शामिल हों जिन्हें वह पहचानती नहींEdition approach मूल रूप से SQLite developers के backward compatibility बनाए रखने के लक्ष्य और आधुनिक recommended settings चाहने वाले users के लिए कम confusing defaults के बीच एक अच्छा compromise लगता है
हाल में मैं RocksDB में काफ़ी दिलचस्पी ले रहा हूँ। यह भी SQLite की तरह embedded database library है, लेकिन मेरे हिसाब से इसमें SQL न होना ही एक बड़ा फ़ायदा है
हर चीज़ को SQL में व्यक्त करना पड़े, यह बंधन database के साथ काम करने में सिर्फ़ रुकावट पैदा करता है
lobste.rs जैसे SQL इस्तेमाल करने वाले system को RocksDB या किसी और key-value database पर कैसे ले जाया जाएगा, यह कल्पना करना मुश्किल है
मैं इस बात से सहमत नहीं हूँ कि
busy_timeoutसेट करना ही समाधान है। SQLite, अगर single write thread रखकर आपस में असंबंधित logical transactions को batch किया जाए, तो बेहद शानदार performance देता हैइसका एक ठोस तरीका https://andersmurphy.com/2025/12/… में समझाया गया है
लेकिन maintenance के लिए database को सीधे modify करना, या ऐसे छिटपुट admin tasks को script से चलाना भी आम बात है जिनके लिए अलग management UI बनाना ज़रूरी नहीं होता। ऐसे में सिर्फ़ bad luck से execution timing टकरा जाने पर server या application का random तरीके से रुक जाना अच्छी बात नहीं है