Baze de date
Curs Complet de Baze de Date¶
SQL, NoSQL — De la fundamente la tehnici avansate¶
Cuprins¶
Partea I — Fundamente teoretice
- Introducere în baze de date
- Modelul relațional
- Proiectarea bazelor de date — modelul Entitate-Relație (ER)
- Normalizarea bazelor de date
Partea II — SQL (Structured Query Language)
5. DDL — Definirea structurii (CREATE, ALTER, DROP)
6. DML — Manipularea datelor (INSERT, UPDATE, DELETE)
7. Interogări SELECT — de la baze la avansat
8. JOIN-uri — combinarea tabelelor
9. Subinterogări (Subqueries)
10. Funcții agregate și GROUP BY
11. Funcții fereastră (Window Functions)
12. Indexare și optimizarea performanței
13. Tranzacții și concurență
14. Vederi (Views), Proceduri stocate și Triggere
15. Securitate și administrare
Partea III — NoSQL
16. Introducere în NoSQL — motivații și clasificare
17. Baze de date Document (MongoDB)
18. Baze de date Key-Value (Redis)
19. Baze de date Column-Family (Cassandra)
20. Baze de date Graf (Neo4j)
Partea IV — Teme avansate
21. SQL vs. NoSQL — ghid de alegere
22. ORM-uri și accesul programatic la baze de date
23. Replicare, sharding și baze de date distribuite
24. Data Warehousing și OLAP
PARTEA I — FUNDAMENTE TEORETICE¶
1. Introducere în baze de date¶
1.1 Ce este o bază de date?¶
O bază de date este o colecție organizată de date, structurată astfel încât să permită stocarea, interogarea și actualizarea eficientă a informațiilor. Un SGBD (Sistem de Gestiune a Bazelor de Date) este software-ul care gestionează baza de date.
┌─────────────────────────────────────────────────────────┐
│ APLICAȚIE │
│ (Web app, Mobile app, Script, Microserviciu) │
├──────────────────────┬──────────────────────────────────┤
│ Driver / Connector │ ORM (SQLAlchemy, Hibernate...) │
├──────────────────────┴──────────────────────────────────┤
│ │
│ SGBD (Database Management System) │
│ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Query │ │ Transaction │ │ Storage Engine │ │
│ │ Processor / │ │ Manager │ │ (InnoDB, WiredT,│ │
│ │ Optimizer │ │ (ACID) │ │ RocksDB...) │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
│ ┌─────────────┐ ┌──────────────┐ ┌─────────────────┐ │
│ │ Buffer Pool │ │ Lock Manager │ │ Recovery (WAL) │ │
│ │ (cache) │ │ (concurrency)│ │ (crash safety) │ │
│ └─────────────┘ └──────────────┘ └─────────────────┘ │
│ │
├──────────────────────────────────────────────────────────┤
│ Stocare pe disc (fișiere de date) │
└──────────────────────────────────────────────────────────┘
1.2 SGBD-uri relaționale populare¶
| SGBD | Tip | Licență | Utilizare tipică |
|---|---|---|---|
| PostgreSQL | Relațional | Open source | Web apps, GIS, analytics, enterprise |
| MySQL/MariaDB | Relațional | Open source | Web apps (LAMP stack), CMS |
| SQLite | Relațional | Public domain | Embedded, mobile, prototipare |
| Oracle DB | Relațional | Comercial | Enterprise, financiar, telecom |
| SQL Server | Relațional | Comercial | Enterprise Microsoft, .NET |
| MongoDB | Document | Open source | Web apps, cataloage, CMS |
| Redis | Key-Value | Open source | Cache, sesiuni, cozi, real-time |
| Cassandra | Column | Open source | IoT, time-series, scale masiv |
| Neo4j | Graf | Open source | Rețele sociale, recomandări, fraud |
1.3 Proprietățile ACID¶
Tranzacțiile în bazele de date relaționale respectă proprietățile ACID:
| Proprietate | Descriere |
|---|---|
| Atomicity | Tranzacția se execută complet sau deloc (all or nothing) |
| Consistency | Baza de date trece dintr-o stare validă în altă stare validă |
| Isolation | Tranzacțiile concurente nu se afectează reciproc |
| Durability | Odată confirmată (COMMIT), modificarea persistă chiar și după crash |
2. Modelul relațional¶
2.1 Concepte fundamentale¶
RELAȚIE (Tabelă):
┌───────────────────────────────────────────────────┐
│ studenti │ ← Numele relației
├─────┬──────────┬────────┬────────┬───────────────┤
│ id │ nume │ varsta │ media │ facultate_id │ ← Atribute (coloane)
├─────┼──────────┼────────┼────────┼───────────────┤
│ 1 │ Ana Pop │ 21 │ 9.50 │ 1 │ ← Tuplu (rând/înregistrare)
│ 2 │ Ion Rus │ 22 │ 8.75 │ 1 │
│ 3 │ Maria D. │ 20 │ 9.80 │ 2 │
└─────┴──────────┴────────┴────────┴───────────────┘
Terminologie:
Relație = Tabelă
Tuplu = Rând (row/record)
Atribut = Coloană (column/field)
Domeniu = Tipul de date al unui atribut (int, varchar, date...)
Grad = Numărul de atribute (coloane)
Cardinalitate = Numărul de tupluri (rânduri)
Schemă = Structura tabelei (nume + atribute + tipuri + constrângeri)
2.2 Chei¶
Cheie Primară (Primary Key — PK):
- Identifică UNIC fiecare rând din tabelă
- Nu poate fi NULL
- O singură cheie primară per tabelă
- Poate fi simplă (o coloană) sau compusă (mai multe coloane)
Cheie Străină (Foreign Key — FK):
- Referință către cheia primară a altei tabele
- Creează RELAȚIA între tabele
- Poate fi NULL (relație opțională)
- Asigură integritatea referențială
Cheie Candidat:
- Orice atribut (sau combinație) care ar putea fi PK
- Unic + not null
- Se alege una ca PK, restul devin chei alternative
Cheie Naturală vs. Cheie Surogat:
Natural: CNP, email — au semnificație reală, dar pot fi instabile
Surogat: id auto_increment / UUID — fără semnificație, stabil (RECOMANDAT)
2.3 Tipuri de relații¶
1:1 (Unu-la-Unu):
Un student are un singur card de acces, un card aparține unui student.
Implementare: FK unic într-una din tabele.
1:N (Unu-la-Mulți) — CEL MAI FRECVENT:
O facultate are mulți studenți, un student aparține unei singure facultăți.
Implementare: FK în tabela „mulți" (studenți) referind PK din tabela „unu" (facultăți).
facultati studenti
┌────┬───────────┐ ┌────┬──────┬───────────────┐
│ id │ nume │ │ id │ nume │ facultate_id │←FK
├────┼───────────┤ 1:N ├────┼──────┼───────────────┤
│ 1 │ Info │◄───────────────│ 1 │ Ana │ 1 │
│ 2 │ Electro │◄───────────────│ 2 │ Ion │ 1 │
└────┴───────────┘ │ 3 │ Maria│ 2 │
└────┴──────┴───────────────┘
M:N (Mulți-la-Mulți):
Un student urmează mai multe cursuri, un curs are mai mulți studenți.
Implementare: tabelă de joncțiune (junction/bridge table) cu 2 FK-uri.
studenti inscrieri (junction) cursuri
┌────┬──────┐ ┌────────────┬──────────┐ ┌────┬──────────┐
│ id │ nume │ │ student_id │ curs_id │ │ id │ nume │
├────┼──────┤ ├────────────┼──────────┤ ├────┼──────────┤
│ 1 │ Ana │◄────│ 1 │ 1 │───►│ 1 │ Algebra │
│ 2 │ Ion │◄────│ 1 │ 2 │───►│ 2 │ Progr. │
└────┴──────┘ │ 2 │ 1 │ └────┴──────────┘
│ 2 │ 2 │
└────────────┴──────────┘
PK = (student_id, curs_id)
3. Proiectarea bazelor de date — modelul Entitate-Relație (ER)¶
3.1 Componentele diagramei ER¶
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ │ │ │ │ │
│ STUDENT │──────────│ INSCRIS_LA │──────────│ CURS │
│ │ M:N │ │ M:N │ │
│ id (PK) │ │ student_id FK│ │ id (PK) │
│ nume │ │ curs_id FK │ │ denumire │
│ email │ │ nota │ │ credite │
│ grupa │ │ data_inscr. │ │ profesor_id │
│ │ │ │ │ │
└──────┬──────┘ └──────────────┘ └──────┬───────┘
│ │
│ N:1 │ N:1
│ │
┌──────┴──────┐ ┌───────┴──────┐
│ FACULTATE │ │ PROFESOR │
│ │ │ │
│ id (PK) │ │ id (PK) │
│ nume │ │ nume │
│ decan │ │ titlu │
└─────────────┘ └──────────────┘
3.2 De la ER la tabele SQL¶
Reguli de conversie:
- Fiecare entitate devine o tabelă
- Fiecare atribut devine o coloană
- Fiecare relație 1:N se implementează cu o cheie străină în tabela „N”
- Fiecare relație M:N se implementează cu o tabelă de joncțiune
- Fiecare relație 1:1 se implementează cu o cheie străină unică
4. Normalizarea bazelor de date¶
Normalizarea elimină redundanța și anomaliile de inserare, actualizare și ștergere.
4.1 Formele normale¶
Forma Normală 0 (nenormalizată):
┌─────┬──────┬──────────────────┬─────────────────────────┐
│ id │ Stud │ Cursuri │ Note │
├─────┼──────┼──────────────────┼─────────────────────────┤
│ 1 │ Ana │ Algebra, Progr. │ 9, 10 │ ← Valori multiple!
│ 2 │ Ion │ Algebra │ 8 │
└─────┴──────┴──────────────────┴─────────────────────────┘
Problemă: atribute cu mai multe valori, grupuri repetitive.
1NF (Prima Formă Normală):
Regulă: Fiecare celulă conține O SINGURĂ valoare atomică.
Se elimină grupurile repetitive.
┌─────┬──────┬──────────┬──────┐
│ id │ Stud │ Curs │ Nota │
├─────┼──────┼──────────┼──────┤
│ 1 │ Ana │ Algebra │ 9 │
│ 1 │ Ana │ Progr. │ 10 │
│ 2 │ Ion │ Algebra │ 8 │
└─────┴──────┴──────────┴──────┘
Problemă: redundanța „Ana" (actualizare anomaly dacă îi schimbăm numele).
2NF (A Doua Formă Normală):
Regulă: 1NF + fiecare atribut non-cheie depinde de ÎNTREAGA cheie primară
(nu doar de o parte a ei — relevant pentru chei compuse).
Separăm în: studenti(id, nume) + inscrieri(student_id, curs, nota)
3NF (A Treia Formă Normală):
Regulă: 2NF + niciun atribut non-cheie nu depinde TRANZITIV de cheia primară.
Exemplu de încălcare:
angajati(id, nume, departament_id, departament_nume)
departament_nume depinde de departament_id, nu direct de id.
→ Separă: angajati(id, nume, dep_id) + departamente(dep_id, dep_nume)
BCNF (Boyce-Codd Normal Form):
Regulă: 3NF + pentru fiecare dependență funcțională X→Y, X este supercheie.
Practic: forma „strictă" a 3NF, rezolvă cazuri marginale.
4.2 Regula practică¶
Normalizare completă (3NF/BCNF) pentru sisteme OLTP (tranzacționale):
✅ Minimizează redundanța
✅ Previne anomalii
✅ Economisește spațiu
Denormalizare controlată pentru sisteme OLAP (analitice) și performanță:
✅ Reduce JOIN-uri
✅ Îmbunătățește viteza interogărilor de citire
❌ Introduce redundanță (trebuie gestionată)
PARTEA II — SQL¶
5. DDL — Definirea structurii¶
5.1 CREATE TABLE¶
-- Creare bază de date
CREATE DATABASE universitate
CHARACTER SET utf8mb4
COLLATE utf8mb4_unicode_ci;
USE universitate;
-- Tabelele sistemului nostru universitar
CREATE TABLE facultati (
id SERIAL PRIMARY KEY, -- PostgreSQL: auto-increment
nume VARCHAR(100) NOT NULL UNIQUE,
cod CHAR(5) NOT NULL UNIQUE,
data_infiintare DATE,
buget DECIMAL(12, 2) DEFAULT 0.00,
activa BOOLEAN DEFAULT TRUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE profesori (
id SERIAL PRIMARY KEY,
nume VARCHAR(100) NOT NULL,
prenume VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
titlu VARCHAR(50) CHECK (titlu IN ('Asist.', 'Lect.', 'Conf.', 'Prof.')),
facultate_id INTEGER REFERENCES facultati(id),
data_angajare DATE NOT NULL DEFAULT CURRENT_DATE,
salariu DECIMAL(10, 2) CHECK (salariu > 0)
);
CREATE TABLE studenti (
id SERIAL PRIMARY KEY,
nume VARCHAR(100) NOT NULL,
prenume VARCHAR(100) NOT NULL,
email VARCHAR(150) UNIQUE NOT NULL,
cnp CHAR(13) UNIQUE,
data_nastere DATE,
an_studiu SMALLINT CHECK (an_studiu BETWEEN 1 AND 6),
grupa VARCHAR(10),
facultate_id INTEGER NOT NULL,
bursa DECIMAL(8, 2) DEFAULT 0.00,
activ BOOLEAN DEFAULT TRUE,
CONSTRAINT fk_student_facultate
FOREIGN KEY (facultate_id)
REFERENCES facultati(id)
ON DELETE RESTRICT -- Nu permite ștergerea facultății dacă are studenți
ON UPDATE CASCADE -- Actualizează FK dacă PK se schimbă
);
CREATE TABLE cursuri (
id SERIAL PRIMARY KEY,
denumire VARCHAR(200) NOT NULL,
cod_curs VARCHAR(20) UNIQUE NOT NULL,
credite SMALLINT NOT NULL CHECK (credite BETWEEN 1 AND 30),
semestru SMALLINT CHECK (semestru IN (1, 2)),
profesor_id INTEGER REFERENCES profesori(id) ON DELETE SET NULL,
max_studenti INTEGER DEFAULT 100,
obligatoriu BOOLEAN DEFAULT TRUE
);
-- Tabelă de joncțiune M:N cu atribute suplimentare
CREATE TABLE inscrieri (
student_id INTEGER NOT NULL REFERENCES studenti(id) ON DELETE CASCADE,
curs_id INTEGER NOT NULL REFERENCES cursuri(id) ON DELETE CASCADE,
data_inscriere TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
nota_examen DECIMAL(4, 2) CHECK (nota_examen BETWEEN 1 AND 10),
nota_laborator DECIMAL(4, 2) CHECK (nota_laborator BETWEEN 1 AND 10),
nota_finala DECIMAL(4, 2) GENERATED ALWAYS AS (
ROUND(nota_examen * 0.6 + nota_laborator * 0.4, 2)
) STORED,
PRIMARY KEY (student_id, curs_id) -- Cheie compusă
);
-- Index pentru interogări frecvente
CREATE INDEX idx_studenti_facultate ON studenti(facultate_id);
CREATE INDEX idx_studenti_an ON studenti(an_studiu);
CREATE INDEX idx_inscrieri_nota ON inscrieri(nota_finala);
CREATE INDEX idx_studenti_nume ON studenti(nume, prenume);
5.2 Tipuri de date SQL comune¶
Categorie Tip Descriere Exemplu
─────────────────────────────────────────────────────────────────────────────
Numeric INTEGER / INT Întreg pe 32 biți 42
BIGINT Întreg pe 64 biți 9223372036854775807
SMALLINT Întreg pe 16 biți 32767
SERIAL Auto-increment (PostgreSQL) 1, 2, 3, ...
DECIMAL(p,s) Precizie exactă (bani!) 99999.99
FLOAT / REAL Virgulă mobilă (aproximativ) 3.14159
DOUBLE PRECISION Virgulă mobilă dublă precizie
Text CHAR(n) Lungime fixă, padding cu spații 'abc '
VARCHAR(n) Lungime variabilă, max n 'abc'
TEXT Lungime nelimitată 'text foarte lung...'
Temporal DATE Doar data '2024-01-15'
TIME Doar ora '14:30:00'
TIMESTAMP Dată + oră '2024-01-15 14:30:00'
INTERVAL Durată '3 days 2 hours'
Boolean BOOLEAN True / False TRUE, FALSE
Binar BYTEA / BLOB Date binare Imagini, fișiere
Special UUID Identificator unic universal 'a0eebc99-9c0b...'
JSON / JSONB Date semi-structurate '{"key": "val"}'
ARRAY Array (PostgreSQL) '{1,2,3}'
ENUM Valori predefinite 'red','green','blue'
5.3 ALTER TABLE¶
-- Adăugare coloană
ALTER TABLE studenti ADD COLUMN telefon VARCHAR(15);
-- Modificare coloană
ALTER TABLE studenti ALTER COLUMN grupa TYPE VARCHAR(20);
-- Redenumire coloană
ALTER TABLE studenti RENAME COLUMN grupa TO grupa_studiu;
-- Ștergere coloană
ALTER TABLE studenti DROP COLUMN telefon;
-- Adăugare constrângere
ALTER TABLE studenti ADD CONSTRAINT chk_bursa CHECK (bursa >= 0);
-- Adăugare cheie străină
ALTER TABLE cursuri ADD CONSTRAINT fk_curs_facultate
FOREIGN KEY (facultate_id) REFERENCES facultati(id);
-- Ștergere constrângere
ALTER TABLE studenti DROP CONSTRAINT chk_bursa;
-- DROP TABLE (ștergere tabelă completă, IREVERSIBIL):
DROP TABLE IF EXISTS inscrieri;
-- TRUNCATE (șterge toate rândurile, păstrează structura, resetează auto-increment):
TRUNCATE TABLE inscrieri;
-- Diferența vs. DELETE FROM inscrieri: TRUNCATE e mai rapid, nu loghez individual
6. DML — Manipularea datelor¶
-- === INSERT ===
-- Inserare singulară:
INSERT INTO facultati (nume, cod, data_infiintare, buget)
VALUES ('Informatică', 'INFO', '1990-10-01', 5000000.00);
INSERT INTO facultati (nume, cod)
VALUES ('Electronică', 'ELEC');
-- Inserare multiplă:
INSERT INTO studenti (nume, prenume, email, an_studiu, facultate_id) VALUES
('Pop', 'Ana', 'ana.pop@univ.ro', 2, 1),
('Rus', 'Ion', 'ion.rus@univ.ro', 3, 1),
('Dima', 'Maria', 'maria.dima@univ.ro', 1, 2),
('Marin', 'Andrei', 'andrei.m@univ.ro', 2, 1),
('Stefan', 'Elena', 'elena.s@univ.ro', 4, 2);
-- INSERT ... SELECT (inserare din interogare):
INSERT INTO arhiva_studenti (nume, prenume, facultate_id)
SELECT nume, prenume, facultate_id
FROM studenti
WHERE activ = FALSE;
-- UPSERT (INSERT sau UPDATE dacă există — PostgreSQL):
INSERT INTO studenti (email, nume, prenume, an_studiu, facultate_id)
VALUES ('ana.pop@univ.ro', 'Pop', 'Ana', 3, 1)
ON CONFLICT (email)
DO UPDATE SET an_studiu = EXCLUDED.an_studiu;
-- === UPDATE ===
-- Actualizare simplă:
UPDATE studenti
SET an_studiu = 3, grupa_studiu = 'B3'
WHERE email = 'ana.pop@univ.ro';
-- Actualizare condiționată:
UPDATE studenti
SET bursa = CASE
WHEN media_generala >= 9.5 THEN 1500.00
WHEN media_generala >= 9.0 THEN 1000.00
WHEN media_generala >= 8.5 THEN 500.00
ELSE 0.00
END
WHERE activ = TRUE AND an_studiu <= 4;
-- Actualizare cu subinterogare:
UPDATE cursuri
SET max_studenti = max_studenti + 20
WHERE id IN (
SELECT curs_id FROM inscrieri
GROUP BY curs_id
HAVING COUNT(*) >= max_studenti * 0.9
);
-- === DELETE ===
-- Ștergere cu condiție:
DELETE FROM studenti WHERE activ = FALSE AND an_studiu > 6;
-- Ștergere cu subinterogare:
DELETE FROM inscrieri
WHERE student_id IN (
SELECT id FROM studenti WHERE activ = FALSE
);
-- ⚠️ ATENȚIE: DELETE fără WHERE șterge TOATE rândurile!
-- DELETE FROM studenti; ← PERICULOS!
7. Interogări SELECT — de la baze la avansat¶
7.1 Anatomia completă a unui SELECT¶
-- Ordinea SCRIERII (cum scriem):
SELECT [DISTINCT] coloane / expresii / funcții -- 1
FROM tabela -- 2
[JOIN alta_tabela ON condiție] -- 3
[WHERE condiție_filtrare] -- 4
[GROUP BY coloane_grupare] -- 5
[HAVING condiție_pe_grupuri] -- 6
[ORDER BY coloane_sortare [ASC|DESC]] -- 7
[LIMIT n OFFSET m] -- 8
-- Ordinea EXECUȚIEI (cum procesează SGBD-ul):
-- FROM → JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT
-- Aceasta explică de ce nu poți folosi alias-uri din SELECT în WHERE!
7.2 Interogări fundamentale¶
-- Selecție simplă:
SELECT * FROM studenti; -- Toate coloanele
SELECT nume, prenume, email FROM studenti; -- Coloane specifice
SELECT DISTINCT an_studiu FROM studenti; -- Valori unice
SELECT DISTINCT ON (facultate_id) nume, facultate_id -- PostgreSQL: un rând per grup
FROM studenti ORDER BY facultate_id, media DESC;
-- Alias-uri:
SELECT
s.nume || ' ' || s.prenume AS nume_complet, -- Concatenare + alias
s.an_studiu AS an,
EXTRACT(YEAR FROM AGE(s.data_nastere)) AS varsta, -- Calcul vârstă
COALESCE(s.bursa, 0) AS bursa_efectiva -- Înlocuire NULL cu 0
FROM studenti s; -- Alias pentru tabelă
-- Filtrare WHERE:
SELECT * FROM studenti WHERE an_studiu = 2;
SELECT * FROM studenti WHERE an_studiu >= 2 AND facultate_id = 1;
SELECT * FROM studenti WHERE an_studiu IN (1, 2, 3);
SELECT * FROM studenti WHERE email LIKE '%@univ.ro'; -- Pattern matching
SELECT * FROM studenti WHERE email ILIKE '%@UNIV%'; -- Case-insensitive (PG)
SELECT * FROM studenti WHERE bursa BETWEEN 500 AND 1500;
SELECT * FROM studenti WHERE cnp IS NULL; -- Verificare NULL
SELECT * FROM studenti WHERE cnp IS NOT NULL;
-- Sortare:
SELECT * FROM studenti ORDER BY nume ASC, prenume ASC;
SELECT * FROM studenti ORDER BY an_studiu DESC, nume ASC;
SELECT * FROM studenti ORDER BY bursa DESC NULLS LAST; -- NULL-urile la final
-- Limitare:
SELECT * FROM studenti ORDER BY bursa DESC LIMIT 10; -- Top 10
SELECT * FROM studenti ORDER BY id LIMIT 20 OFFSET 40; -- Paginare (pg 3)
7.3 Expresii și funcții scalare¶
-- Funcții string:
SELECT
UPPER(nume) AS nume_majuscule,
LOWER(email) AS email_mic,
LENGTH(nume) AS lungime_nume,
CONCAT(nume, ' ', prenume) AS full_name, -- sau: nume || ' ' || prenume
SUBSTRING(cod_curs FROM 1 FOR 3) AS prefix,
TRIM(BOTH ' ' FROM nume) AS trimmed,
REPLACE(email, '@univ.ro', '@newdomain.ro') AS email_nou,
LEFT(cnp, 1) AS sex_code -- '1' sau '2'
FROM studenti;
-- Funcții numerice:
SELECT
ROUND(nota_finala, 1) AS nota_rotunjita,
CEIL(nota_finala) AS nota_sus,
FLOOR(nota_finala) AS nota_jos,
ABS(nota_examen - nota_laborator) AS diferenta,
GREATEST(nota_examen, nota_laborator) AS nota_max,
LEAST(nota_examen, nota_laborator) AS nota_min
FROM inscrieri;
-- Funcții temporale:
SELECT
CURRENT_DATE AS azi,
CURRENT_TIMESTAMP AS acum,
AGE(data_nastere) AS varsta_interval,
EXTRACT(YEAR FROM data_nastere) AS an_nastere,
EXTRACT(MONTH FROM data_inscriere) AS luna_inscriere,
DATE_TRUNC('month', data_inscriere) AS luna,
data_inscriere + INTERVAL '30 days' AS deadline
FROM studenti;
-- CASE — logică condițională:
SELECT
nume, prenume, nota_finala,
CASE
WHEN nota_finala >= 9 THEN 'Excelent'
WHEN nota_finala >= 7 THEN 'Bine'
WHEN nota_finala >= 5 THEN 'Satisfăcător'
ELSE 'Nesatisfăcător'
END AS calificativ
FROM inscrieri i
JOIN studenti s ON i.student_id = s.id;
-- COALESCE — prima valoare non-NULL:
SELECT COALESCE(telefon, email, 'N/A') AS contact FROM studenti;
-- NULLIF — returnează NULL dacă cele două valori sunt egale:
SELECT NULLIF(nota_examen, 0) AS nota_valida FROM inscrieri;
-- Util pentru a evita împărțirea la zero: x / NULLIF(y, 0)
8. JOIN-uri — combinarea tabelelor¶
8.1 Tipuri de JOIN¶
-- INNER JOIN — doar rândurile cu potrivire în AMBELE tabele:
SELECT s.nume, s.prenume, f.nume AS facultate
FROM studenti s
INNER JOIN facultati f ON s.facultate_id = f.id;
-- LEFT (OUTER) JOIN — TOATE rândurile din stânga + potriviri din dreapta:
-- Dacă nu există potrivire, coloanele din dreapta sunt NULL.
SELECT c.denumire, p.nume AS profesor
FROM cursuri c
LEFT JOIN profesori p ON c.profesor_id = p.id;
-- Arată TOATE cursurile, chiar și cele fără profesor alocat (profesor=NULL)
-- RIGHT (OUTER) JOIN — invers: TOATE din dreapta + potriviri din stânga:
SELECT c.denumire, p.nume
FROM cursuri c
RIGHT JOIN profesori p ON c.profesor_id = p.id;
-- Arată TOȚI profesorii, chiar și cei fără cursuri
-- FULL OUTER JOIN — TOATE rândurile din ambele tabele:
SELECT s.nume AS student, i.curs_id, c.denumire AS curs
FROM studenti s
FULL OUTER JOIN inscrieri i ON s.id = i.student_id
FULL OUTER JOIN cursuri c ON i.curs_id = c.id;
-- CROSS JOIN — produs cartezian (fiecare rând × fiecare rând):
SELECT s.nume, c.denumire
FROM studenti s
CROSS JOIN cursuri c;
-- Atenție: 100 studenți × 50 cursuri = 5000 rânduri!
-- SELF JOIN — tabelă cu ea însăși:
-- Exemplu: găsește studenți din aceeași facultate:
SELECT s1.nume AS student1, s2.nume AS student2, f.nume AS facultate
FROM studenti s1
JOIN studenti s2 ON s1.facultate_id = s2.facultate_id AND s1.id < s2.id
JOIN facultati f ON s1.facultate_id = f.id;
8.2 Diagrama Venn a JOIN-urilor¶
INNER JOIN: LEFT JOIN: RIGHT JOIN: FULL OUTER JOIN:
┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┐ ┌───┬───┬───┐
│ │███│ │ │███│███│ │ │ │███│███│ │███│███│███│
│ A │███│ B │ │███│███│ B │ │ A │███│███│ │███│███│███│
│ │███│ │ │███│███│ │ │ │███│███│ │███│███│███│
└───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┘ └───┴───┴───┘
Doar potriviri Tot A + potriviri Tot B + potriviri Tot A + Tot B
8.3 Interogare cu multiple JOIN-uri¶
-- Raport complet: student, facultate, cursuri, note, profesor
SELECT
s.nume || ' ' || s.prenume AS student,
f.nume AS facultate,
s.an_studiu AS an,
c.denumire AS curs,
c.credite,
p.titlu || ' ' || p.nume AS profesor,
i.nota_examen,
i.nota_laborator,
i.nota_finala
FROM studenti s
JOIN facultati f ON s.facultate_id = f.id
JOIN inscrieri i ON s.id = i.student_id
JOIN cursuri c ON i.curs_id = c.id
LEFT JOIN profesori p ON c.profesor_id = p.id
WHERE s.activ = TRUE
ORDER BY s.nume, c.denumire;
9. Subinterogări (Subqueries)¶
-- Subinterogare scalară (returnează o singură valoare):
SELECT nume, prenume, bursa
FROM studenti
WHERE bursa > (SELECT AVG(bursa) FROM studenti WHERE bursa > 0);
-- Subinterogare în lista IN:
SELECT denumire, credite
FROM cursuri
WHERE id IN (
SELECT curs_id FROM inscrieri
GROUP BY curs_id
HAVING COUNT(student_id) > 50
);
-- Subinterogare EXISTS (verifică dacă subinterogarea returnează cel puțin un rând):
-- Studenți care au cel puțin o notă sub 5:
SELECT s.nume, s.prenume
FROM studenti s
WHERE EXISTS (
SELECT 1 FROM inscrieri i
WHERE i.student_id = s.id AND i.nota_finala < 5
);
-- Subinterogare în FROM (derived table):
SELECT top.facultate, top.media_generala
FROM (
SELECT f.nume AS facultate, AVG(i.nota_finala) AS media_generala
FROM inscrieri i
JOIN studenti s ON i.student_id = s.id
JOIN facultati f ON s.facultate_id = f.id
GROUP BY f.nume
) AS top
WHERE top.media_generala > 7
ORDER BY top.media_generala DESC;
-- CTE (Common Table Expression) — mai citibil decât subinterogări imbricate:
WITH note_studenti AS (
SELECT
s.id,
s.nume || ' ' || s.prenume AS student,
AVG(i.nota_finala) AS media
FROM studenti s
JOIN inscrieri i ON s.id = i.student_id
GROUP BY s.id, s.nume, s.prenume
),
media_globala AS (
SELECT AVG(media) AS medie FROM note_studenti
)
SELECT ns.student, ROUND(ns.media, 2) AS media
FROM note_studenti ns, media_globala mg
WHERE ns.media > mg.medie
ORDER BY ns.media DESC;
-- CTE recursiv (ex: ierarhia managerială):
WITH RECURSIVE ierarhie AS (
-- Caz de bază: root (fără manager)
SELECT id, nume, manager_id, 1 AS nivel
FROM angajati
WHERE manager_id IS NULL
UNION ALL
-- Pas recursiv: subordonații
SELECT a.id, a.nume, a.manager_id, h.nivel + 1
FROM angajati a
JOIN ierarhie h ON a.manager_id = h.id
)
SELECT * FROM ierarhie ORDER BY nivel, nume;
10. Funcții agregate și GROUP BY¶
-- Funcții agregate:
-- COUNT(), SUM(), AVG(), MIN(), MAX()
-- Toate IGNORĂ valorile NULL (excepție: COUNT(*) numără toate rândurile)
-- Număr studenți per facultate:
SELECT
f.nume AS facultate,
COUNT(s.id) AS nr_studenti,
AVG(s.bursa) AS bursa_medie,
MAX(s.bursa) AS bursa_max,
SUM(s.bursa) AS total_burse
FROM facultati f
LEFT JOIN studenti s ON f.id = s.facultate_id
GROUP BY f.id, f.nume
ORDER BY nr_studenti DESC;
-- HAVING — filtrare DUPĂ grupare (WHERE filtrează ÎNAINTE de grupare):
-- Cursuri cu media notelor sub 6:
SELECT
c.denumire,
COUNT(i.student_id) AS nr_studenti,
ROUND(AVG(i.nota_finala), 2) AS media,
MIN(i.nota_finala) AS nota_min,
MAX(i.nota_finala) AS nota_max
FROM cursuri c
JOIN inscrieri i ON c.id = i.curs_id
WHERE i.nota_finala IS NOT NULL -- WHERE: filtrare înainte de grupare
GROUP BY c.id, c.denumire
HAVING AVG(i.nota_finala) < 6 -- HAVING: filtrare după grupare
ORDER BY media ASC;
-- GROUPING SETS, ROLLUP, CUBE (PostgreSQL) — agregări multiple:
-- ROLLUP: totaluri ierarhice
SELECT
f.nume AS facultate,
s.an_studiu AS an,
COUNT(*) AS nr_studenti,
ROUND(AVG(i.nota_finala), 2) AS media
FROM studenti s
JOIN facultati f ON s.facultate_id = f.id
JOIN inscrieri i ON s.id = i.student_id
GROUP BY ROLLUP(f.nume, s.an_studiu)
ORDER BY f.nume NULLS FIRST, s.an_studiu NULLS FIRST;
-- Produce: subtotaluri per facultate + total general (unde f.nume = NULL)
-- STRING_AGG (PostgreSQL) / GROUP_CONCAT (MySQL):
SELECT
s.nume || ' ' || s.prenume AS student,
STRING_AGG(c.denumire, ', ' ORDER BY c.denumire) AS cursuri
FROM studenti s
JOIN inscrieri i ON s.id = i.student_id
JOIN cursuri c ON i.curs_id = c.id
GROUP BY s.id, s.nume, s.prenume;
-- Ana Pop: Algebra, Baze de date, Programare
11. Funcții fereastră (Window Functions)¶
-- Window functions operează pe un „fereastră" de rânduri FĂRĂ a colasa grupurile.
-- Diferența vs. GROUP BY: păstrează toate rândurile originale.
-- ROW_NUMBER — numerotare secvențială:
SELECT
nume, prenume, nota_finala,
ROW_NUMBER() OVER (ORDER BY nota_finala DESC) AS pozitie
FROM studenti s
JOIN inscrieri i ON s.id = i.student_id
WHERE i.curs_id = 1;
-- RANK / DENSE_RANK — clasament cu gestionarea egalităților:
SELECT
nume, nota_finala,
RANK() OVER (ORDER BY nota_finala DESC) AS rank, -- 1, 2, 2, 4 (sare)
DENSE_RANK() OVER (ORDER BY nota_finala DESC) AS dense -- 1, 2, 2, 3 (nu sare)
FROM studenti s
JOIN inscrieri i ON s.id = i.student_id;
-- PARTITION BY — fereastră per grup (ca GROUP BY, dar păstrează rândurile):
-- Top 3 studenți per facultate:
SELECT * FROM (
SELECT
f.nume AS facultate,
s.nume || ' ' || s.prenume AS student,
AVG(i.nota_finala) AS media,
ROW_NUMBER() OVER (
PARTITION BY f.id
ORDER BY AVG(i.nota_finala) DESC
) AS rang_in_facultate
FROM studenti s
JOIN facultati f ON s.facultate_id = f.id
JOIN inscrieri i ON s.id = i.student_id
GROUP BY f.id, f.nume, s.id, s.nume, s.prenume
) ranked
WHERE rang_in_facultate <= 3;
-- Funcții agregate ca window:
SELECT
s.nume, i.nota_finala,
AVG(i.nota_finala) OVER () AS media_globala,
AVG(i.nota_finala) OVER (PARTITION BY s.facultate_id) AS media_facultate,
i.nota_finala - AVG(i.nota_finala) OVER (PARTITION BY s.facultate_id) AS dif_fata_medie
FROM studenti s
JOIN inscrieri i ON s.id = i.student_id;
-- LAG / LEAD — acces la rânduri anterioare/următoare:
SELECT
data_inscriere,
COUNT(*) AS inscrieri_zi,
LAG(COUNT(*)) OVER (ORDER BY data_inscriere) AS inscrieri_ziua_anterioara,
COUNT(*) - LAG(COUNT(*)) OVER (ORDER BY data_inscriere) AS diferenta
FROM inscrieri
GROUP BY data_inscriere
ORDER BY data_inscriere;
-- SUM cumulativă (running total):
SELECT
data_inscriere::date AS data,
COUNT(*) AS inscrieri,
SUM(COUNT(*)) OVER (ORDER BY data_inscriere::date) AS total_cumulat
FROM inscrieri
GROUP BY data_inscriere::date;
12. Indexare și optimizarea performanței¶
12.1 Ce este un index?¶
Fără index (Full Table Scan):
Baza de date parcurge FIECARE rând → O(n)
10 milioane rânduri = 10 milioane verificări
Cu index B-Tree:
Structură arborescentă echilibrată → O(log n)
10 milioane rânduri ≈ 23 de pași (log₂ 10M ≈ 23)
Analogie: index = cuprinsul unei cărți
Fără cuprins: răsfoiești pagină cu pagină
Cu cuprins: mergi direct la pagina dorită
12.2 Tipuri de indexuri¶
-- B-Tree (default, cel mai comun): egalitate, range, sortare
CREATE INDEX idx_email ON studenti(email);
-- Compus (multiple coloane — ordinea contează!):
CREATE INDEX idx_facultate_an ON studenti(facultate_id, an_studiu);
-- Funcționează pentru: WHERE facultate_id = 1
-- WHERE facultate_id = 1 AND an_studiu = 2
-- NU funcționează pentru: WHERE an_studiu = 2 (fără facultate_id)
-- Unic:
CREATE UNIQUE INDEX idx_email_unic ON studenti(email);
-- Parțial (doar pentru un subset de rânduri):
CREATE INDEX idx_studenti_activi ON studenti(nume, prenume)
WHERE activ = TRUE;
-- GIN (Generalized Inverted Index — full-text search, JSONB, arrays):
CREATE INDEX idx_cursuri_text ON cursuri USING GIN (to_tsvector('romanian', denumire));
-- Hash (doar egalitate, mai rapid ca B-Tree pentru =, fără range/sort):
CREATE INDEX idx_email_hash ON studenti USING HASH (email);
12.3 EXPLAIN — analiza planului de execuție¶
-- EXPLAIN arată CUM va executa baza de date interogarea:
EXPLAIN ANALYZE
SELECT s.nume, f.nume AS facultate
FROM studenti s
JOIN facultati f ON s.facultate_id = f.id
WHERE s.an_studiu = 2;
-- Output tipic:
-- Hash Join (cost=1.04..2.15 rows=5 width=64) (actual time=0.08..0.09 rows=5)
-- Hash Cond: (s.facultate_id = f.id)
-- -> Seq Scan on studenti s (cost=0.00..1.10 rows=5 width=40)
-- Filter: (an_studiu = 2)
-- Rows Removed by Filter: 95
-- -> Hash (cost=1.02..1.02 rows=2 width=28)
-- -> Seq Scan on facultati f
-- Seq Scan = parcurgere secvențială (fără index)
-- Index Scan = folosește index
-- Bitmap Index Scan = index + recheck
-- Hash Join / Nested Loop / Merge Join = strategii de JOIN
-- Reguli de optimizare:
-- 1. Indexează coloanele din WHERE, JOIN ON, ORDER BY
-- 2. Evită SELECT * (selectează doar coloanele necesare)
-- 3. Evită funcții pe coloane indexate: WHERE UPPER(email) = '...'
-- → Soluție: CREATE INDEX idx_email_lower ON studenti(LOWER(email));
-- 4. ANALYZE actualizează statisticile (optimizer-ul le folosește):
ANALYZE studenti;
13. Tranzacții și concurență¶
13.1 Tranzacții¶
-- O tranzacție grupează mai multe operații ca o unitate atomică:
-- Fie TOATE reușesc, fie NICIUNA.
BEGIN; -- sau: START TRANSACTION;
-- Transfer bursă de la Ana la Ion:
UPDATE studenti SET bursa = bursa - 200 WHERE id = 1;
UPDATE studenti SET bursa = bursa + 200 WHERE id = 2;
-- Verificare:
SELECT id, nume, bursa FROM studenti WHERE id IN (1, 2);
COMMIT; -- Confirmă modificările permanent
-- sau:
-- ROLLBACK; -- Anulează TOTUL de la BEGIN
-- SAVEPOINT — punct de salvare intermediar:
BEGIN;
INSERT INTO inscrieri (student_id, curs_id) VALUES (1, 5);
SAVEPOINT sp1;
INSERT INTO inscrieri (student_id, curs_id) VALUES (1, 6);
-- Ops, greșeală:
ROLLBACK TO sp1; -- Anulează doar inserarea cu curs_id=6
COMMIT; -- Confirmă inserarea cu curs_id=5
13.2 Niveluri de izolare¶
Nivel de izolare Dirty Read Non-Repeatable Read Phantom Read Performanță
──────────────────────────────────────────────────────────────────────────────
READ UNCOMMITTED Posibil Posibil Posibil Cea mai bună
READ COMMITTED Prevenit Posibil Posibil Bună (PG default)
REPEATABLE READ Prevenit Prevenit Posibil* Moderată
SERIALIZABLE Prevenit Prevenit Prevenit Cea mai slabă
* PostgreSQL previne și phantom reads la REPEATABLE READ (implementare superioară)
Dirty Read: Citești date NECONFIRMATE ale altei tranzacții
Non-Repeatable Read: Citești aceleași date de 2 ori, obții valori diferite
Phantom Read: Executat aceeași interogare de 2 ori, obții rânduri diferite
Setare:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
BEGIN;
-- ... interogări ...
COMMIT;
13.3 Locking¶
-- Lock explicit pe rânduri (SELECT ... FOR UPDATE):
BEGIN;
SELECT * FROM studenti WHERE id = 1 FOR UPDATE;
-- Rândul este BLOCAT — altă tranzacție care încearcă FOR UPDATE va AȘTEPTA
UPDATE studenti SET bursa = bursa + 100 WHERE id = 1;
COMMIT;
-- Deadlock:
-- T1: lock rândul A, apoi cere lock pe B
-- T2: lock rândul B, apoi cere lock pe A
-- → SGBD-ul detectează deadlock-ul și anulează una din tranzacții (ROLLBACK automat)
-- Advisory locks (cooperative, application-level):
SELECT pg_advisory_lock(12345); -- Lock global cu ID 12345
-- ... secțiune critică ...
SELECT pg_advisory_unlock(12345);
14. Vederi, Proceduri stocate și Triggere¶
14.1 Vederi (Views)¶
-- O vedere = interogare salvată, tratată ca o tabelă virtuală
CREATE VIEW v_studenti_activi AS
SELECT
s.id, s.nume, s.prenume, s.email,
f.nume AS facultate,
s.an_studiu,
COALESCE(
(SELECT ROUND(AVG(i.nota_finala), 2)
FROM inscrieri i WHERE i.student_id = s.id),
0
) AS media
FROM studenti s
JOIN facultati f ON s.facultate_id = f.id
WHERE s.activ = TRUE;
-- Utilizare (ca o tabelă):
SELECT * FROM v_studenti_activi WHERE media >= 9;
-- Materialized View (precomputat, stocat pe disc — rapid la citire):
CREATE MATERIALIZED VIEW mv_statistici_facultate AS
SELECT
f.id, f.nume,
COUNT(s.id) AS total_studenti,
ROUND(AVG(i.nota_finala), 2) AS media_generala
FROM facultati f
LEFT JOIN studenti s ON f.id = s.facultate_id
LEFT JOIN inscrieri i ON s.id = i.student_id
GROUP BY f.id, f.nume;
-- Trebuie reîmprospătat manual:
REFRESH MATERIALIZED VIEW mv_statistici_facultate;
-- Sau CONCURRENTLY (fără a bloca citirile):
REFRESH MATERIALIZED VIEW CONCURRENTLY mv_statistici_facultate;
14.2 Proceduri stocate și funcții (PostgreSQL)¶
-- Funcție: returnează o valoare
CREATE OR REPLACE FUNCTION calcul_media_student(p_student_id INTEGER)
RETURNS DECIMAL(4,2) AS $$
DECLARE
v_media DECIMAL(4,2);
BEGIN
SELECT ROUND(AVG(nota_finala), 2)
INTO v_media
FROM inscrieri
WHERE student_id = p_student_id AND nota_finala IS NOT NULL;
RETURN COALESCE(v_media, 0.00);
END;
$$ LANGUAGE plpgsql;
-- Apel:
SELECT calcul_media_student(1);
SELECT s.nume, calcul_media_student(s.id) AS media FROM studenti s;
-- Procedură: execută acțiuni, nu returnează valoare (PostgreSQL 11+)
CREATE OR REPLACE PROCEDURE inscrie_student(
p_student_id INTEGER,
p_curs_id INTEGER
) AS $$
DECLARE
v_count INTEGER;
v_max INTEGER;
BEGIN
-- Verificare: cursul nu e plin
SELECT COUNT(*), c.max_studenti
INTO v_count, v_max
FROM inscrieri i
JOIN cursuri c ON c.id = p_curs_id
WHERE i.curs_id = p_curs_id
GROUP BY c.max_studenti;
IF v_count >= v_max THEN
RAISE EXCEPTION 'Cursul este plin (% / %)', v_count, v_max;
END IF;
-- Verificare: studentul nu e deja înscris
IF EXISTS (SELECT 1 FROM inscrieri WHERE student_id = p_student_id AND curs_id = p_curs_id) THEN
RAISE EXCEPTION 'Studentul este deja înscris la acest curs';
END IF;
-- Înscriere
INSERT INTO inscrieri (student_id, curs_id)
VALUES (p_student_id, p_curs_id);
RAISE NOTICE 'Student % înscris cu succes la cursul %', p_student_id, p_curs_id;
END;
$$ LANGUAGE plpgsql;
-- Apel:
CALL inscrie_student(1, 5);
14.3 Triggere¶
-- Trigger = cod executat automat la INSERT, UPDATE sau DELETE
-- Funcția trigger:
CREATE OR REPLACE FUNCTION trg_audit_studenti()
RETURNS TRIGGER AS $$
BEGIN
IF TG_OP = 'INSERT' THEN
INSERT INTO audit_log (tabela, operatie, id_entitate, detalii, timestamp)
VALUES ('studenti', 'INSERT', NEW.id, row_to_json(NEW)::text, NOW());
RETURN NEW;
ELSIF TG_OP = 'UPDATE' THEN
INSERT INTO audit_log (tabela, operatie, id_entitate, detalii, timestamp)
VALUES ('studenti', 'UPDATE', NEW.id,
json_build_object('old', row_to_json(OLD), 'new', row_to_json(NEW))::text,
NOW());
RETURN NEW;
ELSIF TG_OP = 'DELETE' THEN
INSERT INTO audit_log (tabela, operatie, id_entitate, detalii, timestamp)
VALUES ('studenti', 'DELETE', OLD.id, row_to_json(OLD)::text, NOW());
RETURN OLD;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Asocierea trigger-ului cu tabela:
CREATE TRIGGER audit_studenti
AFTER INSERT OR UPDATE OR DELETE ON studenti
FOR EACH ROW
EXECUTE FUNCTION trg_audit_studenti();
-- Acum orice modificare în tabela studenti este logată automat!
-- Trigger validare (BEFORE):
CREATE OR REPLACE FUNCTION trg_validare_nota()
RETURNS TRIGGER AS $$
BEGIN
IF NEW.nota_finala IS NOT NULL AND NEW.nota_finala >= 5 THEN
NEW.status := 'promovat';
ELSIF NEW.nota_finala IS NOT NULL THEN
NEW.status := 'nepromovat';
END IF;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER validare_nota
BEFORE INSERT OR UPDATE OF nota_finala ON inscrieri
FOR EACH ROW
EXECUTE FUNCTION trg_validare_nota();
15. Securitate și administrare¶
-- Creare utilizatori:
CREATE USER app_user WITH PASSWORD 'securepassword123';
CREATE USER readonly_user WITH PASSWORD 'readonlypass';
-- Creare roluri:
CREATE ROLE admin_role;
CREATE ROLE reader_role;
-- Acordare privilegii:
GRANT ALL PRIVILEGES ON DATABASE universitate TO admin_role;
GRANT CONNECT ON DATABASE universitate TO reader_role;
GRANT USAGE ON SCHEMA public TO reader_role;
GRANT SELECT ON ALL TABLES IN SCHEMA public TO reader_role;
ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO reader_role;
-- Asignare roluri la utilizatori:
GRANT admin_role TO app_user;
GRANT reader_role TO readonly_user;
-- Revocare:
REVOKE DELETE ON studenti FROM app_user;
-- Row-Level Security (RLS — PostgreSQL):
ALTER TABLE studenti ENABLE ROW LEVEL SECURITY;
CREATE POLICY student_self_access ON studenti
FOR SELECT
TO student_role
USING (email = current_user);
-- Studenții pot vedea DOAR propriul rând!
-- Backup și restore:
-- pg_dump universitate > backup.sql
-- pg_dump -Fc universitate > backup.dump (format custom, comprimat)
-- pg_restore -d universitate backup.dump
-- pg_dump --schema-only universitate > schema.sql (doar structura)
-- pg_dump --data-only universitate > data.sql (doar datele)
PARTEA III — NoSQL¶
16. Introducere în NoSQL¶
16.1 De ce NoSQL?¶
Limitări ale bazelor relaționale la scară mare:
- Schema rigidă → greu de evoluat în aplicații agile
- JOIN-uri costisitoare la volume mari → degradare performanță
- Scalare verticală (server mai puternic) → limită fizică + cost enorm
- Impedance mismatch → obiectele din cod ≠ tabele relaționale
NoSQL oferă:
- Schema flexibilă sau schemaless
- Scalare orizontală (adaugă servere, nu le faci mai mari)
- Modele de date native (documente, grafuri, chei-valoare)
- Performanță la volum masiv (miliarde de rânduri)
16.2 Teorema CAP¶
Într-un sistem distribuit, poți avea MAXIM 2 din 3:
Consistency Availability Partition Tolerance
(toți văd aceleași (sistemul răspunde (funcționează chiar dacă
date simultan) mereu) rețeaua se partițează)
CP: MongoDB, Redis Cluster, HBase
→ Consistență + Toleranță partiții (poate deveni indisponibil temporar)
AP: Cassandra, DynamoDB, CouchDB
→ Disponibilitate + Toleranță partiții (eventual consistent)
CA: PostgreSQL single-node, MySQL single-node
→ Consistență + Disponibilitate (dar fără toleranță la partiții de rețea)
Notă: în practică, Partition Tolerance nu e opțional într-un sistem distribuit.
Deci alegerea reală e între CP și AP.
16.3 Clasificarea NoSQL¶
┌─────────────────────────────────────────────────────────────────────┐
│ NoSQL │
│ │
│ ┌──────────────┐ ┌───────────┐ ┌─────────────┐ ┌────────────┐ │
│ │ Document │ │ Key-Value │ │Column-Family│ │ Graf │ │
│ │ │ │ │ │ │ │ │ │
│ │ MongoDB │ │ Redis │ │ Cassandra │ │ Neo4j │ │
│ │ CouchDB │ │ Memcached │ │ HBase │ │ ArangoDB │ │
│ │ Firestore │ │ DynamoDB │ │ ScyllaDB │ │ Amazon │ │
│ │ │ │ etcd │ │ │ │ Neptune │ │
│ │ │ │ │ │ │ │ │ │
│ │ Structuri │ │ Cel mai │ │ Time-series │ │ Relații │ │
│ │ JSON/BSON │ │ simplu: │ │ IoT, logs, │ │ complexe, │ │
│ │ flexibile, │ │ get/set │ │ write-heavy │ │ traversări│ │
│ │ interogare │ │ ultra- │ │ distribuție │ │ sociale, │ │
│ │ pe câmpuri │ │ rapid │ │ masivă │ │ recomandări│ │
│ └──────────────┘ └───────────┘ └─────────────┘ └────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
17. Baze de date Document (MongoDB)¶
17.1 Concepte¶
Relațional → MongoDB
────────────────────────────────
Database → Database
Table → Collection
Row → Document (BSON/JSON)
Column → Field
Primary Key → _id (generat automat, ObjectId)
JOIN → Embedding / $lookup (rar, by design)
Schema → Schemaless (flexible)
17.2 Operații CRUD MongoDB¶
// === Conectare și selecție bază de date ===
use universitate
// === INSERT ===
db.studenti.insertOne({
nume: "Ana",
prenume: "Pop",
email: "ana.pop@univ.ro",
varsta: 21,
facultate: "Informatică",
an_studiu: 2,
note: [
{ curs: "Algebra", nota: 9, semestru: 1 },
{ curs: "Programare", nota: 10, semestru: 1 },
{ curs: "Baze de date", nota: 9.5, semestru: 2 }
],
adresa: {
oras: "Cluj-Napoca",
strada: "Str. Memorandumului 28",
cod_postal: "400114"
},
activ: true,
data_inscriere: new Date("2023-10-01")
})
db.studenti.insertMany([
{ nume: "Ion", prenume: "Rus", email: "ion@univ.ro", an_studiu: 3, note: [] },
{ nume: "Maria", prenume: "Dima", email: "maria@univ.ro", an_studiu: 1, note: [] }
])
// === FIND (SELECT) ===
// Toți studenții:
db.studenti.find()
// Cu filtrare:
db.studenti.find({ facultate: "Informatică", an_studiu: 2 })
// Proiecție (selectarea câmpurilor):
db.studenti.find(
{ activ: true },
{ nume: 1, prenume: 1, email: 1, _id: 0 } // 1=include, 0=exclude
)
// Operatori de comparație:
db.studenti.find({ varsta: { $gte: 20, $lte: 25 } }) // >= 20 AND <= 25
db.studenti.find({ an_studiu: { $in: [1, 2, 3] } }) // IN
db.studenti.find({ email: { $regex: /^ana/i } }) // Regex
// Interogări pe documente nested:
db.studenti.find({ "adresa.oras": "Cluj-Napoca" })
// Interogări pe array-uri:
db.studenti.find({ "note.curs": "Algebra" }) // Array conține element
db.studenti.find({ "note.nota": { $gte: 9 } }) // Notă >= 9
db.studenti.find({ note: { $size: 3 } }) // Exact 3 note
db.studenti.find({ "note": { $elemMatch: { curs: "Algebra", nota: { $gte: 9 } } } })
// Sortare, limitare, skip:
db.studenti.find().sort({ "an_studiu": -1, "nume": 1 }).limit(10).skip(20)
// === UPDATE ===
// Actualizare un câmp:
db.studenti.updateOne(
{ email: "ana.pop@univ.ro" },
{ $set: { an_studiu: 3, "adresa.oras": "București" } }
)
// Incrementare:
db.studenti.updateOne(
{ email: "ana.pop@univ.ro" },
{ $inc: { varsta: 1 } }
)
// Adăugare în array:
db.studenti.updateOne(
{ email: "ana.pop@univ.ro" },
{ $push: { note: { curs: "Rețele", nota: 8, semestru: 2 } } }
)
// Actualizare multiplă:
db.studenti.updateMany(
{ activ: false, an_studiu: { $gt: 6 } },
{ $set: { exmatriculat: true } }
)
// === DELETE ===
db.studenti.deleteOne({ email: "test@test.com" })
db.studenti.deleteMany({ activ: false, exmatriculat: true })
// === AGGREGATION PIPELINE ===
// Echivalentul GROUP BY + funcții agregate din SQL:
db.studenti.aggregate([
// Stage 1: Filtrare (WHERE)
{ $match: { activ: true } },
// Stage 2: Unwind array-ul de note (transformă fiecare notă într-un document)
{ $unwind: "$note" },
// Stage 3: Grupare (GROUP BY)
{ $group: {
_id: "$facultate",
nr_studenti: { $addToSet: "$_id" },
media_generala: { $avg: "$note.nota" },
nota_maxima: { $max: "$note.nota" }
}},
// Stage 4: Proiecție
{ $project: {
facultate: "$_id",
nr_studenti: { $size: "$nr_studenti" },
media_generala: { $round: ["$media_generala", 2] },
nota_maxima: 1,
_id: 0
}},
// Stage 5: Sortare
{ $sort: { media_generala: -1 } }
])
// === INDEXURI ===
db.studenti.createIndex({ email: 1 }, { unique: true })
db.studenti.createIndex({ facultate: 1, an_studiu: -1 })
db.studenti.createIndex({ "adresa.oras": 1 })
db.studenti.createIndex({ "note.curs": 1 })
// Text index (full-text search):
db.studenti.createIndex({ nume: "text", prenume: "text" })
db.studenti.find({ $text: { $search: "Ana Pop" } })
18. Baze de date Key-Value (Redis)¶
# Redis = structură de date în memorie, ultra-rapid (sub-milisecundă)
# === Strings (cel mai simplu tip) ===
SET user:1:name "Ana Pop"
GET user:1:name # "Ana Pop"
SET counter 0
INCR counter # 1 (atomic!)
INCR counter # 2
INCRBY counter 10 # 12
# Expirare (TTL):
SET session:abc123 '{"user_id":1}' EX 3600 # Expiră în 3600 secunde
TTL session:abc123 # Secunde rămase
# === Hashes (obiecte) ===
HSET student:1 nume "Ana" prenume "Pop" email "ana@univ.ro" an 2
HGET student:1 nume # "Ana"
HGETALL student:1 # Toate câmpurile
HINCRBY student:1 an 1 # an devine 3
# === Lists (cozi, stive) ===
LPUSH notifications:user:1 "Nota nouă la Algebra"
LPUSH notifications:user:1 "Înscris la curs nou"
LRANGE notifications:user:1 0 -1 # Toate notificările
RPOP notifications:user:1 # Scoate cea mai veche (FIFO)
# === Sets (mulțimi — valori unice) ===
SADD cursuri:algebra:studenti 1 2 3 4 5
SADD cursuri:programare:studenti 1 3 5 7 9
# Studenți înscriși la AMBELE cursuri (intersecție):
SINTER cursuri:algebra:studenti cursuri:programare:studenti # 1, 3, 5
# Studenți la Algebra DAR NU la Programare:
SDIFF cursuri:algebra:studenti cursuri:programare:studenti # 2, 4
# === Sorted Sets (clasamente, leaderboards) ===
ZADD leaderboard 9.5 "Ana" 8.0 "Ion" 9.8 "Maria" 7.5 "Andrei"
ZREVRANGE leaderboard 0 2 WITHSCORES # Top 3: Maria 9.8, Ana 9.5, Ion 8.0
ZRANK leaderboard "Ion" # Poziția lui Ion (0-based)
ZINCRBY leaderboard 0.5 "Ion" # Crește scorul lui Ion cu 0.5
# === Pub/Sub (mesagerie în timp real) ===
# Terminal 1 (subscriber):
SUBSCRIBE notifications
# Terminal 2 (publisher):
PUBLISH notifications "Examen programat: Algebra, 15 Ianuarie"
# === Cazuri de utilizare Redis ===
# 1. Cache: stochează rezultate interogări costisitoare
# 2. Sesiuni: stochează sesiuni utilizator (cu TTL)
# 3. Rate limiting: contorizează request-uri per IP
# 4. Leaderboards: clasamente în timp real
# 5. Cozi de job-uri: liste ca message queue
# 6. Pub/Sub: notificări real-time
19. Baze de date Column-Family (Cassandra)¶
-- Cassandra = distribuție masivă, write-optimized, eventual consistent
-- Limbaj: CQL (Cassandra Query Language — similar SQL)
CREATE KEYSPACE universitate
WITH replication = {
'class': 'NetworkTopologyStrategy',
'datacenter1': 3 -- 3 replici în datacenter1
};
USE universitate;
-- Partition Key + Clustering Key:
-- Partition Key: determină pe care nod se stochează datele
-- Clustering Key: determină ordinea rândurilor ÎN partiție
CREATE TABLE note_studenti (
student_id UUID,
curs_id UUID,
semestru INT,
nota DECIMAL,
data_examen TIMESTAMP,
PRIMARY KEY ((student_id), semestru, curs_id)
-- (student_id) = partition key
-- semestru, curs_id = clustering keys (sortare în partiție)
) WITH CLUSTERING ORDER BY (semestru DESC, curs_id ASC);
-- Insert:
INSERT INTO note_studenti (student_id, curs_id, semestru, nota, data_examen)
VALUES (uuid(), uuid(), 1, 9.5, toTimestamp(now()));
-- Query (TREBUIE să includă partition key!):
SELECT * FROM note_studenti WHERE student_id = ?;
SELECT * FROM note_studenti WHERE student_id = ? AND semestru = 1;
-- NU funcționează (fără partition key → full cluster scan):
-- SELECT * FROM note_studenti WHERE nota > 9; -- EROARE fără ALLOW FILTERING!
-- Time-series data (cazul ideal pentru Cassandra):
CREATE TABLE senzori_temperatura (
senzor_id TEXT,
data DATE,
timestamp TIMESTAMP,
valoare DOUBLE,
PRIMARY KEY ((senzor_id, data), timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);
-- Partiția = un senzor pe o zi → interogări rapide per zi per senzor
20. Baze de date Graf (Neo4j)¶
// Neo4j folosește limbajul Cypher
// === Creare noduri ===
CREATE (ana:Student {nume: "Ana Pop", email: "ana@univ.ro", an: 2})
CREATE (ion:Student {nume: "Ion Rus", email: "ion@univ.ro", an: 3})
CREATE (info:Facultate {nume: "Informatică", cod: "INFO"})
CREATE (algebra:Curs {denumire: "Algebra", credite: 5})
CREATE (progr:Curs {denumire: "Programare", credite: 6})
CREATE (prof1:Profesor {nume: "Dr. Popescu", titlu: "Prof."})
// === Creare relații ===
CREATE (ana)-[:STUDIU_LA {din: 2023}]->(info)
CREATE (ion)-[:STUDIU_LA {din: 2022}]->(info)
CREATE (ana)-[:INSCRIS_LA {nota: 9.5, semestru: 1}]->(algebra)
CREATE (ana)-[:INSCRIS_LA {nota: 10, semestru: 1}]->(progr)
CREATE (ion)-[:INSCRIS_LA {nota: 8, semestru: 1}]->(algebra)
CREATE (prof1)-[:PREDA]->(algebra)
CREATE (prof1)-[:PREDA]->(progr)
CREATE (ana)-[:COLEG_CU]->(ion)
// === Interogări ===
// Toți studenții de la Informatică:
MATCH (s:Student)-[:STUDIU_LA]->(f:Facultate {nume: "Informatică"})
RETURN s.nume, s.an
// Cursurile Anei cu note:
MATCH (ana:Student {nume: "Ana Pop"})-[i:INSCRIS_LA]->(c:Curs)
RETURN c.denumire, i.nota
ORDER BY i.nota DESC
// Studenții care au cursuri comune cu Ana:
MATCH (ana:Student {nume: "Ana Pop"})-[:INSCRIS_LA]->(c:Curs)<-[:INSCRIS_LA]-(coleg:Student)
WHERE coleg <> ana
RETURN DISTINCT coleg.nume, COLLECT(c.denumire) AS cursuri_comune
// Calea cea mai scurtă între doi studenți (prin relații de orice tip):
MATCH path = shortestPath(
(a:Student {nume: "Ana Pop"})-[*]-(b:Student {nume: "Ion Rus"})
)
RETURN path
// Recomandare cursuri (studenții similari Anei au mai luat aceste cursuri):
MATCH (ana:Student {nume: "Ana Pop"})-[:INSCRIS_LA]->(c:Curs)<-[:INSCRIS_LA]-(similar:Student)
MATCH (similar)-[:INSCRIS_LA]->(recomandat:Curs)
WHERE NOT (ana)-[:INSCRIS_LA]->(recomandat)
RETURN recomandat.denumire, COUNT(DISTINCT similar) AS scor
ORDER BY scor DESC
LIMIT 5
// Grafurile excelează la:
// - Rețele sociale (prieteni de prieteni)
// - Sisteme de recomandare
// - Detectarea fraudelor (pattern-uri suspecte)
// - Knowledge graphs
// - Analiza dependințelor (software, infrastructură)
PARTEA IV — TEME AVANSATE¶
21. SQL vs. NoSQL — ghid de alegere¶
Folosește SQL (relațional) când:
✅ Datele au structură clară, stabilă, cu relații complexe
✅ Ai nevoie de tranzacții ACID stricte
✅ Ai nevoie de JOIN-uri complexe
✅ Consistența datelor este critică (banking, ERP, inventar)
✅ Ai interogări ad-hoc complexe (rapoarte, analize)
✅ Volumul e sub ~10 TB pe un singur server
Folosește NoSQL când:
✅ Schema se schimbă frecvent (agile, prototipare)
✅ Volume masive cu scalare orizontală necesară
✅ Modelul de date se potrivește nativ (documente, grafuri, time-series)
✅ Write-heavy workloads (loguri, IoT, metrici)
✅ Latență ultra-mică necesară (cache, sesiuni)
✅ Disponibilitate > consistență (rețele sociale, cataloage)
NoSQL NU înseamnă „fără relații" — înseamnă „nu DOAR relațional".
Multe sisteme moderne folosesc AMBELE (polyglot persistence):
PostgreSQL pentru tranzacții + Redis pentru cache + Elasticsearch pentru search
22. ORM-uri și accesul programatic la baze de date¶
22.1 SQLAlchemy (Python ORM)¶
from sqlalchemy import create_engine, Column, Integer, String, Float, ForeignKey, Boolean
from sqlalchemy.orm import declarative_base, relationship, Session
Base = declarative_base()
class Facultate(Base):
__tablename__ = "facultati"
id = Column(Integer, primary_key=True)
nume = Column(String(100), nullable=False, unique=True)
studenti = relationship("Student", back_populates="facultate")
class Student(Base):
__tablename__ = "studenti"
id = Column(Integer, primary_key=True)
nume = Column(String(100), nullable=False)
prenume = Column(String(100), nullable=False)
email = Column(String(150), unique=True, nullable=False)
an_studiu = Column(Integer)
activ = Column(Boolean, default=True)
facultate_id = Column(Integer, ForeignKey("facultati.id"), nullable=False)
facultate = relationship("Facultate", back_populates="studenti")
def __repr__(self):
return f"<Student {self.nume} {self.prenume}>"
# Conectare:
engine = create_engine("postgresql://user:pass@localhost/universitate")
Base.metadata.create_all(engine)
# Operații:
with Session(engine) as session:
# Insert:
fac = Facultate(nume="Informatică")
session.add(fac)
session.flush() # Generează id-ul
student = Student(
nume="Pop", prenume="Ana", email="ana@univ.ro",
an_studiu=2, facultate_id=fac.id
)
session.add(student)
session.commit()
# Query:
studenti = session.query(Student).filter(
Student.activ == True,
Student.an_studiu >= 2
).order_by(Student.nume).all()
for s in studenti:
print(f"{s.nume} {s.prenume} — {s.facultate.nume}")
# Update:
ana = session.query(Student).filter_by(email="ana@univ.ro").first()
ana.an_studiu = 3
session.commit()
# Delete:
session.query(Student).filter(Student.activ == False).delete()
session.commit()
23. Replicare, sharding și baze de date distribuite¶
23.1 Replicare¶
Primary-Replica (Master-Slave):
┌─────────┐ WAL / binlog ┌──────────┐
│ Primary │ ──────────────────► │ Replica 1 │ (read-only)
│ (R/W) │ ──────────────────► │ Replica 2 │ (read-only)
└─────────┘ └──────────┘
Avantaje: scalare citiri, failover, backup
Dezavantaje: lag de replicare (eventual consistency pe replici)
Multi-Primary:
┌──────────┐ ◄──────────────► ┌──────────┐
│ Primary 1│ │ Primary 2│
│ (R/W) │ │ (R/W) │
└──────────┘ └──────────┘
Avantaje: scrieri pe orice nod
Dezavantaje: conflicte de scriere, complexitate
23.2 Sharding (Partițiionare orizontală)¶
Sharding = distribuirea rândurilor pe mai multe servere
Shard Key: student_id (hash-based)
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Shard 1 │ │ Shard 2 │ │ Shard 3 │
│ id 1-33333 │ │ id 33334- │ │ id 66667- │
│ │ │ 66666 │ │ 100000 │
└──────────────┘ └──────────────┘ └──────────────┘
Avantaje: scalare orizontală aproape nelimitată
Dezavantaje: JOIN-uri cross-shard costisitoare, complexitate operațională
24. Data Warehousing și OLAP¶
OLTP (Online Transaction Processing) OLAP (Online Analytical Processing)
────────────────────────────────── ──────────────────────────────────
Operații: INSERT, UPDATE, DELETE Operații: SELECT complex, agregări
Interogări: simple, pe puține rânduri Interogări: complexe, pe milioane de rânduri
Normalizat (3NF) Denormalizat (Star Schema, Snowflake)
Latență: milisecunde Latență: secunde–minute
Exemplu: PostgreSQL, MySQL Exemplu: Snowflake, BigQuery, Redshift
Star Schema:
┌──────────────┐
│ dim_timp │
│ an, luna, zi │
└──────┬───────┘
│
┌──────────────┐ ┌──────┴───────┐ ┌──────────────┐
│ dim_student │──│ fact_note │──│ dim_curs │
│ nume, facult.│ │ student_id │ │ denumire │
└──────────────┘ │ curs_id │ │ credite │
│ timp_id │ └──────────────┘
│ nota │
│ credite │
└──────────────┘
Anexe¶
A. SQL Cheat Sheet¶
-- DDL: CREATE TABLE | ALTER TABLE | DROP TABLE | TRUNCATE
-- DML: INSERT | UPDATE | DELETE | SELECT
-- DCL: GRANT | REVOKE
-- TCL: BEGIN | COMMIT | ROLLBACK | SAVEPOINT
-- Funcții agregate: COUNT, SUM, AVG, MIN, MAX, STRING_AGG
-- Window: ROW_NUMBER, RANK, DENSE_RANK, LAG, LEAD, SUM OVER, AVG OVER
-- Set ops: UNION, UNION ALL, INTERSECT, EXCEPT
-- Joins: INNER, LEFT, RIGHT, FULL OUTER, CROSS, SELF
-- Subquery: IN, EXISTS, ANY, ALL, CTE (WITH), CTE recursiv
B. Comparație rapidă SQL — MongoDB¶
| SQL | MongoDB |
|---|---|
CREATE TABLE t (a INT, b TEXT) |
db.createCollection("t") (implicit la insert) |
INSERT INTO t VALUES (1,'x') |
db.t.insertOne({a:1, b:"x"}) |
SELECT * FROM t WHERE a > 5 |
db.t.find({a: {$gt: 5}}) |
SELECT a, COUNT(*) GROUP BY a |
db.t.aggregate([{$group:{_id:"$a",c:{$sum:1}}}]) |
UPDATE t SET b='y' WHERE a=1 |
db.t.updateOne({a:1}, {$set:{b:"y"}}) |
DELETE FROM t WHERE a=1 |
db.t.deleteOne({a:1}) |
CREATE INDEX ON t(a) |
db.t.createIndex({a:1}) |
JOIN |
$lookup (aggregation) sau embedding |
Curs realizat ca material de referință pentru studenții de informatică, dezvoltatori și administratori de baze de date.