Optimització de queries
La lectura i interpretació del pla d'execució EXPLAIN ANALYZE és la habilitat fonamental per diagnosticar i optimitzar consultes SQL lentes sobre grans volums de dades. Sense entendre el pla, qualsevol optimització és una aposta a cegues.
Com funciona l'optimitzador d'un SGBD
Quan un SGBD rep una consulta SQL, no l'executa directament. Primer la passa per l'optimitzador (query planner), que genera un pla d'execució òptim seguint aquests passos:
- Anàlisi sintàctica i semàntica: verifica que la consulta és correcta i resol els noms de taules i columnes.
- Generació de plans candidats: enumera diverses maneres d'executar la consulta (quin índex usar, quin algorisme de JOIN, en quin ordre llegir les taules).
- Estimació de costos: per a cada pla candidat, estima el cost total basant-se en les estadístiques de la base de dades (
ANALYZE). - Selecció del pla mínim cost: tria el pla amb el cost estimat més baix.
El model de costos
Cada motor estima el cost d'un pla a partir d'un model propi, normalment basat en el cost d'accedir a disc (seqüencial vs aleatori), el cost de CPU per fila processada i la memòria disponible per a cache. La filosofia és semblant als 4 motors, però el grau de control manual que cada un ofereix sobre aquest model és molt diferent, com es veu a continuació:
El cost s'expressa en unitats abstractes basades en dos paràmetres clau de postgresql.conf:
seq_page_cost(per defecte 1.0): cost de llegir una pàgina del disc en accés seqüencial.random_page_cost(per defecte 4.0): cost de llegir una pàgina en accés aleatori (salt de disc).
En sistemes amb SSD, random_page_cost s'hauria de reduir (típicament a 1.1–1.5), perquè els discos SSD no pateixen la penalització del seek mecànic. Si el valor és massa alt, PostgreSQL preferirà Seq Scans innecessaris.
MySQL/MariaDB no exposa paràmetres de cost de pàgina equivalents a seq_page_cost/random_page_cost de forma directa: l'optimitzador intern (cost-based optimizer des de MySQL 5.7) no permet aquest tipus d'ajust manual fi. El control real que es té és sobre la mida de la cache de dades i la freqüència d'actualització d'estadístiques:
SQL Server no té un equivalent directe a random_page_cost. El Cardinality Estimator (CE) i el cost-based optimizer fan servir estadístiques i un model de costos intern que no és parametritzable manualment de la mateixa manera que a PostgreSQL o Oracle. La influència que es té és indirecta: hints de consulta, OPTION (RECOMPILE) o ajustant la memòria disponible per a la cache de pàgines:
Oracle sí té control explícit sobre el model de costos, similar a PostgreSQL, mitjançant les System Statistics, que capturen el rendiment real de l'I/O i la CPU del sistema:
Nivells de control molt diferents segons el motor
PostgreSQL i Oracle permeten ajustar explícitament el model de costos de l'optimitzador. MySQL/MariaDB i SQL Server, en canvi, automatitzen molt més aquesta part i ofereixen poc marge d'ajust manual directe: cal confiar en la qualitat de les estadístiques i en la memòria disponible per a la cache.
Totes les estadístiques s'actualitzen automàticament en segon pla (per autovacuum a PostgreSQL, per processos equivalents als altres motors), però en càrregues d'escriptura intensa pot ser necessari forçar-les i consultar-les manualment:
-- Actualitzar estadístiques d'una taula concreta
ANALYZE vendes;
-- Veure les estadístiques actuals d'una columna
SELECT attname, n_distinct, correlation
FROM pg_stats
WHERE tablename = 'vendes' AND attname = 'pais';
-- correlation propera a 1.0 = dades físicament ordenades (bon candidat per a BRIN)
-- Actualitzar estadístiques d'una taula concreta
ANALYZE TABLE vendes;
-- Veure estadístiques de cardinalitat dels índexs
SELECT index_name, column_name, cardinality
FROM information_schema.statistics
WHERE table_name = 'vendes' AND table_schema = DATABASE();
MySQL no exposa una mètrica de correlació física equivalent a correlation de PostgreSQL.
EXPLAIN: llegir el pla sense executar
EXPLAIN mostra el pla d'execució estimat sense executar la consulta. Útil per a consultes molt lentes on no volem esperar.
Sortida típica:
Retorna una taula amb columnes id, select_type, table, type, possible_keys, key, rows, Extra. El valor de type indica el mètode d'accés:
type = constoref: ús d'índex (selectivitat alta).type = ALL: escaneig complet de la taula, equivalent aSeq Scande PostgreSQL.
A la pràctica, és més habitual consultar el pla estimat gràfic a SSMS (Ctrl+L) en comptes del format text. Operadors típics que hi apareixen: "Clustered Index Seek", "Index Scan", "Key Lookup".
Els noms de camp següents (cost, rows, width) corresponen al format de sortida de PostgreSQL. Cada node del pla mostra:
cost=inici..total: cost estimat. El primer número és el startup cost (cost fins a retornar la primera fila); el segon és el total cost (cost fins a retornar totes les files).rows: nombre de files estimades que retornarà el node.width: amplada mitjana de cada fila en bytes.
Als altres motors la mateixa informació es presenta amb noms diferents, tal com s'ha vist a les pestanyes anteriors: la columna rows i el camp Extra a l'EXPLAIN de MySQL/MariaDB, els operadors del pla gràfic d'SSMS a SQL Server, i les columnes Cost, Cardinality i Bytes de DBMS_XPLAN.DISPLAY a Oracle.
Llegir un pla niuat
Els plans s'llegeixen de dins cap a fora (de la indentació més profunda cap a la superfície):
EXPLAIN SELECT v.*, c.nom
FROM vendes v
JOIN clients c ON c.id = v.client_id
WHERE v.data_venda >= '2026-01-01';
Hash Join (cost=1234.00..89234.00 rows=50000 width=120)
Hash Cond: (v.client_id = c.id)
-> Seq Scan on vendes v (cost=0.00..75000.00 rows=50000 width=56)
Filter: (data_venda >= '2026-01-01')
-> Hash (cost=800.00..800.00 rows=34400 width=64)
-> Seq Scan on clients c (cost=0.00..800.00 rows=34400 width=64)
S'executa primer: 1) Seq Scan sobre vendes, 2) Seq Scan sobre clients per construir la taula hash, 3) Hash Join combinant els resultats.
EXPLAIN FORMAT=TREE
SELECT v.*, c.nom
FROM vendes v
JOIN clients c ON c.id = v.client_id
WHERE v.data_venda >= '2026-01-01';
El format TREE (disponible des de MySQL 8.0.16) mostra una estructura jeràrquica similar a la de PostgreSQL, amb indentació per nivells i el cost estimat de cada node.
El pla gràfic mostra els operadors en forma d'arbre i es llegeix de dreta a esquerra i de dalt a baix. Els operadors equivalents més habituals són "Hash Match" (per a hash joins), "Nested Loops" i "Merge Join". S'obté amb el pla estimat (Ctrl+L) o l'execucional (Ctrl+M) a SSMS.
EXPLAIN PLAN FOR
SELECT v.*, c.nom
FROM vendes v
JOIN clients c ON c.id = v.client_id
WHERE v.data_venda >= DATE '2026-01-01';
SELECT * FROM TABLE(DBMS_XPLAN.DISPLAY);
DBMS_XPLAN.DISPLAY mostra una estructura amb columnes Id, Operation i sagnat segons la profunditat. Es llegeix de dins (més sagnat) cap a fora, igual que a PostgreSQL.
EXPLAIN ANALYZE: temps reals d'execució
EXPLAIN ANALYZE executa la consulta i afegeix al pla els temps reals i el nombre real de files processades.
EXPLAIN ANALYZE executa la consulta
Per a DELETE, UPDATE o INSERT, emboliqueu-ho en una transacció i feu rollback: BEGIN; EXPLAIN ANALYZE DELETE ...; ROLLBACK;
Des de MySQL 8.0.18, EXPLAIN ANALYZE executa la consulta i retorna un format d'arbre textual, conceptualment molt similar al de PostgreSQL:
La sortida inclou actual time=X..Y rows=N loops=M per a cada node de l'arbre, igual que el actual time de PostgreSQL.
SQL Server no té una comanda "EXPLAIN ANALYZE" directa. L'equivalent s'aconsegueix activant SET STATISTICS TIME, IO ON abans de la consulta (dona temps de CPU/transcorregut i lectures lògiques/físiques per taula), combinat amb el "Actual Execution Plan" (Ctrl+M a SSMS), que afegeix files reals processades a cada operador del pla gràfic:
Els camps addicionals (format de sortida de PostgreSQL i, de forma molt similar, del EXPLAIN ANALYZE de MySQL/MariaDB 8.0.18+):
actual time=inici..fi: temps real en mil·lisegons fins a la primera fila i fins a l'última.rows: nombre real de files processades. Si difereix molt derowsestimat, les estadístiques estan desactualitzades.loops: quantes vegades s'ha executat aquest node (rellevant en Nested Loops).
A SQL Server, la informació equivalent (temps i lectures reals) prové de SET STATISTICS TIME, IO ON combinat amb el pla d'execució real (Ctrl+M); a Oracle, DISPLAY_CURSOR(..., 'ALLSTATS LAST') mostra E-Rows (estimades) i A-Rows (reals) costat a costat per a cada operador.
La trampa de loops
En un Nested Loop, el node interior s'executa tantes vegades com files retorna el node exterior. El actual time mostrat és el temps per iteració, no el total:
Nested Loop (actual time=0.1..150.0 rows=10000 loops=1)
-> Seq Scan on comandes (actual time=0.05..50.0 rows=1000 loops=1)
-> Index Scan on linies (actual time=0.01..0.08 rows=10 loops=1000)
El temps total del node interior és 0.08 ms × 1000 = 80 ms, no 0.08 ms.
Aquesta convenció és pròpia de PostgreSQL i MySQL
La multiplicació manual actual time × loops és necessària a PostgreSQL i al EXPLAIN ANALYZE de MySQL/MariaDB (que reprodueix el mateix format). A SQL Server, el pla d'execució real mostra directament el nombre total de files processades per operador ("Actual Number of Rows", ja agregat). A Oracle, DISPLAY_CURSOR amb ALLSTATS LAST també mostra A-Rows ja acumulat per a totes les execucions del node, sense necessitat de multiplicar manualment.
EXPLAIN (ANALYZE, BUFFERS): lectures de cache vs disc
Afegint BUFFERS, el pla mostra quantes pàgines de dades s'han llegit de la cache de PostgreSQL (shared buffers) i quantes del disc:
EXPLAIN (ANALYZE, BUFFERS, FORMAT TEXT)
SELECT * FROM vendes WHERE pais = 'ES' AND data_venda >= '2026-01-01';
Bitmap Heap Scan on vendes (actual time=45.2..890.3 rows=2500000 width=56)
Recheck Cond: (pais = 'ES')
Buffers: shared hit=12000 read=85000
-> Bitmap Index Scan on idx_vendes_pais
Index Cond: (pais = 'ES')
Buffers: shared hit=250 read=1200
shared hit: pàgines servides des de la cache (ràpid).read: pàgines llegides del disc (lent). Un valor alt dereadindica que la taula o l'índex no cap a la cache; es pot considerar augmentarshared_buffers.
No hi ha una opció integrada dins EXPLAIN equivalent a BUFFERS. Cal consultar-ho per separat:
SET STATISTICS IO ON ja mostra logical reads (cache) i physical reads (disc) per taula directament al missatge de sortida, de forma molt similar al BUFFERS de PostgreSQL:
L'hint GATHER_PLAN_STATISTICS combinat amb DISPLAY_CURSOR(..., 'ALLSTATS LAST') ja inclou la columna Buffers (blocs lògics llegits) directament al pla, igual que EXPLAIN (ANALYZE, BUFFERS) de PostgreSQL:
Nodes principals del pla d'execució
| Node (PostgreSQL) | Descripció | MySQL/MariaDB | SQL Server | Oracle |
|---|---|---|---|---|
Seq Scan |
Llegeix tota la taula seqüencialment. Normal per a taules petites o quan es retorna >10-20% de les files. | type = ALL |
"Table Scan" / "Clustered Index Scan" | TABLE ACCESS FULL |
Index Scan |
Segueix l'índex fila per fila. Òptim per a selectivitat alta (poques files). | type = ref / range |
"Index Seek" | INDEX RANGE SCAN |
Bitmap Index Scan |
Recull punteres de l'índex en un bitmap i accedeix al disc en ordre físic. Millor que Index Scan per a selectivitat mitjana. | sense equivalent directe (optimitzador intern diferent) | "Index Seek" + "Key Lookup" combinats | BITMAP INDEX SCAN |
Hash Join |
Construeix una taula hash del conjunt petit i en fa cerca per cada fila del gran. Òptim quan un dels conjunts cap a memòria. | hash join (suport des de MySQL 8.0.18) | "Hash Match" | HASH JOIN |
Merge Join |
Requereix que ambdós conjunts estiguin ordenats. Eficient per a conjunts grans ja ordenats per l'índex. | rar, l'optimitzador hi recorre poc | "Merge Join" | MERGE JOIN |
Nested Loop |
Per a cada fila exterior, cerca les files interiors. Excel·lent quan la selectivitat interior és molt alta. | algorisme per defecte històric de MySQL per a JOINs | "Nested Loops" | NESTED LOOPS |
Sort |
Ordena un conjunt. Si no cap a work_mem, fa un mergesort extern al disc (molt lent). |
"Using filesort" a Extra |
"Sort" | SORT ORDER BY |
Aggregate |
Calcula funcions d'agrupació (SUM, COUNT, AVG). |
"Using temporary" / agregació pròpia | "Stream Aggregate" | SORT AGGREGATE |
Hash Aggregate |
Versió hash de l'agregació, usar quan els grups caben a memòria. | suport limitat, sovint cau a "Using temporary" | "Hash Match (Aggregate)" | HASH GROUP BY |
MySQL i Nested Loop
Històricament, MySQL/MariaDB resol pràcticament tots els JOINs amb una variant de Nested Loop Join (Block Nested Loop quan no hi ha índex disponible). El suport de Hash Join és relativament recent (MySQL 8.0.18+) i només s'activa en certes condicions, a diferència de PostgreSQL, SQL Server i Oracle, que trien lliurement entre els tres algorismes segons el cost estimat.
Eines de monitoratge
pg_stat_statements i equivalents
Extensió que acumula estadístiques de totes les consultes executades: temps total, temps mig, nombre d'execucions, files retornades.
-- Activar l'extensió (requereix reinici si no és al shared_preload_libraries)
-- A postgresql.conf: shared_preload_libraries = 'pg_stat_statements'
CREATE EXTENSION IF NOT EXISTS pg_stat_statements;
-- Les 10 consultes que consumeixen més temps total
SELECT
round(total_exec_time::numeric, 2) AS temps_total_ms,
calls AS execucions,
round(mean_exec_time::numeric, 2) AS temps_mig_ms,
round(rows / calls::numeric, 1) AS files_per_exec,
left(query, 120) AS consulta
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 10;
-- Reiniciar estadístiques acumulades
SELECT pg_stat_statements_reset();
El Performance Schema, en concret la vista sys.statement_analysis o la taula performance_schema.events_statements_summary_by_digest, acumula estadístiques de totes les consultes (temps total, mitjana, execucions) — equivalent funcional a pg_stat_statements:
Query Store (activat per defecte des de SQL Server 2016+) és l'equivalent directe i fins i tot més complet, ja que guarda l'historial de plans i permet "forçar" un pla antic:
ALTER DATABASE bigdata SET QUERY_STORE = ON;
SELECT qt.query_sql_text, rs.avg_duration, rs.count_executions
FROM sys.query_store_query_text qt
JOIN sys.query_store_query q ON q.query_text_id = qt.query_text_id
JOIN sys.query_store_plan p ON p.query_id = q.query_id
JOIN sys.query_store_runtime_stats rs ON rs.plan_id = p.plan_id
ORDER BY rs.avg_duration DESC;
AWR (Automatic Workload Repository) i ASH (Active Session History), accessibles via vistes com DBA_HIST_SQLSTAT o v$sqlstats, en compleixen la funció. Cal tenir en compte que l'accés complet a AWR/ASH via Enterprise Manager requereix llicència del Diagnostics Pack, no inclosa a totes les edicions:
auto_explain i equivalents
Extensió/mecanisme que registra automàticament al log el pla d'execució de les consultes que superen un llindar de temps:
El slow query log (slow_query_log + long_query_time) compleix una funció similar, encara que sense el pla d'execució complet, només la consulta i el seu temps:
Extended Events és l'eina moderna equivalent, capaç de capturar consultes que superen un llindar de durada:
Es pot configurar trace SQL a nivell de sessió o, més modernament, fer servir els llindars d'ASH/AWR combinats amb alertes de DBMS_SERVER_ALERT per detectar consultes lentes de forma proactiva.
Tècniques de reescriptura de consultes
Substituir subconsultes correlacionades per JOINs
Una subconsulta correlacionada s'executa una vegada per cada fila del resultat exterior, resultant en milers o milions d'execucions. Aquest problema i la seva solució són idèntics conceptualment als quatre motors: és una qüestió d'SQL relacional, no específica d'un motor concret.
-- Lent: subconsulta correlacionada (N execucions de la subconsulta)
SELECT nom, (
SELECT SUM(total) FROM comandes c WHERE c.client_id = cl.id
) AS total_compres
FROM clients cl;
-- Ràpid: JOIN amb agregació
SELECT cl.nom, COALESCE(agg.total_compres, 0) AS total_compres
FROM clients cl
LEFT JOIN (
SELECT client_id, SUM(total) AS total_compres
FROM comandes
GROUP BY client_id
) agg ON agg.client_id = cl.id;
-- Lent: subconsulta correlacionada (N execucions de la subconsulta)
SELECT nom, (
SELECT SUM(total) FROM comandes c WHERE c.client_id = cl.id
) AS total_compres
FROM clients cl;
-- Ràpid: JOIN amb agregació
SELECT cl.nom, COALESCE(agg.total_compres, 0) AS total_compres
FROM clients cl
LEFT JOIN (
SELECT client_id, SUM(total) AS total_compres
FROM comandes
GROUP BY client_id
) agg ON agg.client_id = cl.id;
-- Lent: subconsulta correlacionada (N execucions de la subconsulta)
SELECT nom, (
SELECT SUM(total) FROM comandes c WHERE c.client_id = cl.id
) AS total_compres
FROM clients cl;
-- Ràpid: JOIN amb agregació
SELECT cl.nom, COALESCE(agg.total_compres, 0) AS total_compres
FROM clients cl
LEFT JOIN (
SELECT client_id, SUM(total) AS total_compres
FROM comandes
GROUP BY client_id
) agg ON agg.client_id = cl.id;
-- Lent: subconsulta correlacionada (N execucions de la subconsulta)
SELECT nom, (
SELECT SUM(total) FROM comandes c WHERE c.client_id = cl.id
) AS total_compres
FROM clients cl;
-- Ràpid: JOIN amb agregació
SELECT cl.nom, COALESCE(agg.total_compres, 0) AS total_compres
FROM clients cl
LEFT JOIN (
SELECT client_id, SUM(total) AS total_compres
FROM comandes
GROUP BY client_id
) agg ON agg.client_id = cl.id;
CTEs: materialitzades vs inline
A partir de PostgreSQL 12, les CTEs sense recursió es pleguen (inline) per defecte, permetent a l'optimitzador integrar-les al pla global. Si necessiteu que la CTE s'executi exactament una vegada com a barrera d'optimització, useu MATERIALIZED:
-- CTE inline (per defecte a PG12+): l'optimitzador pot empènyer filtres dins
WITH vendes_recents AS (
SELECT * FROM vendes WHERE data_venda >= '2026-01-01'
)
SELECT pais, COUNT(*) FROM vendes_recents GROUP BY pais;
-- CTE materialitzada: s'executa exactament una vegada i el resultat es guarda
WITH MATERIALIZED vendes_recents AS (
SELECT * FROM vendes WHERE data_venda >= '2026-01-01'
)
SELECT pais, COUNT(*) FROM vendes_recents GROUP BY pais;
Useu MATERIALIZED quan la CTE conté una subconsulta cara que es referencia múltiples vegades, o quan voleu garantir que s'executa exactament una vegada per efectes secundaris.
MySQL/MariaDB (8.0.14+) suporta WITH ... AS (...) però no té la clàusula MATERIALIZED/NOT MATERIALIZED de PostgreSQL. Una CTE no recursiva sempre es comporta com a "inline": l'optimitzador la pot plegar al pla global sense que l'usuari pugui forçar el comportament contrari.
SQL Server suporta WITH ... AS (...) (Common Table Expressions), però tampoc té control de materialització explícit. Les CTEs es tracten sempre com a vistes en línia que l'optimitzador pot expandir o no segons el seu propi criteri, sense cap paraula clau que ho permeti forçar:
Oracle té un control equivalent (i fins i tot anterior a PostgreSQL): els hints /*+ MATERIALIZE */ i /*+ INLINE */ dins la clàusula WITH (subquery factoring) permeten forçar explícitament el comportament:
Materialització de CTEs: només PostgreSQL i Oracle ho controlen explícitament
MySQL/MariaDB i SQL Server no donen a l'usuari cap mecanisme per forçar si una CTE es materialitza o es plega; només PostgreSQL (MATERIALIZED) i Oracle (hints MATERIALIZE/INLINE) ofereixen aquest control explícit.
Evitar funcions en WHERE que impedeixen l'ús d'índexs
Aplicar una funció sobre una columna indexada al WHERE impedeix l'ús de l'índex B-Tree (tret que existeixi un índex funcional), perquè el motor ha d'avaluar la funció per a cada fila abans de poder comparar-la. Aquest problema i la seva solució (reescriure com a rang) són idèntics conceptualment als quatre motors; només canvia la sintaxi de la funció d'extracció de data (EXTRACT a PostgreSQL/Oracle, YEAR() a MySQL/SQL Server).
-- Lent: la funció sobre la columna impedeix l'ús de l'índex B-Tree
SELECT * FROM vendes WHERE EXTRACT(year FROM data_venda) = 2026;
-- Ràpid: rang equivalent que sí permet usar l'índex
SELECT * FROM vendes
WHERE data_venda >= '2026-01-01' AND data_venda < '2027-01-01';
-- Lent: càlcul sobre columna indexada
SELECT * FROM productes WHERE preu * 1.21 > 100;
-- Ràpid: reescriure per aïllar la columna
SELECT * FROM productes WHERE preu > 100 / 1.21;
-- Lent: la funció sobre la columna impedeix l'ús de l'índex B-Tree
SELECT * FROM vendes WHERE YEAR(data_venda) = 2026;
-- Ràpid: rang equivalent que sí permet usar l'índex
SELECT * FROM vendes
WHERE data_venda >= '2026-01-01' AND data_venda < '2027-01-01';
-- Lent: càlcul sobre columna indexada
SELECT * FROM productes WHERE preu * 1.21 > 100;
-- Ràpid: reescriure per aïllar la columna
SELECT * FROM productes WHERE preu > 100 / 1.21;
-- Lent: la funció sobre la columna impedeix l'ús de l'índex B-Tree
SELECT * FROM vendes WHERE YEAR(data_venda) = 2026;
-- Ràpid: rang equivalent que sí permet usar l'índex
SELECT * FROM vendes
WHERE data_venda >= '2026-01-01' AND data_venda < '2027-01-01';
-- Lent: càlcul sobre columna indexada
SELECT * FROM productes WHERE preu * 1.21 > 100;
-- Ràpid: reescriure per aïllar la columna
SELECT * FROM productes WHERE preu > 100 / 1.21;
-- Lent: la funció sobre la columna impedeix l'ús de l'índex B-Tree
SELECT * FROM vendes WHERE EXTRACT(YEAR FROM data_venda) = 2026;
-- Ràpid: rang equivalent que sí permet usar l'índex
SELECT * FROM vendes
WHERE data_venda >= DATE '2026-01-01' AND data_venda < DATE '2027-01-01';
-- Lent: càlcul sobre columna indexada
SELECT * FROM productes WHERE preu * 1.21 > 100;
-- Ràpid: reescriure per aïllar la columna
SELECT * FROM productes WHERE preu > 100 / 1.21;
work_mem i el seu impacte
work_mem controla quanta memòria pot usar cada operació de Sort o Hash. Si l'operació no hi cap, PostgreSQL fa un mergesort extern al disc (molt més lent). El valor per defecte (4 MB) és sovint insuficient per a taules grans:
-- Veure si hi ha sorts/hashes que van al disc
EXPLAIN (ANALYZE, BUFFERS)
SELECT client_id, SUM(total) FROM vendes GROUP BY client_id ORDER BY SUM(total) DESC;
-- Si apareix "Sort Method: external merge Disk: 1234kB" -> cal més work_mem
-- Augmentar work_mem per a la sessió actual (no canvia la config global)
SET work_mem = '256MB';
-- Tornar a executar i verificar que ara usa "Sort Method: quicksort Memory: 45kB"
sort_buffer_size controla la memòria per a ORDER BY/GROUP BY quan no es pot resoldre amb un índex, i join_buffer_size la memòria per a joins sense índex disponible. Tots dos són paràmetres per connexió, igual que work_mem:
No hi ha un paràmetre equivalent configurable manualment per sessió. La memòria per a operacions de Sort o Hash es gestiona automàticament mitjançant memory grants, calculats per l'optimitzador segons l'estimació de files, i controlats globalment per "Max Server Memory" i el Resource Governor (per a pools de recursos). És una diferència filosòfica respecte a PostgreSQL i Oracle: SQL Server automatitza el que aquests permeten ajustar manualment.
PGA_AGGREGATE_TARGET (mode automàtic, recomanat) gestiona la memòria de totes les sessions de forma global. Alternativament, en mode manual (WORKAREA_SIZE_POLICY = MANUAL), SORT_AREA_SIZE és l'equivalent directe a work_mem:
work_mem és per operació i per connexió
Si work_mem = 256MB i hi ha 100 connexions actives, cadascuna amb 5 operacions de sort simultànies, el consum màxim teòric és de 256 MB × 100 × 5 = 128 GB. Augmenteu-lo amb prudència i preferiblement per sessió o per rol, no globalment. A MySQL/MariaDB el mateix raonament aplica a sort_buffer_size/join_buffer_size; a SQL Server i Oracle (en mode automàtic), el motor gestiona aquest risc internament.
AC5074/02/03 — Miniactivitat
Analitzeu el següent pla EXPLAIN ANALYZE i responeu les preguntes:
Hash Join (cost=45234.00..312450.00 rows=500000 width=88)
(actual time=8234.5..45123.2 rows=487234 loops=1)
Hash Cond: (v.producte_id = p.id)
Buffers: shared hit=1200 read=98000
-> Seq Scan on vendes v (cost=0.00..234000.00 rows=10000000 width=56)
(actual time=0.1..12340.0 rows=10000000 loops=1)
Filter: (data_venda >= '2026-01-01')
Rows Removed by Filter: 3500000
Buffers: shared hit=800 read=95000
-> Hash (cost=2100.00..2100.00 rows=105000 width=32)
(actual time=234.5..234.5 rows=105000 loops=1)
-> Seq Scan on productes p (cost=0.00..2100.00 rows=105000 width=32)
(actual time=0.1..123.4 rows=105000 loops=1)
Planning Time: 12.3 ms
Execution Time: 45890.1 ms
Identificació: (1) Quin és el coll d'ampolla principal i per quina raó? (2) Quines dades indiquen que les estadístiques poden estar desactualitzades? (3) Quants blocs s'han llegit del disc i quin impacte té? (4) Proposeu dues millores concretes (índexs, particionament, configuració) i expliqueu per quin motiu espereu millora en cadascuna.