Arhitectura datelor și a sistemelor, de la zero Lecția 17 / 80

Baze de date relaționale: când SQL este răspunsul potrivit

Postgres ca alegere implicită. ACID, scheme, joins, platforma stabilă de 20 de ani care alimentează discret majoritatea sistemelor tranzacționale ale lumii.

Bun venit la Modulul 3. Modulul anterior a fost despre ce se schimbă din momentul în care încetezi să fii pe o singură mașină: rețelele eșuează, ceasurile sunt în dezacord, consensul e scump, exactly-once delivery e mai mult o afirmație de marketing pe care idempotency o salvează. Cu acele fundații puse, putem în sfârșit să vorbim despre stratul de date cu vocabular de adult.

Modulul 3 acoperă familiile majore de stocare a datelor, un model per lecție, și le leagă în lecția 24 sub umbrela polyglot persistence. Începem de unde încep, rămân, și unde ar trebui să fie alegerea implicită majoritatea echipelor în 2026: baza de date relațională. La sfârșitul acestei lecții ar trebui să știi de ce Postgres este răspunsul corect și plictisitor pentru cea mai mare parte a workload-urilor tranzacționale, ce promite de fapt ACID, unde modelul relațional e nepotrivit, și de ce „am depășit Postgres” e o propoziție care ar trebui să te facă suspicios înainte s-o accepți.

Modelul relațional, pe scurt

Edgar F. Codd a publicat „A Relational Model of Data for Large Shared Data Banks” în 1970. Lucrarea argumenta că datele ar trebui stocate ca relations (mulțimi de tuples cu atribute denumite), că operațiile ar trebui exprimate într-un limbaj declarativ în loc de a parcurge pointeri, și că aranjamentul fizic ar trebui ascuns de aplicație. Acea lucrare are acum 56 de ani. Vocabularul s-a schimbat (tabele, rânduri și coloane în loc de relations, tuples și atribute), dar modelul e același care e în producție astăzi.

Piesele pe care le folosești efectiv: tabele cu scheme fixe de coloane tipizate; rânduri cu o valoare per coloană; primary keys care identifică un rând în mod unic; foreign keys care arată către un primary key altundeva cu integritate referențială impusă de motor; joins care combină rânduri pe condiții de potrivire, cu query planner-ul care decide algoritmul; indexuri (mai ales B-trees, uneori hash, GIN, GiST, BRIN) pentru lookup-uri rapide; și SQL, limbajul declarativ de query.

Modelul a fost dominant peste cincizeci de ani dintr-un singur motiv bun: separă curat întrebarea logică (ce date vreau) de întrebarea fizică (cum sunt stocate și recuperate). Acea separare permite bazei de date să evolueze independent de aplicație, permite acelorași date să răspundă la query-uri pe care designerul schemei nu le-a imaginat niciodată, și permite operatorului să adauge un index marțea viitoare ca să rezolve un query lent fără să schimbe o singură linie de cod al aplicației.

ACID, ce înseamnă de fapt

Fiecare bază de date relațională promovează ACID. Acronimul e atât de familiar încât a devenit zgomot, dar fiecare literă e o promisiune specifică care costă ceva motorul să o țină.

Atomicity. O tranzacție e tot sau nimic. Dacă o tranzacție conține cinci scrieri și a treia eșuează, primele două sunt rolled back și starea bazei de date arată ca și cum tranzacția n-ar fi început niciodată. Mecanismul este write-ahead log (WAL): fiecare schimbare e adăugată într-un log durabil înainte să fie aplicată fișierelor de date, iar la restart baza de date redă tranzacțiile committed și le aruncă pe cele uncommitted.

Consistency. Fiecare tranzacție trece baza de date dintr-o stare validă în alta. „Validă” e definită de constrângerile pe care le-ai declarat: not-null, unique, check, foreign-key, exclusion. Baza de date refuză să facă commit la o tranzacție care ar viola o constrângere. Acest C nu e același cu C-ul din CAP, apropo; e mult mai îngust. CAP-consistency e „fiecare citire vede ultima scriere peste replicas.” ACID-consistency e „regulile de integritate pe care le-am declarat încă rezistă.”

Isolation. Tranzacțiile concurente nu interferează una cu alta într-un mod care produce un rezultat pe care programarea serială nu l-ar produce. Capcana e că serializability strictă e scumpă, iar standardul SQL definește patru niveluri de izolare cu garanții progresiv mai slabe: read uncommitted, read committed, repeatable read, serializable. Postgres are by default read committed; majoritatea sistemelor de producție rulează pe read committed sau repeatable read. Fenomenele pe care le poți întâlni la niveluri mai slabe (dirty reads, non-repeatable reads, phantom reads, write skew) au nume specifice pentru că au cauzat bug-uri specifice în lumea reală. Să știi pe ce nivel rulează tranzacțiile tale, și ce anomalii permite acel nivel, e parte din scrierea de cod corect de aplicație. Majoritatea inginerilor nu se gândesc la asta destul de des.

Durability. Odată ce o tranzacție e committed, schimbarea supraviețuiește unui crash. Mecanismul e, din nou, WAL: commit-ul returnează doar după ce intrarea de log a fost flushed pe stocare durabilă. Pe o singură mașină asta înseamnă fsync pe disc. Într-un setup replicat de obicei înseamnă că WAL-ul a ajuns la cel puțin o replică sincronă.

Cele patru împreună sunt motivul pentru care un transfer bancar funcționează: debit-ul și credit-ul sunt atomice (unul sau celălalt, niciodată jumătate), consistente (constrângerile de integritate țin pe tot parcursul), izolate (o tranzacție concurentă nu poate vedea starea aplicată pe jumătate), și durabile (odată ce chitanța se tipărește, banii s-au mișcat efectiv). Nu e nevoie de coordonare la nivel de aplicație. Baza de date face treaba.

De ce modelul relațional este „alegerea implicită corectă” în 2026

Tooling-ul e matur: migrări, ORMs, query analyzers, unelte de backup, framework-uri de replicare, oferte cloud-managed, integrări de observability. Caracteristicile operaționale sunt bine înțelese, cu runbook-uri pentru fiecare formă comună de workload. Bazinul de hiring e enorm: fiecare backend engineer a scris SQL. Documentația e exhaustivă (manualul Postgres are singur peste trei mii de pagini). Și costul de a greși e mic: dacă Postgres nu se potrivește, poți muta datele afară, în timp ce store-urile exotice te prind în capcană în momentul în care modelul tău de date s-a întărit.

Modelul relațional este și un knowledge multiplier. Un inginer care învață Postgres învață abilități transferabile: SQL, indexare, query plans, lock-uri, WAL-ul. Un inginer care învață un store NoSQL proprietar învață acel store, și o ia de la zero la următorul job.

Cei doi dominatori open-source

În 2026 două baze de date relaționale open-source rulează cea mai mare parte a load-ului tranzacțional de producție de pe internet: PostgreSQL și MySQL. Nu sunt interschimbabile, dar diferențele contează mai puțin decât contau cândva.

Postgres e cea mai aliniată cu standardul SQL dintre cele două. Suportă un set mai bogat de tipuri din cutie: arrays, ranges, enums, full-text search, JSON și JSONB, hstore, tipuri geografice prin PostGIS. Are puncte de extensie mai puternice: poți adăuga tipuri noi de date, metode noi de indexare, hook-uri noi pentru query planner, toate fără să faci fork la sursă. Materialized views, common table expressions, window functions, recursive queries sunt first-class și sunt așa de un deceniu. Nivelul de izolare implicit este read committed; serializable, când îți trebuie, e bine implementat. Pentru proiecte noi care încep în 2026, Postgres este alegerea implicită clară.

MySQL e cea mai simplă operațional dintre cele două, mai ales în forma sa de InnoDB-cu-row-replication de lungă durată. Povestea de replicare e mai veche și încercată în luptă pe distanțe foarte lungi; o grămadă de infrastructură globală legacy e pe MySQL pentru că în 2010 nimic altceva nu replica la fel de bine peste continente. Query planner-ul e mai puțin sofisticat decât cel al Postgres, sistemul de tipuri e mai îngust, iar suportul JSON, deși prezent, nu e la paritate cu JSONB-ul Postgres. MySQL e încă uriaș în baze de cod legacy (WordPress, Drupal, mult din web-ul timpuriu PHP-era) iar fork-ul MariaDB l-a păstrat sănătos. Alege MySQL astăzi doar dacă ai un motiv operațional puternic sau o amprentă MySQL existentă pe care nu vrei s-o migrezi. Altfel Postgres.

Realitatea cloud-managed

Aproape nimeni nu mai rulează Postgres singur. Rulează o ofertă cloud-managed și tratează baza de date de dedesubt ca și cum ar fi un produs de vendor. Amazon RDS oferă Postgres vanilla pe infrastructura administrată AWS cu backup-uri, patching, failover, și read replicas. Aurora Postgres este reimplementarea AWS a stratului de stocare de sub motorul de query Postgres, cu failover mai rapid și economii la scară. Google Cloud SQL și Azure Database for PostgreSQL sunt echivalentele pe celelalte două cloud-uri. Neon este Postgres cu o interfață serverless, prietenoasă cu branching; Supabase este Postgres plus un API auto-generat și un strat realtime; Crunchy Bridge și Timescale Cloud completează gazdele specializate.

Ofertele cloud restricționează ce poți face (fără extensii arbitrare, fără tunarea fiecărui parametru de server), dar te scapă de povara operațională care obișnuia să fie treaba unui DBA full-time. Pentru majoritatea echipelor schimbul merită.

Când SQL e răspunsul potrivit, și când nu este

Modelul relațional se potrivește când tranzacțiile contează (bani, inventar, identitate, audit), când datele au relații naturale (utilizatorii au comenzi, comenzile au line items, line items referențiază produse), când vei avea nevoie de query-uri ad-hoc pe care nu le-ai planificat (echipa de raportare, echipa de operații, noul product manager), și când raportarea are nevoie de joins. Dacă majoritatea acestora se aplică, vrei o bază de date relațională, Postgres specific.

E nepotrivit când pattern-ul de acces este single-key lookup la throughput extrem (folosește un KV store, lecția 18), când schema evoluează sălbatic peste rânduri și o schemă fixă ar fi o minciună (consideră un document store, lecția 19), pentru time-series cu reguli de retenție (TimescaleDB, InfluxDB, ClickHouse), când traversarea de graf este query-ul dominant (graf-urile mari vor o bază de date de tip graf), sau pentru scanări analitice masive (store-urile columnare ca BigQuery, Snowflake, ClickHouse, Redshift sunt cu un ordin de mărime mai ieftine la scară).

Testul cinstit: enumeră primele cinci query-uri pe care le va rula aplicația ta, după frecvență. Dacă sunt toate single-key gets, vrei un KV store. Dacă sunt joins complexe, vrei SQL. Dacă sunt mixte, răspunsul e de obicei SQL plus un cache.

Teza „Postgres este suficient”

Lecția 8 a făcut argumentul că majoritatea echipelor over-engineer prima lor arhitectură. Corolarul Postgres este mai ascuțit: majoritatea echipelor sub zece milioane de utilizatori nu au nevoie de nimic mai sofisticat decât un Postgres bine tunat, o read replica sau două, indexare atentă, și un connection pooler.

Conservativ, o instanță Postgres modernă pe o mașină de dimensiune sensibilă (64 vCPU, 256 GB RAM, NVMe storage) gestionează zeci de mii de tranzacții de scriere pe secundă și sute de mii de query-uri de citire pe secundă, cu latency median în milisecunde de o singură cifră. Read replicas în spatele PgBouncer multiplică liniar throughput-ul de citire. Autovacuum tunat menține bloat-ul gestionabil. Native partitioning gestionează tabele cu sute de milioane de rânduri fără cliffs de performanță.

Stripe, Notion, GitLab, monoliții Rails din era Heroku, mult din lumea SaaS mică și mijlocie au rulat și continuă să ruleze pe această formă. Conversația „avem nevoie să facem shard” vine de obicei mult mai târziu decât sugerează diagramele de arhitectură, și chiar și atunci, sharding-ul în interiorul Postgres (pe tenant, pe customer, pe regiune) bate de obicei trecerea la un alt motor.

O schemă lucrată

Iată o mică schemă de e-commerce așa cum ai proiecta-o efectiv. Patru tabele: users, products, orders, line items. Foreign keys forțează integritatea referențială. Primary keys sunt ID-uri surrogate. Modelul e neremarcabil, ceea ce e și ideea.

erDiagram
    USERS ||--o{ ORDERS : places
    ORDERS ||--|{ LINE_ITEMS : contains
    PRODUCTS ||--o{ LINE_ITEMS : "appears in"
    USERS {
        bigint id PK
        text email
        text name
        timestamp created_at
    }
    PRODUCTS {
        bigint id PK
        text sku
        text name
        numeric price
        int stock
    }
    ORDERS {
        bigint id PK
        bigint user_id FK
        text status
        numeric total
        timestamp placed_at
    }
    LINE_ITEMS {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        numeric unit_price
    }

Un query care întreabă „ce a cumpărat utilizatorul 42 în ultimele 30 de zile, cu numele produselor și totaluri” e un join peste patru tabele, exprimat în vreo zece linii de SQL, executat în milisecunde cu indexurile potrivite. Încearcă să scrii acel query într-un key-value store și vezi ce-ți cade din mâini.

Unde aterizează această lecție

Modelul relațional este alegerea implicită, plictisitoare și corectă pentru workload-uri tranzacționale. Postgres este alegerea implicită, plictisitoare și corectă în interiorul modelului relațional. Majoritatea echipelor nu vor avea nevoie niciodată de altceva pentru system of record, iar echipele care vor avea, vor ști pentru că au măsurat durerea, nu din modă arhitecturală.

Următoarele două lecții acoperă familiile care chiar bat modelul relațional pe workload-uri specifice: key-value stores (lecția 18) și document stores (lecția 19). Modulul 3 face un tur al tuturor familiilor majore înainte ca lecția 24 să le aducă împreună ca polyglot persistence: folosește Postgres ca system of record, plus câteva store-uri specializate pentru workload-urile unde Postgres nu e unealta potrivită.

Citations and further reading

  • Edgar F. Codd, “A Relational Model of Data for Large Shared Data Banks”, Communications of the ACM, June 1970. The founding document. Worth reading once just to see how much of the modern vocabulary is already there in the original.
  • The PostgreSQL documentation, https://www.postgresql.org/docs/ (retrieved 2026-05-01). The most thorough free database manual on the internet. Start with the Concurrency Control and Performance Tips chapters.
  • Jim Gray and Andreas Reuter, “Transaction Processing: Concepts and Techniques” (Morgan Kaufmann, 1992). Still the reference text for ACID, isolation levels, and the WAL.
  • Stripe Engineering, “Online Migrations at Scale”, https://stripe.com/blog/online-migrations (retrieved 2026-05-01). The companion piece to the Stripe case study from lesson 8: how to evolve a Postgres schema without downtime.
  • Markus Winand, “Use The Index, Luke”, https://use-the-index-luke.com/ (retrieved 2026-05-01). The best free resource on SQL indexing, applicable across engines.
Caută