Optimizare bază de date pentru magazin online
TTFB (Time To First Byte) este jumătate din viteza unui site. Un server care răspunde în 800ms face frontend-ul irelevant — oricâte imagini WebP ai avea. Optimizarea bazei de date taie din acest timp server, cu indexi corecți, query-uri eficiente, cache și tuning pe MySQL/PostgreSQL.
De ce contează TTFB
TTFB = timpul de la cerere HTTP până la primul byte primit. Se descompune în: DNS + conexiune TLS + request processing + query DB + randare server. Într-un magazin online, 60-80% din TTFB este timpul petrecut în bază de date.
Ținte: sub 200ms (excelent), 200-600ms (acceptabil), peste 600ms (problemă), peste 1s (critic). Un TTFB de 800ms înseamnă că LCP-ul nu poate coborî sub 800ms, orice ai face pe frontend.
Indexi compuși — fundația
90% din query-urile lente se rezolvă cu indexi corecți. Greșeala clasică: index pe o singură coloană când filtrarea se face pe mai multe.
Exemplu: listare produse pe categorie, active, sortate:
SELECT * FROM products
WHERE category_id = 42 AND status = 'active'
ORDER BY sort_order DESC LIMIT 20; Index greșit (câte unul per coloană) = DB alege un index, apoi filtrează restul în memorie:
CREATE INDEX idx_category ON products(category_id);
CREATE INDEX idx_status ON products(status); Index corect — compus, în ordinea selectivității:
CREATE INDEX idx_cat_status_sort
ON products(category_id, status, sort_order DESC); Rezultat: query-ul devine index-only scan, nu mai atinge tabelul decât pentru cele 20 de rânduri returnate. Pe 100k produse: de la 350ms la 2ms.
Cum verifici indexii — EXPLAIN
Pune EXPLAIN în fața oricărui query lent:
EXPLAIN SELECT ... ; Citește coloanele cheie:
- type / access type:
ALL= full table scan (rău).index= full index scan (mai bine).range,ref,eq_ref= bine.const= ideal. - rows: estimarea de rânduri citite. Dacă citește 100k ca să returneze 20 = index lipsă.
- Extra:
Using filesortșiUsing temporary= sortare pe disk (rău).Using index= index-only (ideal). - key: indexul ales efectiv. Dacă e
NULL, niciun index nu a fost folosit.
Query N+1 — cel mai frecquent asasin de performanță
Pattern clasic cu ORM-uri (Eloquent, Django ORM, Prisma, Hibernate): încarcă o listă de produse, apoi pentru fiecare produs încarcă categoria sa, într-o buclă.
// RĂU — 101 query-uri
products = Product.objects.all()[:100]
for p in products:
print(p.category.name) # 1 query per produs // BINE — 1 query cu JOIN (eager loading)
products = Product.objects.select_related('category').all()[:100]
for p in products:
print(p.category.name) # 0 query-uri suplimentare În Eloquent (Laravel): Product::with('category')->take(100)->get(). În Prisma: prisma.product.findMany({ include: { category: true } }). Detectează N+1 cu query logging — dacă vezi 100+ query-uri pe o singură pagină, îl ai.
Pool de conexiuni — overhead ascuns
Deschiderea unei conexiuni DB durează 20-50ms (handshake TCP + auth). Dacă aplicația deschide conexiune per request, 1000 req/s = 20-50s doar în handshakes.
- PostgreSQL: PgBouncer în modul transaction pooling. Reutilizează conexiuni între request-uri.
- MySQL: ProxySQL sau pool nativ în framework (HikariCP pe Java, mysql2 pool pe Node).
- Setări:
pool_sizeegal cu (CPU cores × 2) + disk spindles. Prea mare = OOM, prea mic = queue.
Cache Redis — 90%+ hit rate
Catalogul de produse se schimbă rar, dar se citește la fiecare request. Perfect pentru cache. Ordine de prioritizare:
- Object cache (WordPress/WooCommerce): plugin Redis Object Cache. Elimină 80% din query-uri — un WooCommerce lent devine rapid instant.
- Query cache aplicație: rezultatul query-urilor „top produse", „categorii", „recomandări" cache-uite cu TTL 5-60 min.
- Full page cache: pagini statice (home, categorie, produs) servite direct din Redis/Nginx. Invalidare la editare produs.
- Sesiuni: mută sesiunile din DB/filesystem în Redis. Mai rapid, distribuit.
Evită capcana: cache fără strategie de invalidare = date vechi (preț greșit, stoc expirat). Folosește key versioning sau event-driven invalidation.
Partiționare — pentru tabele mari
Tabelele peste 1M rânduri beneficiază de partiționare. Clasic pentru magazin: orders partiționat pe lună:
-- PostgreSQL partiționare declarativă
CREATE TABLE orders (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
customer_id BIGINT,
total DECIMAL(10,2)
) PARTITION BY RANGE (created_at);
CREATE TABLE orders_2026_07 PARTITION OF orders
FOR VALUES FROM ('2026-07-01') TO ('2026-08-01'); Query pe luna curentă citește doar partiția lunii curente (30 zile), nu tot istoricul de 5 ani. Indexii sunt per-partiție — mai mici, mai rapizi. Maintenance (VACUUM, ANALYZE) poate rula per-partiție fără să blocheze tot tabelul.
Tuning MySQL / PostgreSQL — setări cu impact mare
MySQL (my.cnf)
innodb_buffer_pool_size= 50-70% din RAM total. Cea mai importantă setare.innodb_buffer_pool_instances= 1 per GB de buffer pool.innodb_log_file_size= 256MB-1GB pentru magazine cu multe scrieri.query_cache_type = OFF— în MySQL 8+ a fost eliminat, pe 5.7 cauzează lock contention.max_connections= pool_size × număr instanțe aplicație + margină.
PostgreSQL (postgresql.conf)
shared_buffers= 25% din RAM.effective_cache_size= 50-75% din RAM (estimare, nu alocă).work_mem= 4-16MB (sort/hash per query; mare = mai multă memorie per conexiune).maintenance_work_mem= 256MB-1GB (VACUUM, CREATE INDEX).random_page_cost= 1.1 pe SSD (default 4.0 e pentru disk mecanic).autovacuum = on,autovacuum_naptimeagresiv pe tabele cu multe scrieri.
Slow query log — instrumentul de diagnostic
Pornește slow query log și analizează-l săptămânal. MySQL:
[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow.log
long_query_time = 0.1 # 100ms
log_queries_not_using_indexes = 1 PostgreSQL: log_min_duration_statement = 100. Analizează cu pt-query-digest (MySQL) sau pg_stat_statements (PostgreSQL). Top 10 query-uri lente = top 10 oportunități de optimizare.
Caz concret — WooCommerce lent la checkout
Magazin cu 8.000 produse, checkout dura 4-8 secunde. Cauze găsite prin slow log:
- Query N+1: la calcul shipping, câte un query per produs din coș (5-15 query-uri).
- Lipsă index pe
wp_woocommerce_order_items(order_id). - Object cache inactiv (WordPress folosea filesystem).
Intervenții: eager loading pe shipping, index adăugat, Redis Object Cache activat. Rezultat: checkout 4-8s → 0,8-1,2s. Conversie +23% în 30 zile.