Temă
Analiza bazei de date
Analiză făcută pe 30 octombrie 2026, pe 45 de tabele și 9 migrații, cu datele profilului volume (11.075 de anunțuri, 2.501 comenzi, 2.072 de utilizatori, 10.594 mesaje) și traficul scriptului de performanță plus al suitelor e2e, măsurat cu performance_schema.
Normalizare
Structura e în forma normală a treia. Coloanele care par în plus sunt copii intenționate, fiecare cu un motiv; regulile pnpm db:check verifică cele care se pot desincroniza.
| Copie | Unde | De ce |
|---|---|---|
| ratingul și numărul de recenzii | users.rating_average, rating_count | apar pe fiecare card de anunț; altfel, o agregare pe recenzii la fiecare listare |
| vizualizări și favorite | listing_stats (rând separat, 1:1 cu anunțul) | afișare fără numărare la fiecare cerere; scrierile frecvente nu ating rândul anunțului |
| promovarea curentă | listings.promotion_type, promoted_until | sortarea paginii principale (istoricul complet rămâne în promotions) |
| prețurile comenzii | orders.*_bani | prețul și taxele de la momentul cumpărării, nu cele de azi |
| statutul de firmă al vânzătorului | orders.seller_is_business | dreptul de retragere depinde de statutul de la cumpărare, nu de cel actual |
users păstrează profilul și datele curente ale firmei (afișate pe anunțuri, cerute de DSA, folosite de segmentele CRM și de filtrele Backoffice), deci fără îmbinări suplimentare la citire. Secretele, conturile de plată și istoricul verificărilor au tabele proprii (vezi mai jos).
O bază sau mai multe
Recomandare: o singură bază. Domeniile sunt legate prin chei străine (comenzi ↔ anunțuri ↔ utilizatori, CRM ↔ utilizatori), iar operațiile importante scriu în mai multe domenii în aceeași tranzacție (o comandă: anunțul, comanda, plata, evenimentul CRM). Mai multe baze pe același server nu ar aduce viteză; ar aduce tranzacții și copii de siguranță mai greu de ținut consistente și, pe cPanel, câte un utilizator și drepturi pentru fiecare bază. Separarea pe domenii există deja în cod (apps/api/app/Modules/*).
Indexuri
Adăugate (migrația reporting_indexes), toate pe data creării, pentru perioadele din rapoarte, panoul executiv, segmentele CRM și listele Backoffice „cele mai noi întâi”:
| Interogare | Rânduri citite înainte → după |
|---|---|
| Backoffice: ultimele 25 de comenzi | 2.451 + sortare → 25 |
| Backoffice: ultimii 25 de utilizatori | 2.006 + sortare → 25 |
| utilizatori noi în 30 de zile | 2.006 → 96 (doar din index) |
| consimțăminte pe 30 de zile | 2.682 → 154 |
Pentru comenzile și anunțurile pe ultimele 30 de zile, MySQL citește încă tot tabelul, pentru că în datele de test o mare parte sunt recente și o citire completă e mai ieftină. Pe măsură ce istoricul crește, trece singur pe index. Scriptul de performanță nu arată nicio regresie.
Nu am șters niciun index. Cele raportate ca nefolosite de sys.schema_unused_indexes sunt fie obligatorii pentru cheile străine (InnoDB le cere), fie servesc funcții pe care traficul de test nu le-a atins (jurnalul de audit, consimțămintele cookie), iar indexul full-text al anunțurilor apare greșit în acea listă.
De urmărit pe viitor
- Pagina principală ordonează după „promovat acum”, o expresie care depinde de ora curentă, deci nu poate folosi un index; la 7.000 de anunțuri active durează sub 70 ms. La sute de mii de anunțuri, soluția e o interogare separată pentru anunțurile promovate.
- Căutarea din Backoffice folosește
LIKE '%text%', care nu poate folosi indexuri; la volume mari, aceeași căutare full-text ca pe site.
Implementat: secrete, conturi de plată, contoare, cereri de firmă
Migrația split_secrets_stats_business a mutat, cu datele existente:
| Din | În | De ce |
|---|---|---|
users.password_hash, totp_secret | user_credentials | un utilizator citit pentru un card, o listă CRM sau o căutare din Backoffice nu mai aduce hash-ul parolei și cheia TOTP; doar autentificarea le citește |
users.payout_holder, payout_iban | payout_accounts (cu istoric) | o schimbare de IBAN păstrează contul vechi; echipa financiară vede la plăți un IBAN schimbat în ultimele 7 zile, semnal clasic de fraudă |
listings.views_count, favorites_count | listing_stats | fiecare vizualizare și fiecare favorit scrie un rând mic, nu rândul anunțului editat de vânzător și de moderatori; modificări atomice care creează rândul la prima folosire |
users.business_reviewed_at, business_rejection_reason | business_verifications | fiecare cerere, cu datele trimise, decizia, cine a decis și de ce, ca la verificarea identității |
Migrația a fost verificată valoare cu valoare pe date reale (parole, chei, IBAN-uri, contoare, motive de respingere), în ambele sensuri (up și down), pe MySQL 8 și MariaDB. Cu această ocazie au fost reparate două defecte mai vechi ale ștergerii contului: contoarele de favorite nu scădeau, iar cheia TOTP rămânea salvată. Regulile db:check verifică acum și noile tabele (17 reguli).
De urmărit pe viitor
- Istoricul comenzii ca evenimente (
order_events) și returul ca tabel propriu; istoricul rundelor de moderare. - Localitățile ca date de referință (nomenclatorul SIRUTA), pentru filtre corecte pe județ și căutare după distanță.
- Atributele pe categorii (mărime, an…), pentru validare și filtre.
- Agregate zilnice pentru rapoarte și panoul executiv; reguli de păstrare pentru tabelele care tot cresc; identificatori publici aleatori (ULID).
- Variabilele de eveniment în automatizările CRM (de exemplu numele anunțului în e-mailul „Anunțul tău a expirat”); azi șabloanele folosesc doar
,,.