Data model-lər və query dilləri
Data model-lər və query dilləri
Data model software-in bəlkə də ən vacib hissəsidir, çünki o, yalnız kodun necə yazıldığını deyil, problemin necə düşünüldüyünü müəyyən edir. Tipik tətbiq bir-birinin üzərinə qoyulmuş model qatlarından ibarətdir: tətbiq developer-i real dünyanı obyekt və data strukturları ilə, onları da JSON və ya cədvəllərlə ifadə edir; database mühəndisi bu cədvəlləri disk-dəki byte-larla, hardware mühəndisi byte-ları maqnit sahəsi və elektrik impulsları ilə təmsil edir. Hər qat alt qatın mürəkkəbliyini təmiz model arxasında gizlədir.
Hər model bəzi işləri asan, bəzilərini isə çətin və ya mümkünsüz edir. Buna görə application-a uyğun model seçmək böyük təsirə malikdir. Bu fəsil ümumi məqsədli üç model ailəsini, relational, document və graph modellərini, və onların query dillərini müqayisə edir. Fəsil 3 isə bu modellərin storage engine səviyyəsində necə implementasiya olunduğunu göstərir.
Relational model və NoSQL-in yaranması
Bu gün ən tanınan model 1970-ci ildə Edgar Codd-un təklif etdiyi relational modeldir: data relation-larda, yəni cədvəllərdə, nizamsız tuple-lar, yəni sətirlər, toplusu kimi saxlanılır. Model biznes data emalı üçün yaranmışdı: transaction processing və batch processing. O dövrdə rəqib modellər də var idi, amma 1980-ci illərdə relational model qalib gəldi və iyirmi beş il hökmranlıq etdi.
Relational modelin əsas uğuru yüksək səviyyəli abstraksiya idi: database-in daxili təmsilini tətbiqdən gizlədirdi. 2010-cu illərdə NoSQL hərəkatı bunu sorğuladı. Hərəkatın arxasında bir neçə ehtiyac dayanırdı: relational database-lərin çətinliklə verdiyi scalability, çox böyük data həcmi və yüksək write throughput, açıq mənbəli pulsuz alətlər, relational modeldə yaxşı dəstəklənməyən xüsusi sorğular və daha dinamik, ifadəli data model istəyi. Nəticə belədir: yaxın gələcəkdə relational database-lər NoSQL store-larla yanaşı işləyəcək, buna polyglot persistence deyilir.
Object-relational impedance mismatch
Tətbiq kodu obyekt yönümlü dillərdə yazılır. Data relational cədvəllərdə saxlanırsa, obyektlər ilə cədvəllər arasında çevirmə qatı lazımdır. Bu ayrılığa impedance mismatch deyilir. ORM framework-ləri bu çevirmənin boilerplate-ini azaldır, amma iki model arasındakı fərqi tam gizlədə bilmir.
Nümunə kimi LinkedIn profilini götürək. Hər istifadəçinin bir user_id, first_name, last_name sahəsi var, amma iş yerləri, təhsil və əlaqə məlumatları çoxdur. Bu, one-to-many əlaqədir. Relational modeldə üç yol var: ayrı positions, education cədvəlləri və user_id ilə foreign key; müasir SQL-in dəstəklədiyi structured data tipləri və ya JSON sütunlar; yaxud JSON-u mətn kimi saxlayıb tətbiqdə parse etmək.
Document model isə bütün profili bir JSON sənədində saxlayır:
{
"user_id": 251,
"first_name": "Bill",
"positions": [
{"job_title": "Co-chair", "organization": "Bill & Melinda Gates Foundation"},
{"job_title": "Co-founder, Chairman", "organization": "Microsoft"}
],
"education": [{"school_name": "Harvard University", "start": 1973, "end": 1975}]
}
Bu təmsilin üstünlüyü locality-dir: profili göstərmək üçün bir sorğu kifayətdir, cədvəllər arasında join və ya çoxlu sorğu lazım deyil. One-to-many əlaqələr tree strukturu yaradır və JSON bu tree-ni açıq şəkildə göstərir.
Many-to-one və many-to-many əlaqələr
Profildə region_id və industry_id sahələri mətn deyil, ID-dir. Niyə? Çünki ID-lər standartlaşdırılmış dəyərlər verir, dil və ad dəyişəndə yalnız bir yerdə yenilənir, axtarışı asanlaşdırır. ID-nin mənası insan üçün heç nə demir, buna görə də heç vaxt dəyişmək lazım gəlmir. Bu, normalization-ın əsas ideyasıdır: hər faktın bir yerdə saxlanması.
Normalizasiya many-to-one əlaqə yaradır: çoxlu profil bir region-a aiddir. Relational database-də bu asan join-dir. Document database-lərdə join dəstəyi zəifdir və ya yoxdur; tətbiq kodu join-i özü emulyasiya etməli olur: bütün region-ları yaddaşa yükləyib ID-ni ad ilə əvəz etmək kimi.
Problem böyüyür, çünki tətbiq inkişaf etdikcə əlaqələr mürəkkəbləşir. "Organization" sadə mətn deyil, öz səhifəsi olan entity-yə çevrilir; istifadəçilər bir-birini tövsiyə edir. Bu, many-to-many əlaqədir və document modeldə sənədlər arasında reference-lər tələb edir, yəni yenə join lazımdır.
Tarixin təkrarı: hierarchical və network modellər
Bu mübahisə yeni deyil. 1970-ci illərdə IBM-in IMS sistemi hierarchical model istifadə edirdi: bütün data bir tree-dir, hər qeydin tam bir valideyni var. Bu, bugünkü JSON sənədlərinə çox oxşayır. IMS one-to-many ilə yaxşı işləyirdi, many-to-many-də isə çətinlik çəkirdi: ya data-nı dublikat etmək, ya əl ilə reference saxlamaq lazım gəlirdi.
Həlledici iki təklif gəldi. CODASYL komitəsinin network model-i qeydə bir neçə valideyn icazə verirdi; qeydlər arasında keçid pointer-lərlə idi və sorğu "access path"-i izləməklə, yəni linked list-də gəzməklə, yazılırdı. Bu, data-nı dəyişməyi və tətbiq kodunu yazmağı çətinləşdirirdi: hər sorğu üçün proqramçı yolu əvvəlcədən planlaşdırmalı idi.
Relational model isə bunları aradan qaldırdı: data sadəcə cədvəldir, istənilən sətir seçilə bilər, access path-i isə query optimizer avtomatik seçir. Yeni index əlavə edəndə sorğuları dəyişmək lazım deyil. Bu, document database-lərin bugünkü vəziyyətinə bənzəyir: one-to-many üçün locality yaxşıdır, many-to-many üçün isə hierarchical model-in problemləri qayıdır. Document database-lər CODASYL-in access path problemini qaytarmadı, çünki reference-lər pointer deyil, ID-dir və sorğu ilə həll olunur.
Relational və document model bu gün
Hansı model tətbiq kodunu sadələşdirir? Cavab data-nın formasından asılıdır. Data sənəd kimi, yəni one-to-many tree kimi görünürsə və adətən tam yüklənirsə, document model təbii gəlir; relational shredding, yəni sənədi cədvəllərə parçalamaq, mürəkkəblik gətirir. Many-to-many əlaqələr çoxdursa, document modeldə join emulyasiyası tətbiq kodunu və performansı pisləşdirir; relational model daha uyğundur. Yüksək əlaqəli data üçün isə graph model ikisindən də yaxşıdır.
İkinci fərq schema-dır. Document database-lər schema-on-read-dir: struktur yazılan zaman yoxlanılmır, oxuyan kod onu şərh edir. Relational database-lər schema-on-write-dir: database schema-nı tətbiq edir. Bu, dillərdə dynamic və static type checking fərqinə bənzəyir. Fərq schema dəyişəndə üzə çıxır. Tutaq ki, name sahəsini first_name və last_name-ə bölmək lazımdır. Document modeldə sadəcə yeni sənədlər yeni formatda yazılır, köhnələri oxuyan kod isə:
if user.first_name is None:
user.first_name = user.name.split(" ")[0]
Relational database-də migration tələb olunur:
ALTER TABLE users ADD COLUMN first_name text;
UPDATE users SET first_name = split_part(name, ' ', 1);
ALTER TABLE əksər sistemlərdə millisaniyələrlə çəkir; UPDATE isə böyük cədvəldə yavaşdır. Schema-on-read heterogen data üçün faydalıdır: sənədlərin formaları fərqlidirsə və ya xarici sistemlərdən gəlirsə. Data vahiddirsə, schema sənədləşdirmə və yoxlama üçün faydalıdır.
Üçüncü fərq locality-dir. Bir sənəd disk-də davamlı byte zənciri kimi saxlanırsa, tam oxumaq sürətlidir. Amma yalnız bir sahəsi lazımdırsa, bütün sənəd oxunur; dəyişiklik isə çox vaxt bütün sənədi yenidən yazmağı tələb edir. Buna görə sənədləri kiçik saxlamaq və ölçüsünü artıran yazmalardan qaçmaq tövsiyə olunur. Locality document-ə məxsus deyil: Google-un Spanner-i və Oracle-un multi-table index cluster-ləri eyni ideyani relational modeldə tətbiq edir.
Praktikada modellər yaxınlaşır. Relational database-lərin çoxu JSON sütunları, onların üzərində index-ləri və sorğuları dəstəkləyir; bəzi document database-lər isə client-side join-ə bənzər əməliyyatlar əlavə edib. Hibrid model gələcəyin yoludur.
Query dilləri: declarative və imperative
Relational modelin gətirdiyi ikinci yenilik declarative query dilidir. SQL-də nə istədiyinizi yazırsınız: hansı şərtlər, hansı sütunlar, hansı sıra. Necə alınacağını isə query optimizer həll edir. Imperative kodda isə sıra ilə əməliyyatlar yazılır:
function getSharks() {
var sharks = [];
for (var i = 0; i < animals.length; i++) {
if (animals[i].family === "Sharks") sharks.push(animals[i]);
}
return sharks;
}
SELECT * FROM animals WHERE family = 'Sharks';
Declarative dilin iki üstünlüyü var. Birincisi, qısa və işlək olmaqla yanaşı implementasiya detallarını gizlədir: database index əlavə edə, storage layout-u dəyişə bilər, sorğu eyni qalır. İkincisi, imperative kod əməliyyatların sırasını müəyyən edir və buna görə paralelləşdirmək çətindir; declarative sorğuda sıra təyin olunmur, database onu çoxlu core-da paralel işlədə bilər. Eyni fərq web-də CSS ilə JavaScript DOM manipulyasiyası arasındadır: CSS-də "seçilmiş element-in fonunu mavi et" yazılır, JavaScript-də isə bütün element-lər gəzilib şərt yoxlanılır.
MapReduce sorğulama
Bəzi NoSQL store-lar MapReduce-u məhdud sorğu dili kimi dəstəkləyir. O, nə tam declarative, nə də tam imperative-dir: map və reduce funksiyaları kod parçalarıdır, amma onların arasında framework yerləşir. Məsələn, aylıq köpəkbalığı müşahidələrini saymaq üçün map hər sənəddən (ay, say) cütü emit edir, reduce isə eyni ay üçün dəyərləri toplayır. Bu funksiyalar pure olmalıdır: yalnız verilən data ilə işləyir, əlavə sorğu etmir, yan təsiri yoxdur. Bu, onları istənilən sırada, istənilən maşında işlətməyə imkan verir.
Problem odur ki, əlaqəli iki funksiya yazmaq tək declarative sorğudan daha çox iş tələb edir və optimizer onların içini görmür. Buna görə çox sistem sonradan declarative aggregation pipeline əlavə etdi. Fəsil 10-da MapReduce-un batch processing framework kimi istifadəsinə ətraflı qayıdacağıq.
Graph modellər
Many-to-many əlaqələr adi haldırsa və data getdikcə daha əlaqəli olursa, onu graph kimi modelləşdirmək təbiidir: vertex-lər və edge-lər. Sosial şəbəkə, veb-də səhifələr və link-lər, yol şəbəkəsi, dəzgahlar və hissələr. Graph-lar homogen olmaq məcburiyyətində deyil: Facebook-un graph-ında insanlar, yerlər, hadisələr və comment-lər eyni graph-ın vertex-ləridir.
Property graph modeldə hər vertex-in unikal ID-si, çıxan və gələn edge dəsti və key-value property-ləri var; hər edge-in ID-si, başlanğıc və son vertex-i, əlaqə tipini bildirən label-i və property-ləri var. Bu model relational cədvəllərlə də ifadə oluna bilər:
CREATE TABLE vertices (vertex_id integer PRIMARY KEY, properties json);
CREATE TABLE edges (
edge_id integer PRIMARY KEY,
tail_vertex integer REFERENCES vertices(vertex_id),
head_vertex integer REFERENCES vertices(vertex_id),
label text, properties json);
Vertex-lər arasında istənilən tipli edge ola bilər; verilən vertex-in bütün edge-lərini tapmaq asandır və graph-ı traverse etmək mümkündür. Fərqli edge label-ləri bir graph-da müxtəlif növ məlumatı saxlayır; model evolvable-dır: yeni funksiya üçün sadəcə yeni edge tipi əlavə olunur.
Cypher kimi graph query dillərində traversal nümunə kimi yazılır: "ABŞ-da doğulub Avropada yaşayan insanları tap". Sorğu vertex-dən çıxıb WITHIN edge-ləri ilə şəhərdən ştata, oradan ölkəyə keçən yolu *0.. operatoru ilə, yəni istənilən sayda addımla, təsvir edir. Eyni sorğunu SQL-də recursive common table expression ilə yazmaq mümkündür, amma 4 sətirlik Cypher 29 sətirlik SQL-ə çevrilir. Data model-in bəzi sorğuları asan, bəzilərini çətin etdiyinin ən aydın nümunəsidir.
Triple-store model property graph-a bərabərdir, amma data üç hissəli ifadələr kimi saxlanılır: subject, predicate, object. "Jim likes bananas" belə triple-dir. Object ya primitive dəyər, ya başqa vertex-dir; predicate birinci halda property, ikinci halda edge-dir. Bu modelin query dili SPARQL-dir və semantic web ideyasından gəlir. Triple-lar üzərində daha köhnə Datalog dili isə qaydalar yazmağa imkan verir: within_recursive(Location, Name) kimi predicate-lər digər predicate-lərlə ifadə olunur və mürəkkəb sorğular kiçik qaydalardan toplanır.
Modelləri müqayisə
| Meyar | Relational | Document | Graph |
|---|---|---|---|
| Təbii data forması | Cədvəllər, many-to-one və many-to-many | One-to-many tree, özündə tam sənəd | Hər şey hər şeylə əlaqəli |
| Schema | Schema-on-write | Schema-on-read | Çevik, yeni edge tipi asan |
| Join | Güclü, optimizer həll edir | Zəif, tətbiq emulyasiya edir | Traversal modelin özüdür |
| Locality | Sətir səviyyəsində, join-lərlə dağınıq | Sənəd bütöv saxlanır | Vertex-lər pointer və ya ID ilə |
| Çətin sorğu | Dərin hierarchy, dəyişən dərinlikli traversal | Many-to-many, cross-document | Bütün cədvəli skan edən aggregation |
| Uyğun nümunə | Sifariş, mühasibat, inventar | Profil, məqalə, event log | Sosial graph, bilik qrafı, yol şəbəkəsi |
Modellərin heç biri digərini əvəz etmir. Çox vaxt tətbiq bir neçə modeli eyni anda istifadə edir və Fəsil 12-də göründüyü kimi, eyni data-nı fərqli sistemlərdə fərqli formada saxlamaq normal hala çevrilir.
Tapşırıqlar
- Onlayn mağazanın data-sını götürün: müştəri, sifariş, məhsul, kateqoriya, rəy. Hansı əlaqələr one-to-many, hansılar many-to-many-dir? Document modeldə hansı sənəd sərhədlərini seçərdiniz və hansı sorğular join emulyasiyası tələb edərdi?
- "İstifadəçinin dostlarının dostlarının bəyəndiyi məhsullar" sorğusunu relational SQL ilə və property graph üçün nümunə əsaslı dil ilə yazın. Hər versiyada neçə join və ya traversal var? Dərinlik 2-dən n-ə dəyişəndə hansı versiya dəyişmir?
- Schema-on-write ilə işləyən sistemdə milyard sətirli cədvələ yeni sütun əlavə edib köhnə sətirləri doldurmalısınız. Downtime olmadan bunu necə edərdiniz? Eyni dəyişikliyi schema-on-read sistemində etsəniz, mürəkkəblik hara köçür?