PostgreSQL Locks con il Partitioning

Il locking e' fondamentale per garantire la consistenza dei dati. L'utilizzo delle partizioni consente di gestire grandi moli di dati in modo efficiente. In questa paginetta vediamo le problematiche di locking applicate a tabelle partizionate PostgreSQL in modo pratico con avvertenze e suggerimenti.

Per motivi di spazio vengono si considera che il lettore gia' conosca il partitioning e la gestione dei lock in PostgreSQL.

Introduzione

In PostgreSQL non solo le DML (INSERT, UPDATE, DELETE, MERGE) ma anche le DDL (eg. CREATE INDEX, ALTER TABLE) possono far parte di una transazione e quindi venir confermate con un COMMIT o rigettate con un ROLLBACK.
PostgreSQL ha una sofisticata gestione dei lock per garantire la consistenza delle transazioni e per proteggere gli oggetti durante le operazioni di modifica dei dati (DML) e di modifica della struttura dei dati (DDL).
Per comprendere i lock in PostgreSQL bisogna sapere che vi sono lock a livello di tabella e lock a livello di riga. Hanno regole ed implementazioni molto diverse tra loro e quindi vanno analizzati separatamente.

PostgreSQL supporta da sempre il partitioning mediante l'ereditarieta' degli oggetti ma questo ne rendeva l'utilizzo complesso e non sempre efficiente. Il partizionamento nativo, introdotto con la versione 10 e' invece molto piu' efficiente, molto piu' utilizzato ed e' quello che vedremo in questa paginetta. Il partizionamento nativo consente di definire tabelle partizionata con le modalita' Range, List ed Hash. In PostgreSQL la tabella partizionata non contiene nessun dato ma solo le definizioni, le partizioni sono in effetti tabelle a tutti gli effetti che possono essere utilizzate direttamente.

I lock PostgreSQL che ci interessano maggiormente sono i lock a livello di tabella che sono importanti sopratutto per le operazioni di DDL e che, con il partizionamento, presentano diversi casi particolari.

Table-level Locks

Il lock a livello di tabella ha molteplici lock mode a seconda del tipo di operazione. I lock vengono liberati al momento del commit o del rollback della transazione.
Dal punto di vista dell'implementazione PostgreSQL i table-level lock vengono mantenuti in memoria in un area dedicata.

La figura seguente (che e' una versione leggermente semplificata rispetto alla tabella completa) indica quale lock richiedono i vari comandi SQL e quali sono gli eventuali conflitti. Analizzando la figura e' evidente che una CREATE INDEX blocca le UPDATE, mentre una CREATE INDEX CONCURRENTLY consente le UPDATE; e' possibile lanciare una CREATE INDEX anche se ve ne e' una gia' in corso, mentre e' possibile lanciare solo una CREATE INDEX CONCURRENTLY alla volta.

Table-Level Locks

Requested Lock Mode Existing Lock Mode
ACCESS SHARE ROW EXCLUSIVE SHARE UPDATE EXCLUSIVE SHARE SHARE ROW EXCLUSIVE ACCESS EXCLUSIVE
SQL SELECT INSERT, UPDATE, DELETE, MERGE VACUUM, ANALYZE, CREATE INDEX CONCURRENTLY, REINDEX CONCURRENTLY, ALTER TABLE (some), ALTER INDEX (some), CREATE STATISTICS CREATE INDEX CREATE TRIGGER, ALTER TABLE (some) DROP TABLE, TRUNCATE, VACUUM FULL, REINDEX, CLUSTER, ALTER TABLE (most), ALTER INDEX (most), LOCK TABLE, DROP INDEX, ...
ACCESS SHARE SELECT           X
ROW EXCLUSIVE INSERT, UPDATE, DELETE, MERGE       X X X
SHARE UPDATE EXCLUSIVE VACUUM, ...     X X X X
SHARE CREATE INDEX   X X   X X
SHARE ROW EXCLUSIVE CREATE TRIGGER, ...   X X X X X
ACCESS EXCLUSIVE DROP TABLE, ... X X X X X X

Ma come si comporta PostgreSQL nel caso di tabelle partizionate?
Le partizioni in PostgreSQL sono tabelle a tutti gli effetti e si comportano come tali. Ogni partizione foglia e' una tabella separata e quindi indipendente dal punto di vista dei lock. Ma vi sono molti casi un cui viene coinvolta la tabella partizionata e non solo le singole partizioni individualmente come vedremo nei prossimi paragrafi che riportano le piu' tipiche operative: Create / Attach / Drop / Truncate / Detach Partition, Create / Drop Index on a Partitioned Table, Sub Partitioning, Default Partition.

Create partition

Il modo piu' semplice per creare una partizione e collegarla alla tabella partizionata e' il comando di CREATE:
CREATE TABLE measurement_y2008m02 PARTITION OF measurement FOR VALUES FROM ('2008-02-01') TO ('2008-03-01');
Warning Attenzione: la creazione di una nuova partizione e' un'operazione tipicamente di breve durata ma utilizza un lock ACCESS EXCLUSIVE sulla tabella partizionata. Questo significa che la richiesta di lock viene accodata fino al termine agli accessi correnti e le richieste successive sulla tabella partizionata vengono poste a loro volta in attesa.

Se le attivita' sulla tabella partizionata sono sempre di breve durata non c'e' problema: il lock verra' ottenuto dopo poco tempo e la creazione della partizione avverra' velocemente. Ma se vi sono query di lunga durata l'attesa del lock per la CREATE PARTITION e per le richieste successive sara' elevata, per questo motivo e' possibile utilizzare una modalita' alternativa effettuando l'ATTACH di una partizione.

Attach partition

Tip Anziche' creare direttamente una partizione e' possibile creare una tabella e successivamente trasformarla in una partizione eseguendo un ATTACH sulla tabella partizionata. ALTER TABLE... ATTACH PARTITION... richiede un lock SHARE UPDATE EXCLUSIVE sulla tabella partizionata ed un lock ACCESS EXCLUSIVE sulla partizione aggiunta e sull'eventuale DEFAULT partition.
L'aggiunta di una partizione non blocca ne le SELECT ne le INSERT/UPDATE/DELETE/MERGE sulla tabella partizionata.
CREATE TABLE measurement_y2008m02 (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS); ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02 FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' );

Se la partizione da connettere e' vuota l'operazione e' di ATTACH e' molto veloce e richiede lock che consentono DML sulla tabella partizionata senza attese.
Se la futura partizione contiene dati PostgreSQL deve effettuare uno scan per controllare che tutti i record presenti soddisfino le condizioni di partizionamento.

Tip Suggerimento: per evitare rallentamenti in fase di ATTACH di una partizione che gia' contiene dati e' possibile creare preventivamente tutti gli indici necessari ed utilizzare un CHECK constraint che puo' essere successivamente elimitato. In questo modo l'attach sara' veloce anche se la partizione da aggiungere contiene molti record.

Vi sono alcuni comandi ulteriori da utilizzare ma l'operativa non e' complessa:

CREATE TABLE measurement_y2008m02 (LIKE measurement INCLUDING DEFAULTS INCLUDING CONSTRAINTS); ALTER TABLE measurement_y2008m02 ADD CONSTRAINT y2008m02 CHECK ( logdate >= DATE '2008-02-01' AND logdate < DATE '2008-03-01' ); -- popolamento dati -- creazione indici ... ALTER TABLE measurement ATTACH PARTITION measurement_y2008m02 FOR VALUES FROM ('2008-02-01') TO ('2008-03-01' ); ... ALTER TABLE measurement_y2008m02 DROP CONSTRAINT y2008m02;

Drop partition

Il modo piu' semplice per eliminare una partizione e' quello di cancellarla!
DROP TABLE measurement_y2006m02;

Il drop di una partizione e' terribilmente piu' efficiente di una DELETE e delle successivi attivita' di VACUUM, l'unica avvertenza e' sul locking...

Warning Attenzione: la drop di una partizione e' un'operazione tipicamente di breve durata ma utilizza un lock ACCESS EXCLUSIVE anche sulla tabella partizionata. Questo significa che la richiesta di lock viene accodata fino al termine agli accessi correnti e le richieste successive sulla tabella partizionata vengono poste a loro volta in attesa.
Se le attivita' sulla tabella partizionata sono sempre di breve durata non c'e' problema: il lock verra' ottenuto dopo poco tempo e la drop della partizione avverra' velocemente. Ma se vi sono query di lunga durata l'attesa del lock sara' elevata, per questo motivo e' possibile utilizzare modalita' alternative per eliminare i dati da una partizione.

Truncate partition

La truncate di una partizione e' molto veloce e richiede un lock ACCESS EXCLUSIVE solo sulla partizione.

Il truncate di una partizione e' terribilmente piu' efficiente di una DELETE e delle successivi attivita' di VACUUM e non coinvolge la tabella partizionata ma solo la partizione interessata. Tutto lo spazio viene immediatamente liberato e le statistiche aggiornate e' quindi un'alternativa valida al drop di una partizione. E' comunque opportuno effettuare il drop attendendo un momento in cui non vi sono problemi di lock (il drop e' comunque di breve durata.

Detach partition

ALTER TABLE... DETACH PARTITION... richiede un lock ACCESS EXCLUSIVE sulla tabella partizionata, un lock ACCESS EXCLUSIVE sulla partizione rimossa e sull'eventuale DEFAULT partition. E' importante notare che un lock ACCESS EXCLUSIVE blocca sia le SELECT che le altre operazioni di DML.
E' vero che la DETACH tipicamente e' molto veloce, pero' deve restare in attesa del lock e possono accodarsi le sucessivie richieste di accesso alla tabella partizionata che vanno in conflitto e quindi restano in attesa anche loro.

Tip Suggerimento: dalla versione PG14 e' disponibile ALTER TABLE... DETACH PARTITION... CONCURRENTLY che utilizza un locking meno pesante.

Con la modalita' CONCURRENTLY internamente vengono effettuate due transazioni; nella prima transazione viene avviata la fase di detach utilizzando un lock SHARE UPDATE EXCLUSIVE sulla tabella e sulla partizione, questo lock non blocca le DML. Dopo il primo commit si attende la conclusione di tutte le transazioni attive sulla partizione; quindi viene richiesto un lock SHARE UPDATE EXCLUSIVE sulla tabella partizionata ed un lock ACCESS EXCLUSIVE sulla partizione. In pratica le DML rivolte alla tabella partizionata non vengono mai bloccate.
La modalita' CONCURRENTLY non puo' essere utilizzata in una transazione e non puo' essere utilizzata se e' presente una DEFAULT partition. Nel caso in cui l'operazione sia stata interrotta a meta' puo' essere completata con DETACH PARTITION... FINALIZE.

Create index

Gli indici creati sulle tabelle partizionate vengono automaticamente creati su tutte le partizioni attuali e su quelle future. E' possibile creare indici solo sulle singole partizioni perche' sono tabelle a tutti gli effetti: questo consente di avere alcune partizioni con piu' indici (eg. le piu' recenti) ed altre con un numero minore di indici (eg. le partizioni "storiche").

La creazione di un indice richiede un lock SHARE e, se la dimensione degli oggetti e' elevata, tale lock deve essere mantenuto a lungo perche' la costruzione di un indice richiede tempo [NdA O(n log n)]. Come e' noto un lock SHARE non blocca le SELECT ma blocca tutte le modifiche sulla tabella con INSERT, UPDATE, DELETE, MERGE. Inoltre la creazione di un indice, se la tabella sottostante ha molti record, puo' richiedere molto tempo, quindi il lock viene mantenuto a lungo.
Con le normali tabelle, per evitare il blocco sulle modifiche dei dati, per la creazione di un indice on-line si utilizza la clausola CONCURRENTLY, ma tale modalita' non e' consentita per le tabelle partizionate.

Tip E' possibile creare un nuovo indice su una tabella partizionata senza rallentare le operazioni di modifica dei dati, cosi' come avviene sulle tabelle singole, creando l'indice con la clausola ONLY sulla tabella partizionata e sfruttando la clausola CONCURRENTLY nella creazione degli indici su tutte le partizioni. La creazione di un indice con la clausola ONLY modifica solo i metadati della tabella partizionata ed e' molto veloce. La creazione di un indice con la clausola CONCURRENTLY richiede un lock SHARE UPDATE EXCLUSIVE che e' compatibile con le DML.

-- Creare l'indice sulla parent table (metadata only), l'indice risultera' INVALID CREATE INDEX pgbench_accounts_idx_bid ON ONLY pgbench_accounts (bid); -- Creare gli indici con CONCURRENTLY (non blocca le DML) per ogni partizione presente CREATE INDEX CONCURRENTLY pgbench_accounts_idx_bid_1 ON pgbench_accounts_1 (bid); -- Associare gli indici delle partizioni all'indice della tabella partizionata ALTER INDEX pgbench_accounts_idx_bid ATTACH PARTITION pgbench_accounts_idx_bid_1; ... -- Quando tutte le partizioni hanno l'indice associato, l'indice della tabella partizionata diventa VALID

Per controllare la presenza di indici INVALID in PostgreSQL si utilizza la seguente query:

SELECT n.nspname as schema, c2.relname as table, c1.relname as index
  FROM pg_class c1, pg_index i, pg_namespace n, pg_class c2
 WHERE c1.relnamespace = n.oid
   AND i.indexrelid = c1.oid
   AND c2.oid = i.indrelid
   AND i.indisvalid = false;

Come gia' riportato, possono essere creati indici anche sulle singole partizioni; come per le normali tabelle anche questi indici possono essere creati con la clausola CONCURRENTLY per non bloccare INSERT/UPDATE/DELETE.

Drop index

La cancellazione di un indice e' di breve durata ma richiede un lock ACCESS EXCLUSIVE.
Per la DROP INDEX e' presente la possibilita' di specificare la clausola CONCURRENTLY che consente di cancellare l'indice senza bloccare le altre attivita' sulla tabella, ma tale possibilita' e' presente solo per indici non partizionati.

Per la DROP il comportamento nella gestione dei lock e' diverso: anche con la clausola CONCURRENTLY viene usato un lock ACCESS EXCLUSIVE, ma l'istruzione attende che non vi siano altri accessi e quindi non crea un lock in attesa. Quando la tabella e' libera da altri accessi il lock viene richiesto ed ottenuto immediatamente e l'operazione di cancellazione di un indice e' di breve durata.
Vi sono pero' molti limiti nell'uso della clausola CONCURRENTLY nella DROP (eg. transazioni, cascade) e non e' possibile utilizzarla neanche per gli indici di tabelle partizionate.

VACUUM ed ANALYZE

Sulle tabelle partizionate non viene mai eseguito il VACUUM: non contengono dati!
E' invece molto importante, come per tutte le altre tabelle in PostgreSQL, l'esecuzione del VACUUM su tutte le partizioni foglie. Non vi sono differenze particolari da questo punto di vista rispetto alle normali tabelle e valgono quindi tutte le indicazioni sul VACUUM di PostgreSQL.

Subpartitioning

Quanto visto per il partizionamento si applica in modo ricorsivo se una partizione e' a sua volta partizionata ed e' quindi stato utilizzato il subpartitioning. In pratica un lock sulla partizione intermedia sara' da applicare su tutte le partizioni figlie.

Ad esempio se si esegue un ATTACH di una partizione che a sua volta e' partizionata il lock ACCESS EXCLUSIVE sulla partizione da aggiungere viene richesto anche su tutte le sotto partizioni.

Default partition

Dalla versione PG11 e' possibile definire una partizione di DEFAULT in cui vengono inseriti i record nel caso in cui non vi sia la relativa partizione.
Il vantaggio e' che non viene restituito l'errore ERROR: no partition of relation table_name found for row nel caso manchi la partizione corrispondente, ma il rischio e' che non vengano gestite correttamente le partizioni e la maggioranza del record finisca nella partizione di DEFAULT vanificando l'uso del partizionamento.

Warning Attenzione: l'utilizzo della partizione di DEFAULT impedisce l'uso di alcune delle tecniche descritte (eg. DETACH... CONCURRENTLY) per evitare lock sospensivi e rallenta altre operative.
In generale e' opportuno evitare l'utilizzo della DEFAULT partition in PostgreSQL.

Lock timeout

Abbiamo visto diverse tecniche per utilizzare lock che non entrino in attesa, tuttavia alcune richiedono una certa preparazione. Tip Vi e' pero' una semplice alternativa applicabile quando i comandi sono dati in modo interattivo. L'idea e' quella di utilizzare i normali comandi di CREATE/ALTER/... anche se possono rimanere in attesa di un lock impostando un timeout di breve durata: se la tabella e' libera il lock viene ottenuto e si svolge l'operazione desiderata; se invece il lock risulta in attesa si interrompe dopo poco tempo e non genera cosi' una coda di lock.
SET lock_timeout TO '2s' ALTER TABLE...

Lock Sizing

PostgreSQL utilizza un area di memoria per mantenere i Table-Level lock. L'area e' dimensionata basandosi sui parametri max_connections (default: 100), max_locks_per_transaction (default: 64) e max_pred_locks_per_transaction (default: 0) che porta ad un totale massimo di 6.400 lock. Il limite non e' per transazione ma e' totale per tutte le sessioni attive.
Nella pratica e' difficile superare il limite dei lock in shared memory, ma nel caso in cui avvenga l'errore e' il seguente:

ERROR: out of shared memory HINT: You might need to increase max_locks_per_transaction.

Quando si opera su tabelle con un gran numero di partizioni e non si effettua il pruning per PostgreSQL ciascuna partizione e' una tabella a tutti gli effetti ed i Table-Level lock vengono richiesti per tutte le partizioni: in questi casi e' possibile superare il limite dei lock e ricevere l'errore.

Evitare l'overpartitioning ed effettuare il pruning risolve il problema.
Se non e' possibile agire altrimenti per evitare l'errore non si puo' fare altro che aumentare il valore del parametro max_locks_per_transaction (o il max_connections):

Row-level Locks

I lock a livello di riga riguardano solo le attivita' di scrittura e non le letture: due transazioni diverse non possono mantenere un lock sulla stessa riga contemporaneamente. Al contrario le attivita' in sola lettura non vengono mai messe in attesa da un lock di riga, anche se e' possibile che vedano righe diverse grazie all'MVCC (MultiVersion Concurrency Control).
Anche i lock a livello di riga vengono liberati al momento del commit o del rollback della transazione.

Cio' premesso quali sono le differenze sui lock row-level tra le normali tabelle e le partizioni?
Nessuna differenza! Le normali tabelle e le partizioni sono trattate nello stesso identico modo. Le tabelle partizionate o sottopartizionate non contengono nessun dato, le partizioni foglia sono normali tabelle ed i lock a livello di riga vengono mantenuti sulla heap nella tupla interessata.

In Postgres il lock di riga non ha alcun limite, semplicemente richiede una scrittura sulla heap per indicare che e' presente un lock. Se non si utilizzano particolari estensioni e query i lock di riga non sono visibili, diventano evidenti solo quando vi e' un conflitto ed una transazione resta in attesa: solo in questo momento diventano visibili sulla pg_locks. Ma non vi e' nessuna differenza nel trattamento dei lock a livello di riga quando queste fanno parte di partizioni.

Varie ed eventuali

Possono essere utili le pagine che descrivono il partizionamento nativo, il locking e gli indici in PostgreSQL.

Come sempre la documentazione ufficiale (eg. lock, ALTER TABLE) e' la fonte piu' completa ed affidabile delle informazioni.


Titolo: PostgreSQL Locks con il Partitioning
Livello: Avanzato (3/5)
Data: 1 Aprile 2025
Versione: 1.0.1 - 1 Aprile 2025
Autore: mail [AT] meo.bogliolo.name