SQLite Deep Dive (Part-2) — Data Types & Type Affinity

SQLite ရဲ့ type system ဟာ တခြား SQL database တွေနဲ့ ဘာကြောင့် မတူတာလဲ ၊ dynamic typing နဲ့ type affinity rule တွေဆိုတာဘာလဲ၊။

SQLite Deep Dive.png

တခြား SQL Database တွေ (ဥပမာ- PostgreSQL, MySQL) မှာ Column တစ်ခုကို INTEGER လို့ သတ်မှတ်လိုက်ရင် အဲဒီထဲကို “Hello” ဆိုတဲ့ String ထည့်လို့ မရပါဘူး။ ဒါပေမဲ့ SQLite မှာတော့ ဒါဟာ ဖြစ်နိုင်ပါတယ်။ ဘာကြောင့်လဲ? ဘာဖြစ်လို့ SQLite က တခြားသူတွေနဲ့ မတူဘဲ ဒီလို ဒီဇိုင်းဆွဲထားတာလဲ?

INTEGER Column ကြေညာရင် Integer ပဲ ထည့်ရမယ်၊ VARCHAR(255) ဆိုရင် String ပဲ ရမယ်။ Database ဟာ တင်းကြပ်တဲ့ Gatekeeper တစ်ယောက်လို ပြုမူတယ်လို့ ကျွန်တော်တို့ သင်ယူခဲ့ရပါတယ်။ ဒါပေမဲ့ SQLite ကတော့ ဒီစည်းမျဉ်းတွေ အကုန်လုံးကို ဖျက်ဆီးပစ်လိုက်ပါတယ်။

SQLite ရဲ့ ဒီ Flexible ဖြစ်လွန်းတဲ့ System က ဘယ်လောက်တောင် Elegant ဖြစ်သလဲဆိုတာ သိသွားရင် Column Declaration တွေကို ကြည့်တဲ့ အမြင်ဟာ တစ်သက်လုံး ပြောင်းလဲသွားပါလိမ့်မယ်။

လူတိုင်းကို ပထမဆုံး အံ့ဩစေတဲ့အရာ

SQLite မှာ ဒါကို တကယ် Run ကြည့်လိုက်ပါ။ Error လည်း တက်မှာမဟုတ်သလို Warning လည်း ပြမှာ မဟုတ်ပါဘူး။

CREATE TABLE test (id INTEGER, name TEXT, score REAL);
INSERT INTO test VALUES ('hello', 42, 'not a number');
SELECT * FROM test;

အလုပ်လုပ်သွားပါတယ်။ INTEGER ထဲကို String ထည့်တယ်၊ TEXT ထဲကို Integer ထည့်တယ်၊ REAL ထဲကို String ထည့်တယ်။ SQLite ကတော့ ဘာမှမညည်းညူဘဲ ပျော်ပျော်ရွှင်ရွှင်ပဲ သိမ်းဆည်းပေးလိုက်ပါတယ်။

PostgreSQL မှာဆိုရင် ဒါဟာ ချက်ချင်း Error တက်မှာဖြစ်ပြီး၊ MySQL မှာဆိုရင် တိတ်တဆိတ် Coerce လုပ်ရင်း Data တွေ ပျက်စီးသွားနိုင်ပါတယ်။ SQLite မှာတော့ ဒါဟာ Bug မဟုတ်ပါဘူး။ ဒါဟာ Dynamic Typing လို့ ခေါ်တဲ့ Fundamental Design Decision တစ်ခုပဲ ဖြစ်ပါတယ်။


Type တွေမဟုတ်ဘဲ Storage Class တွေ

ဒီနေရာမှာ အဓိကကျတဲ့ Mental Shift (အတွေးအခေါ်အပြောင်းအလဲ) တစ်ခု လိုအပ်ပါတယ်။ SQLite ဟာ Column Type တွေနဲ့ မတွေးပါဘူး။ သူက Storage Class တွေနဲ့ပဲ အလုပ်လုပ်တာပါ။ ထူးခြားချက်က အဲဒီ Storage Class တွေဟာ Column တွေက ပိုင်ဆိုင်တာမဟုတ်ဘဲ Value ကိုယ်တိုင်က ပိုင်ဆိုင်တာ ဖြစ်ပါတယ်။

SQLite မှာ Storage Class စုစုပေါင်း (၅) မျိုး တိတိ ရှိပါတယ်-

ဒါဟာ SQLite ထဲမှာ ရှိနိုင်သမျှ List အပြည့်အစုံပါပဲ။ SQLite database တိုင်းထဲက Value တိုင်းဟာ ဒီ ၅ မျိုးထဲက တစ်မျိုးမျိုးပဲ ဖြစ်ရပါမယ်။ ကျွန်တော်တို့ ရေးလိုက်တဲ့ Column Declaration တွေဖြစ်တဲ့ INTEGER, VARCHAR(255), BOOLEAN, DATETIME ဆိုတာတွေဟာ Value တစ်ခု ဘယ် Storage Class ရမလဲဆိုတာကို တင်းတင်းကျပ်ကျပ် ကန့်သတ်ဖို့ မဟုတ်ပါဘူး။ အဲဒါတွေဟာ Type Affinity ဆိုတဲ့ Process ကို လွှမ်းမိုးဖို့အတွက်ပဲ ဖြစ်ပါတယ်။ တကယ်တမ်း Complexity တွေ ရှိနေတဲ့နေရာကလည်း ဒီ Type Affinity ပဲ ဖြစ်ပါတယ်။


Type Affinity: Flexibility ရဲ့ နောက်ကွယ်က System

SQLite က Column Declaration တစ်ခုကို မြင်တဲ့အချိန်မှာ Type Name ကို Literal အမှတ်အသားပြုပြီး သိမ်းဆည်းတာ မဟုတ်ပါဘူး။ အဲဒီအစား Affinity (၅) မျိုးထဲက တစ်ခုခုနဲ့ Map လုပ်လိုက်ပါတယ်။ ဒါတွေကတော့ TEXT၊ NUMERIC၊ INTEGER၊ REAL နဲ့ BLOB (NONE လို့လည်း ခေါ်ပါတယ်) တို့ပဲ ဖြစ်ပါတယ်။

ဒီ Affinity ဆိုတာ Preference တစ်ခုသာဖြစ်ပြီး Constraint မဟုတ်ပါဘူး။ SQLite ကို ဒီလို Message ပေးလိုက်တာနဲ့ တူပါတယ်။“ဒီ Column အတွက် Value တစ်ခုရလာရင် ဖြစ်နိုင်မယ်ဆိုရင်တော့ အကြံပြုထားတဲ့ Storage Class မျိုးဖြစ်အောင် Coerce လုပ်ကြည့်ပေးပါ။ တကယ်လို့ Coercion လုပ်လို့ အဆင်မပြေဘူးဆိုရင်တော့ (ဥပမာ- INTEGER affinity ရှိတဲ့ Column ထဲ “hello” လို့ ထည့်လိုက်ရင်) မပြောင်းလဲတော့ဘဲ ပေးတဲ့အတိုင်းပဲ သိမ်းလိုက်ပါ” ဆိုတာပါပဲ။

Mapping Rule တွေကတော့ တော်တော်လေး စိတ်ဝင်စားဖို့ကောင်းပါတယ်။ ကြေညာထားတဲ့ Type Name ကို စစ်ဆေးပြီး Column ရဲ့ Affinity ကို SQLite က အောက်ပါအတိုင်း ဆုံးဖြတ်ပါတယ်-

NUMERIC Affinity ဟာ အသုံးပြုသူတွေကို အံ့အားသင့်စေဆုံး Affinity ပါပဲ။ သူက Opportunistic ဖြစ်ပါတယ်။ NUMERIC column ထဲကို “3.14” ဆိုတဲ့ String ထည့်ရင် SQLite က REAL value 3.14 အဖြစ် အလိုလို Convert လုပ်ပေးပါတယ်။ “7” ဆိုတဲ့ String ထည့်ရင် INTEGER 7 ဖြစ်သွားပါတယ်။ ဒါပေမဲ့ “hello” လိုမျိုး ကိန်းဂဏန်းအဖြစ် Cleanly Convert မလုပ်နိုင်တဲ့ Text မျိုးဆိုရင်တော့ ဘာမှမပြောင်းလဲတော့ဘဲ TEXT အဖြစ်ပဲ ဆက်လက်သိမ်းဆည်းပါတယ်။ ဒီ Conversion ဟာ အခွင့်သာမှ လုပ်ဆောင်တာဖြစ်ပြီး အတင်းအကျပ် လုပ်ဆောင်တာမျိုး မဟုတ်ပါဘူး။


လက်တွေ့မှာ Coercion Flow ဘယ်လိုဖြစ်ပေါ်သလဲ

Column တစ်ခုထဲကို Value တစ်ခု Insert လုပ်တဲ့အချိန်မှာ နောက်ကွယ်မှာ အမှန်တကယ် ဖြစ်ပျက်နေတဲ့ အဆင့်ဆင့်ကို လေ့လာကြည့်ရအောင်-

  1. Value ပေးပို့ခြင်း: အသုံးပြုသူက Value တစ်ခု ပေးပို့လိုက်တယ် (ရိုက်ထည့်ပုံပေါ်မူတည်ပြီး ကိုယ်ပိုင် Storage Class ရှိနှင့်ပြီးသားပါ)။

  2. Affinity စစ်ဆေးခြင်း: SQLite က သက်ဆိုင်ရာ Column ရဲ့ Affinity ကို စစ်ဆေးပါတယ်။

  3. Coercion ပြုလုပ်ခြင်း: တကယ်လို့ Coercion လုပ်လို့ရတယ် (သဘောကျတယ်) ဆိုရင် → Preferred Storage Class ထဲကို Convert လုပ်ပြီး သိမ်းဆည်းပါတယ်။

    Coercion လုပ်လို့ မရဘူး (သဘောမကျဘူး) ဆိုရင် → ပေးလိုက်တဲ့အတိုင်းပဲ မပြောင်းလဲဘဲ သိမ်းဆည်းပါတယ်။

  4. သိမ်းဆည်းခြင်း: Value ကို Column ရဲ့ Declared Type နဲ့ မဟုတ်ဘဲ Actual Storage Class နဲ့ပဲ သိမ်းဆည်းလိုက်ပါတယ်။

ဒီ Flow မှာ အထူးသတိပြုရမှာက Rejection လုံးဝမရှိတာပါပဲ။ SQLite ဟာ Standard Column တစ်ခုမှာ Insert လုပ်တဲ့အတွက် Type Error ဆိုတာကို ဘယ်တော့မှ Throw မလုပ်ပါဘူး။ အောင်မြင်စွာ Coerce လုပ်နိုင်ရင်လုပ်၊ မလုပ်နိုင်ရင် ပေးတဲ့အတိုင်း သိမ်းပါတယ်။

ဒါကြောင့်မို့လို့ Column တစ်ခုတည်းမှာရှိတဲ့ Row နှစ်ကြောင်းဟာ လုံးဝကွဲပြားတဲ့ Storage Class တွေကို ကိုင်ဆောင်ထားနိုင်တာ ဖြစ်ပါတယ်။

CREATE TABLE mixed (val NUMERIC);
INSERT INTO mixed VALUES (1);        -- INTEGER အဖြစ် သိမ်းတယ်
INSERT INTO mixed VALUES (1.5);      -- REAL အဖြစ် သိမ်းတယ်
INSERT INTO mixed VALUES ('hello');  -- TEXT အဖြစ် သိမ်းတယ်
INSERT INTO mixed VALUES (NULL);     -- NULL အဖြစ် သိမ်းတယ်
INSERT INTO mixed VALUES (x'ff');    -- BLOB အဖြစ် သိမ်းတယ်

ဒါတွေအားလုံးဟာ valid ဖြစ်ပါတယ်။ အကုန်လုံးက same column ထဲမှာပဲ တည်ရှိနေတာပါ။


လူတိုင်းကို အခက်တွေ့စေတတ်သော Edge Cases

SQLite ရဲ့ ပျော့ပြောင်းလွန်းတဲ့ Type System ကြောင့် အခြား SQL Database တွေကနေ ပြောင်းလဲအသုံးပြုလာသူတွေအနေနဲ့ အောက်ပါ အခြေအနေတွေမှာ အမှားအယွင်း ဖြစ်ပေါ်တတ်ပါတယ်။

DATETIME ဆိုတဲ့ Storage Class လည်း မရှိပါဘူး။ DATETIME လို့ ကြေညာရင် NUMERIC affinity ပဲ ရပါတယ်။ Date တွေကို အောက်ပါ Format (၃) မျိုးထဲက ကြိုက်ရာနဲ့ သိမ်းဆည်းနိုင်ပါတယ်-


ဒီ Design ကို ဘာကြောင့် ရွေးချယ်ခဲ့တာလဲ

ဒီအချက်အလက်အားလုံးကို ခြုံငုံကြည့်ပြီး ဒါဟာ Engineering ပိုင်း အားနည်းချက်တစ်ခုလားလို့ ထင်မှတ်မှားစရာ ရှိပါတယ်။ ဒါပေမယ့်လည်း ဒီဇိုင်းရဲ့ နောက်ကွယ်မှာ ခိုင်မာတဲ့ Principle ရှိတဲ့ အကြောင်းရင်းတစ်ခု ရှိနေပါတယ်။ SQLite ရဲ့ ဖန်တီးရှင် Richard Hipp က ဒီချဉ်းကပ်ပုံကို “Manifest Typing” လို့ ခေါ်ဆိုပြီး Type ဆိုတာ Container (Column) ရဲ့ Property မဟုတ်ဘဲ Value ကိုယ်တိုင်ရဲ့ Property ဖြစ်တယ်လို့ ယူဆပါတယ်။ ဒါဟာ Python၊ JavaScript နဲ့ Ruby တို့လို Dynamic Type ရှိတဲ့ Programming Language အတော်များများ အလုပ်လုပ်ပုံနဲ့ အလွန်နီးစပ်ပါတယ်။ အဲဒီ Language တွေမှာ Variable တွေမှာ Type မရှိဘဲ Value တွေမှာပဲ Type ရှိတာကြောင့် SQLite ရဲ့ Type System ဟာ ဒီ Language တွေနဲ့ သဘာဝကျကျ Interface ဖြစ်စေဖို့ ရည်ရွယ်ပြီး ဒီဇိုင်းဆွဲထားတာ ဖြစ်ပါတယ်။

Interoperability အတွက်လည်း လက်တွေ့ကျတဲ့ အကျိုးကျေးဇူးတွေ ရှိပါတယ်။ SQLite ဟာ Data တွေကို အဆက်မပြတ် ထည့်သွင်းထုတ်ယူနေတဲ့ Application တွေထဲမှာ Embed ဖြစ်ဖို့ ဖန်တီးထားတာပါ။ ဥပမာအားဖြင့် Python App တစ်ခုကနေ SQLite ဆီ Value တစ်ခု Pass လုပ်လိုက်တဲ့အခါ အဲဒီ Value ဟာ Python Type (String, Int, Float, None) အဖြစ်နဲ့ ရောက်ရှိလာပါတယ်။ Strict SQL Typing သာ သုံးထားရင် Boundary တိုင်းမှာ Type Translation တွေ အဆင့်ဆင့် လုပ်နေရမှာဖြစ်ပေမဲ့ Manifest Typing ကတော့ ဒီ Friction တွေကို အကုန်ကျော်ဖြတ်ပေးလိုက်ပါတယ်။

ဒါ့အပြင် CSV ဖိုင်တွေ၊ Scraped Data တွေ ဒါမှမဟုတ် Log Output တွေလိုမျိုး Type တသမတ်တည်း မရှိနိုင်တဲ့ Source တွေကနေ Data Import လုပ်ပြီး သိမ်းဆည်းဖို့အတွက် SQLite ကို ဒါကပဲ ကောင်းကောင်း Enable လုပ်ပေးပါတယ်။ ကြိုတင်ပြီး Value Format တိုင်းကို လိုက်လံမှန်းဆနေရတဲ့ Schema မျိုး မလိုအပ်ဘဲ Schema မရှိတဲ့ Data တွေကိုပါ စိတ်ချလက်ချ သိမ်းဆည်းနိုင်စေတာ ဖြစ်ပါတယ်။


လက်တွေ့အသုံးချမှုတွင် သတိပြုရန်များ

နေ့စဉ် SQLite ကို အသုံးပြုနေတဲ့အချိန်မှာ ဒီ Type System ရဲ့ ထူးခြားချက်တွေကို သိသာထင်ရှားစွာ မြင်တွေ့ရခဲပါတယ်။ ဥပမာအားဖြင့် CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT, created_at TEXT) လို့ ရေးသားပြီး Insert လုပ်တဲ့အခါမှာ မိမိတို့ ထည့်သွင်းလိုက်တဲ့ Data တွေအပေါ် ဂရုစိုက်မှုရှိနေသရွေ့တော့ SQLite ဟာ မျှော်လင့်ထားတဲ့အတိုင်းပဲ ပုံမှန်အလုပ်လုပ်ပေးသွားမှာ ဖြစ်ပါတယ်။

ဒါပေမယ့်လည်း ဒီ Type System ဟာ အောက်ပါ အခြေအနေ (၃) မျိုးမှာတော့ သိသိသာသာ ထင်ရှားလာပြီး အန္တရာယ်ရှိတဲ့ အမှားအယွင်းတွေကို ဖြစ်ပေါ်စေနိုင်ပါတယ်။ ပထမတစ်ခုကတော့ External Source တွေကနေ Data Import လုပ်နေတဲ့ အခြေအနေမျိုးဖြစ်ပြီး Type တွေဟာ Column Declaration အတိုင်း အတိအကျ ရောက်ရှိလာလိမ့်မယ်လို့ ယုံကြည်ထားရင် အမှားအယွင်း ကြုံရနိုင်ပါတယ်။ ဒါကြောင့် Data တွေ သွင်းပြီးတဲ့အခါမှာ SELECT typeof(column_name) FROM table နဲ့ အမြဲတမ်း စစ်ဆေးသင့်ပါတယ်။

ဒုက္ခပေးနိုင်တဲ့ ဒုတိယအချက်ကတော့ Mixed-type Value တွေ ရှိနေနိုင်တဲ့ Column တစ်ခုမှာ Comparison ဒါမှမဟုတ် Sorting လုပ်တဲ့အခါမှာ ဖြစ်ပါတယ်။ ORDER BY ဒါမှမဟုတ် WHERE Clause တွေက ထွက်လာမယ့် Result တွေကို မယုံကြည်ခင်မှာ လက်ရှိ Data တွေရဲ့ Actual Storage Class တွေက ဘာလဲဆိုတာကို အရင်စစ်ဆေးဖို့ လိုအပ်ပါတယ်။ နောက်ဆုံး တတိယအချက်အနေနဲ့ကတော့ SQLite ပေါ်မှာ Tooling တွေ ဒါမှမဟုတ် ORM တွေ တည်ဆောက်နေပြီး SQLite က လုံးဝမပေးနိုင်တဲ့ Type Enforcement (တင်းကြပ်စွာ ကန့်သတ်ခြင်း) အကြောင်း Assumption တွေ မှားယွင်းစွာ ပြုလုပ်မိနေတဲ့အခါမှာ ဖြစ်ပါတယ်။

ဒီနေရာမှာ typeof() function ဟာ အကောင်းဆုံး မိတ်ဆွေဖြစ်လာပါတယ်။ သူဟာ Value တစ်ခုချင်းစီရဲ့ Storage Class ဖြစ်တဲ့ “integer”၊ “real”၊ “text”၊ “blob” ဒါမှမဟုတ် “null” ဆိုတာတွေကို အမှန်အတိုင်း ပြန်လည်ဖော်ပြပေးနိုင်ပါတယ်။ Application ရဲ့ Behavior တစ်ခုခု မှားယွင်းနေတယ်လို့ ယူဆရတဲ့အခါတိုင်း typeof() ကို အသုံးပြုခြင်းဖြင့် SQLite က အဆိုပါ Data ကို တကယ်တမ်း ဘယ်လိုပုံစံနဲ့ ကိုင်တွယ်ထားတယ်ဆိုတာကို အဖြေရှာနိုင်မှာ ဖြစ်ပါတယ်။


Overall

Type Affinity ဆိုတာ SQLite ရဲ့ Philosophy ကို ပထမဆုံးအကြိမ် ဖွင့်ပြလိုက်တဲ့ နေရာတစ်ခုပဲ ဖြစ်ပါတယ်။ အဲဒီခံယူချက်ကတော့ “Programmer ကို ယုံကြည်ပါ၊ Friction ကို လျှော့ချပါ၊ ပေးတဲ့အတိုင်း သိမ်းဆည်းပါ” ဆိုတာပါပဲ။ ဒါဟာ တကယ်တော့ Trade-off တစ်ခု လုပ်ထားခြင်း ဖြစ်ပါတယ်။ Flexibility နဲ့ Interoperability ကို ရရှိအောင် တည်ဆောက်ထားတဲ့အတွက် တခြား Database တွေမှာပါရှိတဲ့ Strict Typing ဆိုတဲ့ Guardrail တွေကိုတော့ လက်လွှတ်လိုက်ရတာ ဖြစ်ပါတယ်။

ဒါဟာ မှားယွင်းတဲ့ ချဉ်းကပ်မှုလို့ မဆိုနိုင်ပါဘူး။ မတူညီတဲ့ ရည်ရွယ်ချက်တွေအတွက် Optimize လုပ်ထားခြင်းသာ ဖြစ်ပါတယ်။ PostgreSQL ရဲ့ INTEGER column ဟာ Contract တစ်ခုလိုပါပဲ။ Integer မဟုတ်တဲ့ ဘာ Data မျိုးကိုမှ လုံးဝ ဝင်ခွင့်မပေးပါဘူး။ SQLite ရဲ့ INTEGER column ကတော့ Preferenceတစ်ခုသာ ဖြစ်ပါတယ်။ Integer ဖြစ်တာကို ပိုသဘောကျပေမဲ့ Database ဘက်ကတော့ ဘာ Data ပဲပေးပေး လက်ခံပြီး နေရာချပေးဖို့ အသင့်ရှိနေပါတယ်။

ဒီလို Distinction ကို အပြည့်အဝ နားလည်သွားတဲ့အခါ SQLite ရဲ့ Behavior တော်တော်များများဟာ သဘာဝကျကျနဲ့ Sense ရှိလာပါလိမ့်မယ်။ Query Planner တွေ၊ Index System တွေနဲ့ နောက်လာမယ့် Part 3 မှာ ပြောပြမယ့် NULL Handling တွေအထိ အားလုံးဟာ ဒီ Root Philosophy တစ်ခုတည်းကနေပဲ ဆင်းသက်လာတာ ဖြစ်ပါတယ်။

Part 3 မှာတော့ NULL အကြောင်းကို ပိုပြီး နက်နက်နဲနဲ ဆွေးနွေးသွားပါမယ်။ SQLite ရဲ့ NULL Handling ဟာ System တစ်ခုလုံးမှာ အမှားဆုံး နားလည်ထားကြတဲ့ အပိုင်းဖြစ်သလို၊ အကျိုးသက်ရောက်မှု အကြီးမားဆုံး အပိုင်းလည်း ဖြစ်ပါတယ်။ Three-valued logic တွေ၊ NULL ကို တိတ်တဆိတ် ကျော်သွားတတ်တဲ့ Index တွေနဲ့ ဝါရင့် SQL Developer တွေကိုတောင် အံ့သြသွားစေမယ့် Aggregate Function တွေအကြောင်းကို အသေးစိတ် ကြည့်ကြမှာပါ။

SQLite Deep Dive ဆိုတာ SQLite ကို Storage Layer ကနေ အပေါ်ကို တဖြည်းဖြည်း တက်ပြီး လေ့လာသွားတဲ့ Series တစ်ခု ဖြစ်ပါတယ်။ ဘယ်လို သုံးရမလဲဆိုတာထက် ဘာကြောင့် ဒီလို ဖြစ်နေရတာလဲဆိုတာကို သိချင်တဲ့ Developer တွေအတွက် ရည်ရွယ်ပြီး ဆက်လက် တင်ဆက်ပေးသွားမှာ ဖြစ်ပါတယ်။