Progresele pe care produsele open source le înregistrează la fiecare nouă versiune majoră nu se mai încadrează la capitolul surprize. Şi totuşi, atunci când este vorba despre servere de baze de date, ele trebuie salutate, semnalate şi evaluate. Este vorba de produse extrem de complexe şi de pretenţioase, având în vedere că adesea sunt piese vitale într-un sistem informatic.
În urmă cu doi ani am publicat o evaluare comparativă a celor mai populare SGBD-uri open source ("MySQL şi PostgreSQL" în NET Report 102, martie 2001, disponibil online pe situl revistei). Recitind acel articol, am constatat cu plăcere că ambelor sisteme li s-au adus ameliorări semnificative. MySQL a introdus suportul pentru tranzacţii şi pentru restricţii FOREIGN KEY (prin tabele InnoDB), a apărut UNION (nu însa şi celelalte operaţii cu seturi) iar versiunea 4.1 (în fază alfa) aduce mult-aşteptatele subqueries. Lipsesc în continuare tabele virtuale (views), procedurile stocate şi "declanşatoarele" (triggers), dar toate figurează în planurile pentru versiunile viitoare.
La acea vreme PostgreSQL 7.1 era în fază beta iar saltul faţă de versiunile precedente era semnificativ. Versiunea 7.2 s-a concentrat asupra optimizărilor specifice pentru volume mari de date (onorând astfel interesul crescând al întreprinderilor), dar nu a adus multe noutăţi majore în privinţa funcţionalităţii. Din acest motiv, această prezentare se va concentra asupra diferenţelor faţă de versiunea 7.1.
Lista noilor caracteristici pe care le voi prezenta nu este, desigur, exhaustivă. Găsiţi altfel de liste pe web (la developer.postgresql.org/docs/postgres/release-7-3.html şi developer.postgresql.org/docs/postgres/release-7-2.html). Am selectat câteva care consider că sunt de interes mai larg şi am încercat să furnizez câteva exemple de utilizare.
Comodităţi
Am inclus în această secţiune caracteristici noi care, deşi nu sunt esenţiale, elimină unele inconveniente în utilizarea sistemului şi adaugă un plus de expresivitate.
Domenii
Un domeniu este un nume dat unui tip de date împreună cu câteva caracteristici opţionale: valoarea implicită (default) şi restricţia NOT NULL. Odată ce un domeniu este definit, el poate fi utilizat în definirea tabelelor:
CREATE DOMAIN juds AS VARCHAR(2) DEFAULT 'B' NOT NULL;
CREATE TABLE Adresa (
...
codjud juds,
...
);
Pe lângă faptul că avem mai puţin de scris, utilizarea domeniilor este un element de stil foarte important, care ne poate ajuta să creăm structuri mai consistente şi mai expresive. Ca regulă generală, o condiţie de joncţiune (join) trebuie să implice doar coloane definite pe acelaşi domeniu.
Implementarea domeniilor este la început şi este departe de "adevăratele domenii relaţionale" de care vorbesc teoreticienii (aspect care merită tratat pe larg), însă lucrurile evoluează rapid. În versiunea 7.4 (acum în fază alfa) apar câteva caracteristici noi. În primul rând se adaugă restricţia CHECK, astfel încât vom putea scrie:
CREATE DOMAIN juds AS VARCHAR(2) DEFAULT 'B' NOT NULL
CHECK (VALUE IN ('AB', 'AG', 'AR', ... 'VN', 'VS'));
Pe de alta parte, va apărea şi instrucţiunea ALTER DOMAIN, astfel încât domeniile vor deveni o posibilitate reală de gestionare centralizată a tipurilor specifice utilizate într-o bază de date.
Un DROP mai deştept
O nouă funcţionalitate pe care o aduce versiunea 7.3 este urmărirea relaţiilor de dependenţă dintre obiectele bazei de date (dependency tracking). Caracteristica ţine mai mult de "bucătăria internă", dar are câteva efecte de suprafaţă, în special la instrucţiunile DROP (pe care le voi numi de eliminare, spre a le diferenţa de cele de ştergere). De pildă, dacă în contextul paragrafului precedent vom încerca instrucţiunea:
DROP DOMAIN juds;
vom obţine un mesaj de eroare, deoarece tabela Adresa depinde de această definiţie. Avem acum opţiunile CASCADE sau RESTRICT pentru majoritatea instrucţiunilor de eliminare (excepţie fac doar cele referitoare la administrarea utilizatorilor şi drop database) precum şi pentru câteva versiuni de ALTER TABLE. Varianta implicită este RESTRICT, aşa că dacă vrem să eliminăm domeniul împreună cu toate coloanele care se bazează pe el vom scrie:
DROP DOMAIN juds CASCADE;
Tot în acest paragraf merită amintit că a fost eliminat un efect supărător al utilizării coloanelor SERIAL. Acum instrucţiunea DROP TABLE va elimina şi secvenţele create pentru aceste coloane, astfel încât atunci când vom crea din nou tabela nu vom obţine eroare. În plus, SERIAL nu mai este considerat implicit UNIQUE, aşa că nu se mai creează un index.
În fine, pentru funcţii, reguli (rules) şi tabele virtuale (views) sintaxa CREATE OR REPLACE ne scuteşte în faza de dezvoltare de utilizarea instrucţiunilor DROP. Această facilitate este foarte comodă mai cu seamă la dezvoltarea declanşatoarelor (triggers), deoarece corectarea funcţiei nu mai implică re-crearea trigger-ului (acesta va lucra mereu cu ultima versiune a funcţiei).
Un ALTER mai complet
În comparaţia cu MySQL de care aminteam, unul dintre punctele slabe ale lui PostgreSQL era legat de posibilităţile de modificare a tabelelor. Practic, adăugarea unei simple clauze NOT NULL implica refacerea tabelei, trecând datele printr-o tabelă temporară.
Acum lucrurile stau cu totul altfel, aşa încât se pot adăuga coloane împreună cu restricţiile la nivel de coloană (inclusiv NULL sau NOT NULL), se pot schimba opţiunile de stocare la nivel de coloană şi se poate stabili modul de culegere a datelor statistice pentru coloane.
Noutăţile cele mai spectaculoase sunt însă posibilităţile de eliminare a coloanelor şi a restricţiilor la nivel de tabelă - ambele cu opţiunile CASCADE sau RESTRICT privind eliminarea obiectelor dependente. Aceste posibilităţi sunt foarte importante deoarece elimină situaţiile în care o tabelă trebuie re-creată în faza de exploatare. Practic, cu ALTER TABLE se poate face aproape orice cu o tabelă, chiar dacă conţine milioane de linii. E adevărat că adesea se lucrează în mai mulţi paşi, dar modificarile de structură sunt operaţii extrem de sensibile şi efectele laterale trebuie atent urmărite.
De pildă, să presupunem că vrem să schimbam tipul unei coloane din integer în text, în condiţiile în care coloana face parte dintr-o cheie primară şi este restricţionată cu un CHECK. Definiţia iniţială este:
CREATE TABLE Exemplu (
codOp INTEGER
CHECK (codOp IN (0, 1, 3, 7)),
tipOp CHAR(2),
descr TEXT,
PRIMARY KEY (codOp, tipOp)
);
Operaţiile necesare sunt următoarele:
ALTER TABLE Exemplu
RENAME codOp TO codOp_old;
Am redenumit coloana codOp.
ALTER TABLE Exemplu
ADD codOp text
CONSTRAINT ex_op
CHECK (codOp IN ('0', '1', '3', '7'));
Am adăugat o coloană numita codOp, cu restricţia CHECK corespunzătoare. Posibilitatea de a impune restricţia la adăugarea coloanei este nouă. În versiunile precedente puteam doar adăuga restricţia la nivel de tabelă.
UPDATE Exemplu
SET codOp = codOp_old::text;
Populăm noua coloană prin conversia valorilor întregi din codOp_old.
ALTER TABLE Exemplu
DROP COLUMN codOp_old CASCADE;
Eliminăm coloana veche. Utilizarea clauzei CASCADE va elimina alte obiecte care depind de coloana eliminată, de pildă views sau restricţii foreign key. De notat că odată cu coloana vor fi eliminaţi automat indecşi bazaţi pe aceasta (indiferent de clauza CASCADE/RESTRICT), în cazul nostru indexul exemplu_pkey (creat automat la declararea cheii primare).
ALTER TABLE Exemplu
ALTER codOp SET NOT NULL;
Trebuie să setăm coloana la NOT NULL, deoarece altfel nu o vom putea folosi într-o cheie primară.
ALTER TABLE Exemplu
ADD PRIMARY KEY (codOp, tipOp);
Adaugăm cheia primară. Aceasta este o restricţie (constraint) la nivel de tabelă.
Câteva observaţii:
O idee bună este efectuarea tuturor acestor operaţii într-o singură tranzacţie, încadrându-le între BEGIN şi COMMIT. Avantajul este că în cazul unei erori se va produce un ROLLBACK şi vom ramâne cu tabela "curată".
Este important de remarcat că aceste operaţii afectează ordinea coloanelor. Teoria relaţională nu oboseşte să sublinieze că ordinea coloanelor este irelevantă (ca şi a liniilor de altfel), ceea ce nu a împiedicat producătorii să implementeze în SQL operaţii bazate pe ordinea coloanelor. Nu vă bazaţi pe această ordine.
Nu încercaţi aşa ceva pe baze de date aflate în exploatare! E doar un exemplu didactic. În practică, efectele unor asemenea manevre sunt extrem de periculoase şi afectează toate aplicaţiile care utilizează baza de date. Dacă totuşi trebuie să faceţi modificări structurale, folosiţi tabele virtuale şi reguli.
În MySQL toată această schimbare se poate face dintr-un foc (ALTER TABLE cu CHANGE). Nu vă recomand.
Scheme
O schemă (schema) poate fi considerată un spaţiu de nume (namespace) într-o baza de date. Ideea este că - spre deosebire de multe alte SGBD-uri - PostgreSQL nu permite accesul dintr-o anume bază de date la obiectele dintr-o altă bază de date. Cu toate acestea, această facilitate ar putea fi în anumite situaţii foarte utilă. Din această perspectivă, o schemă poate fi considerată ca o bază de date independentă (într-o oarecare măsură) în cadrul unei alte baze de date.
Orice bază de date dispune implicit de o schemă numită public. Dacă vom folosi baza de date aşa cum o făceam cu versiunile precedente, toate operaţiile se vor referi la obiectele din această schemă. Putem însă crea şi alte scheme:
CREATE SCHEMA mir;
Putem acum să folosim schema mir ca o bază de date independentă, cu condiţia să calificăm numele obiectelor prin notaţia cu punct: numeSchema.numeObiect. De pildă putem crea o tabelă în această schemă:
CREATE TABLE mir.Exemplu (
cod SERIAL PRIMARY KEY,
info TEXT
);
Trebuie să remarcăm că, pe lângă tabelă, în schema mir s-a creat atât secvenţa implicită exemplu_cod_seq corespunzătoare câmpului declarat SERIAL cât şi indexul exemplu_pkey corespunzător cheii primare.
Deoarece schemele sunt spaţii de nume, înseamnă că putem avea obiecte cu numele Exemplu şi în alte scheme. Ce se întâmplă însă dacă ne referim la un obiect fără să-i calificăm numele prin numele schemei? În general, comportamentul sistemului este cel aşteptat: va căuta obiectul în schema public. De fapt, mecanismul de căutare este definit de o variabilă a bazei de date numită search_path, pe care o putem consulta:
SHOW search_path;
În mod normal, vom obţine un rezultat de forma:
search_path
------------
$user,public
Aceasta înseamnă că în mod implicit, sistemul va căuta obiectul într-o schemă proprie utilizatorului (adică având numele identic cu username-ul curent), iar dacă nu există o astfel de schema va căuta în schema public.
Putem să schimbăm această ordine:
SET search_path TO mir,public;
Am stabilit astfel că schema mir este schema curentă şi obiectele ne-calificate vor fi căutate mai întâi în această schemă. Această setare este valabilă doar pe durata sesiunii.
Se poate discuta despre drepturi şi privilegii în cadrul schemelor, dar în cadrul acestei prezentări este mai important să subliniem posibilele utilizari.
În primul rând, schemele permit organizarea obiectelor unei baze de date în grupuri logice, astfel încât administrarea să fie mai comodă. Se poate concepe, de pildă, o organizare în care obiectele corespunzătoare unor anumite aplicaţii să fie grupate într-o schemă, păstrând însă posibilitatea accesului la alte obiecte.
O altă posibilitate este de a integra aplicaţii de la terţi fără a risca conflicte de nume.
O posibilitate interesantă se oferă furnizorilor de servicii în regim ASP (application service providers). Astfel, o aceeaşi aplicaţie poate rula pentru mai mulţi clienţi, fiecare dispunând de o schemă proprie. E suficient ca la conectare să se identifice clientul şi să se stabilească variabila search_path.
Funcţii tabelare
Funcţiile SQL care returnează seturi nu sunt o noutate. Iată un exemplu simplu:
CREATE FUNCTION GetDep (int) RETURNS SETOF Ang AS '
SELECT * FROM Ang WHERE dep = $1;
' LANGUAGE 'SQL';
Funcţia returnează liniile din tabela Ang corespunzătoare angajaţilor dintr-un anumit departament furnizat ca parametru printr-un cod (număr întreg). Problema era că manevrarea setului rezultat era foarte incomodă. Ca să obţinem marca şi numele angajaţilor trebuia să scriem:
SELECT marca(GetDEp(1)), nume(GetDep(1));
Noutăţile pe care le aduce versiunea 7.3 sunt două. În primul rând, funcţiile tabelare (table functions - funcţii care returnează seturi de înregistrări) pot fi acum folosite în clauza FROM. Astfel, putem scrie selecţii de genul următor:
SELECT marca, nume FROM GetDep(1)
WHERE salar > 12000000;
În al doilea rând, putem scrie acum şi funcţii PLPGSQL care să returneze seturi. Pentru aceasta avem la dispoziţie instrucţiunea RETURN NEXT care returnează o valoare (poate fi un articol) fără a opri execuţia funcţiei. De pildă, funcţia de mai sus o putem scrie în PLPGSQL astfel:
CREATE OR REPLACE FUNCTION GetDep (int)
RETURNS SETOF Ang AS '
DECLARE
a Ang%ROWTYPE;
BEGIN
FOR a IN SELECT * FROM Ang WHERE dep = $1 LOOP
RETURN NEXT a;
END LOOP;
RETURN;
END;
' LANGUAGE 'PLPGSQL';
RETURN NEXT va returna liniile iar RETURN va termina execuţia. Rezultatul poate fi folosit exact ca în cazul variantei SQL.
Desigur, exemplele de mai sus sunt simpliste. Cele mai multe situaţii în care avem nevoie de seturi de articole pot fi implementate mai simplu cu un view, dar există situaţii în care determinarea liniilor care trebuie selectate se face pe baza unor condiţii care nu pot fi exprimate decât procedural, caz în care funcţiile tabelare sunt sigura opţiune posibilă.
Altele
Dintre noutăţile pe care nu le-am tratat în această prezentare trebuie măcar să amintesc câteva care pot fi importante pentru unii utilizatori.
Există posibilitatea de a pregăti un plan de execuţie pentru o anume interogare, chiar parametrizată. Acest plan este stocat de serverul de baze de date şi, de regulă, se traduce printr-un spor de performanţă care poate fi semnificativ în cazul interogărilor care se folosesc în mod repetat. Spre exemplu, putem să pregătim un plan astfel:
PREPARE GetAng (int) AS
SELECT * FROM Ang WHERE dep = $1
ORDER BY nume;
Apoi, cererea poate fi executată folosind planul, astfel:
EXECUTE GetAng (1);
Este disponibil limbajul procedural PL/Python. Deocamdată funcţionalitatea sa este limitată (de pildă lipseşte tratarea excepţiilor în accesul la baza de date), dar există speranţe că versiunile viitoare vor aduce multe îmbunătăţiri. Este cert însă că pentru anumite procesari (de pilda cele care implică stringuri sau structuri complexe) funcţiile Python pot fi mult mai comode decât cele scrise în PLPGSQL.
Au apărut facilităţi suplimentare pentru lucrul cu stringuri. Dintre acestea menţionez operatorul SIMILAR TO, care aduce expresiile regulate definite de SQL99. Acestea sunt mai puternice decât cele suportate de LIKE şi acceptă câteva dintre metacaracterele din expresiile regulate POSIX. În plus, a apărut şi o formă nouă a funcţiei Substring care foloseşte operatorul SIMILAR TO. Extrem de utilă este şi funcţia Overlay, care înlocuieşte un substring cu altul.
Concluzie
Una peste alta, PostgreSQL devine un rival demn de luat în seama în competiţia cu SGBD-urile comerciale importante. Iar eforturile proiectanţilor de a face sistemul cât mai Oracle-like sugerează că de acum PostgreSQL nu mai este o jucărie, ci vizează clar mediile corporatiste.