Mirajv1.0
FR

37. PostgreSQL : Transactions et session

Ce chapitre décrit, pour une instance MIRAJ en langage PostgreSQL :

  • le contrôle des transactions (BEGIN, COMMIT, ROLLBACK, SAVEPOINT) et ce qui se passe après une erreur dans un bloc de transaction ;
  • les instructions de définition (CREATE TABLE, DROP TABLE…) dans un bloc de transaction ;
  • les niveaux d'isolement et le modèle de concurrence de MIRAJ : une isolation sérialisable sans verrou de ligne, où un conflit se règle en recommençant la transaction ;
  • les lectures verrouillantes (SELECT … FOR UPDATE), LOCK TABLE et les verrous consultatifs ;
  • les paramètres de session (SET, SHOW, RESET), le chemin de recherche (search_path), le fuseau horaire et le délai d'instruction ;
  • les curseurs SQL, les instructions préparées et la remise à zéro d'une session.

Les exemples utilisent le schéma d'exemple ventes (tables client, article, commande, ligne_commande) créé et rempli au chapitre 31. Prise en main : trois clients, trois articles (stocks 500, 200 et 80), trois commandes. Les résultats montrés sont ceux qu'affiche un client PostgreSQL : étiquette de l'instruction (BEGIN, UPDATE 1…), lignes, ou erreur avec son SQLSTATE. Les exemples à deux sessions se lisent de haut en bas, la session A à gauche, la session B à droite.


37.1 Auto-validation et transactions explicites#

Auto-validation#

Hors d'un bloc de transaction, chaque instruction forme sa propre transaction, validée dès qu'elle réussit et annulée en entier si elle échoue. C'est le comportement de PostgreSQL, et celui qu'attendent psql et les pilotes.

Une requête qui contient plusieurs instructions séparées par ; (envoyées en une fois par psql -c ou par un pilote) forme une transaction implicite, comme dans PostgreSQL : si l'une échoue, les écritures des instructions précédentes sont annulées et les suivantes ne sont pas exécutées.

UPDATE article SET stock = stock - 1 WHERE id = 1; SELECT 1/0; UPDATE article SET stock = stock - 1 WHERE id = 2
UPDATE 1
ERROR:  division by zero   (22012)

Après quoi les stocks des articles 1 et 2 sont toujours 500 et 200. Un COMMIT écrit dans la requête valide ce qui le précède (UPDATE …; COMMIT; SELECT 1/0 garde l'UPDATE), et un BEGIN y ouvre un bloc ordinaire, qui reste ouvert après la requête s'il n'est pas fermé. Une instruction de définition (CREATE TABLE…) de la requête n'est pas défaite par l'échec d'une instruction qui la suit.

Ouvrir une transaction#

BEGIN [WORK | TRANSACTION] [mode [, mode] …];
START TRANSACTION [mode [, mode] …];

où mode est :

ISOLATION LEVEL {SERIALIZABLE | REPEATABLE READ | READ COMMITTED | READ UNCOMMITTED}
READ WRITE | READ ONLY
[NOT] DEFERRABLE

Les modes s'écrivent séparés par une virgule ou par une espace. [NOT] DEFERRABLE est accepté sans effet. Le niveau d'isolement est décrit au 37.3.

BEGIN;
INSERT INTO commande (client_id) VALUES (3) RETURNING id;
INSERT INTO ligne_commande VALUES (4, 1, 10, 1.20);
UPDATE article SET stock = stock - 10 WHERE id = 1;
COMMIT;
BEGIN
 id
----
  4
(1 ligne)

INSERT 0 1
INSERT 0 1
UPDATE 1
COMMIT

Un BEGIN envoyé alors qu'une transaction est déjà ouverte est accepté sans erreur ni avis, et la transaction en cours continue.

Valider ou annuler#

COMMIT [WORK | TRANSACTION] [AND [NO] CHAIN];
END [WORK | TRANSACTION];
ROLLBACK [WORK | TRANSACTION] [AND [NO] CHAIN];
ABORT;
  • COMMIT (ou END) valide toutes les écritures de la transaction : elles deviennent visibles des autres sessions et durables.
  • ROLLBACK (ou ABORT) les annule toutes : chaque ligne reprend sa valeur d'avant la transaction.
  • AND CHAIN ouvre aussitôt une nouvelle transaction, avec le même mode (READ ONLY ou READ WRITE) ; AND NO CHAIN est la valeur par défaut.
  • COMMIT ou ROLLBACK hors de toute transaction sont acceptés sans erreur ni avis.
BEGIN;
UPDATE article SET stock = stock - 100 WHERE id = 1;
SELECT stock FROM article WHERE id = 1;     -- 400 : la transaction voit ses écritures
ROLLBACK;
SELECT stock FROM article WHERE id = 1;     -- 500
InstructionÉtiquette rendue
BEGIN, BEGIN WORK, BEGIN TRANSACTIONBEGIN
START TRANSACTIONSTART TRANSACTION
COMMIT, ENDCOMMIT (ROLLBACK si le bloc était en échec)
ROLLBACK, ABORT, ROLLBACK TO SAVEPOINTROLLBACK
SAVEPOINT, RELEASESAVEPOINT, RELEASE

Erreur dans un bloc de transaction#

Comme dans PostgreSQL, une erreur met le bloc en échec : jusqu'à sa fin, toute instruction reçoit 25P02. Seuls ROLLBACK, ROLLBACK TO SAVEPOINT (voir 37.2) et COMMIT — qui annule alors, et rend l'étiquette ROLLBACK — sont acceptés.

BEGIN;
SELECT 1/0;
SELECT 1;
COMMIT;
BEGIN
ERROR:  division by zero                                   (22012)
ERROR:  current transaction is aborted, commands ignored until end of transaction block   (25P02)
ROLLBACK

Un conflit de concurrence (SQLSTATE 40001) ou un interblocage (40P01), voir 37.4, met lui aussi le bloc en échec, mais il a déjà annulé la transaction entière : ROLLBACK TO SAVEPOINT ne la rattrape pas, seul un ROLLBACK (ou un COMMIT, qui rend ROLLBACK) termine le bloc.

Transaction en lecture seule#

READ ONLY interdit toute écriture dans la transaction (25006), et l'erreur met le bloc en échec comme toute autre :

BEGIN READ ONLY;
SELECT count(*) FROM client;                       -- 3
UPDATE client SET ville = 'x' WHERE id = 1;
ROLLBACK;
ERROR:  Cannot execute statement in a READ ONLY transaction.   (25006)

SHOW transaction_read_only rend on dans une telle transaction, off sinon.

Instructions de définition dans une transaction#

Les instructions de définition les plus courantes s'exécutent dans la transaction du bloc : ROLLBACK les défait, COMMIT les garde.

InstructionAnnulable avec la transaction
CREATE TABLEoui, sauf CREATE TABLE … AS SELECT et une table temporaire
CREATE [UNIQUE] INDEX, ALTER TABLE … ADD COLUMN (sans REFERENCES), ALTER TABLE … ADD CONSTRAINT nom UNIQUE (…)oui, sur une table que la transaction n'a pas encore écrite
DROP TABLEoui, sans CASCADE, pour une table dont ne dépend ni vue ni clé étrangère d'une autre table, et que la transaction n'a pas écrite (une table créée par la transaction elle-même se supprime toujours ainsi)
BEGIN;
CREATE TABLE remise (code varchar(20) PRIMARY KEY, taux numeric(4,2));
ALTER TABLE article ADD COLUMN poids integer;
CREATE INDEX idx_article_stock ON article (stock);
ROLLBACK;
SELECT count(*) FROM remise;
SELECT poids FROM article;
BEGIN
CREATE TABLE
ALTER TABLE
CREATE INDEX
ROLLBACK
ERROR:  relation "remise" does not exist   (42P01)
ERROR:  Unknown column 'poids' in 'field list'   (42703)
  • ROLLBACK TO SAVEPOINT défait de même celles exécutées depuis le point (37.2) ; un COMMIT en échec et la fermeture de la session sans COMMIT les défont aussi.
  • Elles sont appliquées tout de suite : les autres sessions voient la table créée ou la colonne ajoutée avant le COMMIT.
  • TRUNCATE dans un bloc est annulable lui aussi (34. Langage DML).
  • Toute autre instruction de définition (CREATE VIEW, ALTER TABLE … RENAME, DROP COLUMN, CREATE FUNCTION…), ou l'une de celles du tableau hors des conditions dites, valide d'abord la transaction en cours, avec ses écritures et ses instructions de définition annulables, puis s'exécute ; un ROLLBACK qui suit n'a plus rien à défaire :
BEGIN;
INSERT INTO client (nom, email) VALUES ('Paul Leroy', '[email protected]');
CREATE VIEW v_ville AS SELECT DISTINCT ville FROM client;    -- valide l'INSERT qui précède
ROLLBACK;                                                    -- n'annule plus rien
SELECT nom FROM client WHERE nom = 'Paul Leroy';             -- la ligne est là, la vue aussi

Un script de migration qui emploie d'autres instructions que celles du tableau prévoit donc les instructions inverses, ou une sauvegarde (40. Catalogue et outils).

Ce qui ne dépend pas de la transaction#

  • Une séquence : une valeur servie par nextval n'est jamais rendue par ROLLBACK. Il en va de même du compteur d'une colonne serial ou d'identité : un INSERT annulé laisse un trou.
  • Les verrous consultatifs de session (pg_advisory_lock, voir 37.6) : ni COMMIT ni ROLLBACK ne les rendent.

Les paramètres de session, eux, suivent la transaction : un SET fait dans une transaction annulée est défait (voir 37.7).

Variable autocommit#

Extension de MIRAJ, utile aux outils qui ne savent pas ouvrir de bloc : SET autocommit = 0 fait ouvrir une transaction par la première instruction qui suit, jusqu'au prochain COMMIT ou ROLLBACK. SET autocommit = 1 (ou TO on) revient au mode par défaut. Les pilotes PostgreSQL n'en ont pas besoin : leur propre mode « autocommit » (psycopg2 connection.autocommit, JDBC setAutoCommit) envoie simplement des BEGIN et des COMMIT.


37.2 Points de sauvegarde#

SAVEPOINT nom;
ROLLBACK [WORK | TRANSACTION] TO [SAVEPOINT] nom;
RELEASE [SAVEPOINT] nom;
  • SAVEPOINT nom pose un point de reprise dans la transaction ouverte.
  • ROLLBACK TO nom défait tout ce que la transaction a écrit depuis ce point, retire les points posés après lui, et garde le point lui-même ainsi que la transaction. C'est aussi le moyen de sortir un bloc de l'état d'échec sans tout perdre.
  • RELEASE nom retire le point et ceux posés après lui, sans rien défaire.
  • COMMIT et ROLLBACK effacent tous les points.
BEGIN;
INSERT INTO client (nom, email, ville) VALUES ('Léa Petit', '[email protected]', 'Lille');
SAVEPOINT avant_commande;
INSERT INTO commande (client_id) VALUES (99);          -- client inconnu
ROLLBACK TO SAVEPOINT avant_commande;                   -- le bloc redevient utilisable
INSERT INTO commande (client_id) VALUES (4);
COMMIT;                                                  -- le client et la commande sont écrits
BEGIN
INSERT 0 1
SAVEPOINT
ERROR:  Cannot add or update a child row: a foreign key constraint fails ("ventes"."commande",
        CONSTRAINT "commande_ibfk_1" FOREIGN KEY ("client_id") REFERENCES "client" ("id"))   (23503)
ROLLBACK
INSERT 0 1
COMMIT

Comme dans PostgreSQL :

  • des points de même nom s'empilent : ROLLBACK TO et RELEASE visent le plus récent, et l'ancien redevient visible une fois le nouveau relâché ;
  • un nom inconnu donne savepoint "nom" does not exist (3B001) ;
  • hors d'un bloc de transaction, SAVEPOINT, RELEASE et ROLLBACK TO sont refusés (25P01, SAVEPOINT can only be used in transaction blocks, de même RELEASE SAVEPOINT … et ROLLBACK TO SAVEPOINT …) ;
  • ROLLBACK TO défait aussi les SET faits depuis le point (37.7), et les instructions de définition annulables exécutées depuis le point (37.1) ;
  • après une erreur de la classe 40 (conflit, interblocage), la transaction est déjà annulée en entier : ROLLBACK TO est refusé (25P01) et le bloc se termine par ROLLBACK.

Différences avec PostgreSQL :

  • les lignes tenues depuis le point (FOR UPDATE) restent tenues jusqu'à la fin de la transaction ;
  • édition Cluster : SAVEPOINT est refusé dans une transaction qui écrit les partitions d'un autre nœud.

37.3 Niveaux d'isolement#

MIRAJ n'a qu'un comportement, le plus strict : SERIALIZABLE. Tout se passe comme si les transactions s'exécutaient l'une après l'autre. Les quatre niveaux de la norme sont acceptés et mémorisés (les pilotes les envoient à l'ouverture d'une transaction, et les relisent), mais aucun ne relâche l'isolation : une transaction ouverte en READ COMMITTED — le niveau annoncé par défaut — lit elle aussi un instantané fixe et peut recevoir un conflit à sa validation.

InstructionPortée
BEGIN ISOLATION LEVEL …, START TRANSACTION ISOLATION LEVEL …la transaction ouverte
SET TRANSACTION ISOLATION LEVEL …, SET TRANSACTION READ ONLY | READ WRITEla transaction en cours (à écrire après BEGIN)
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL …les transactions suivantes de la session
SET SESSION CHARACTERISTICS AS TRANSACTION READ ONLY | READ WRITEtoutes les instructions suivantes de la session, hors bloc comprises
SHOW TRANSACTION ISOLATION LEVEL, SHOW transaction_isolation, current_setting('transaction_isolation')niveau en vigueur
SHOW transaction_isolation;                 -- read committed (valeur par défaut)
BEGIN ISOLATION LEVEL SERIALIZABLE;
SHOW TRANSACTION ISOLATION LEVEL;           -- serializable
COMMIT;
SHOW TRANSACTION ISOLATION LEVEL;           -- read committed
SET SESSION CHARACTERISTICS AS TRANSACTION ISOLATION LEVEL REPEATABLE READ;
BEGIN;
SHOW transaction_isolation;                 -- repeatable read
SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;
SHOW transaction_isolation;                 -- read uncommitted
COMMIT;
SHOW default_transaction_isolation;         -- repeatable read

Le niveau annoncé par défaut d'une session est read committed, comme dans PostgreSQL ; RESET ALL et SET default_transaction_isolation TO DEFAULT y reviennent. SET default_transaction_isolation TO 'serializable' et SET default_transaction_read_only TO on valent SET SESSION CHARACTERISTICS AS TRANSACTION … : ils fixent le niveau et le mode des transactions suivantes de la session (une écriture y reçoit alors 25006, hors bloc comprise ; BEGIN READ WRITE ouvre malgré tout une transaction en écriture). SHOW default_transaction_isolation et SHOW default_transaction_read_only rendent le niveau et le mode des transactions suivantes, tels que la dernière de ces instructions les a fixés, SHOW transaction_isolation et SHOW transaction_read_only le niveau et le mode en vigueur (ceux de la transaction en cours dans un bloc, ceux des transactions suivantes hors bloc). SET transaction_isolation et SET transaction_read_only valent SET TRANSACTION … : la transaction en cours seule.

SET default_transaction_read_only TO on;
UPDATE client SET ville = ville WHERE id = 1;
BEGIN READ WRITE;
UPDATE client SET ville = ville WHERE id = 1;
COMMIT;
RESET default_transaction_read_only;
SET
ERROR:  Cannot execute statement in a READ ONLY transaction.   (25006)
BEGIN
UPDATE 1
COMMIT
RESET

Avec psycopg2, connection.set_session(isolation_level='REPEATABLE READ', readonly=True) envoie ces instructions, et SHOW transaction_isolation rend ensuite repeatable read.


37.4 Modèle de concurrence#

L'isolation sérialisable est obtenue par contrôle multiversion optimiste : chaque transaction travaille sur son propre instantané des données, sans poser de verrou de ligne. Rien n'attend, sauf une ligne tenue explicitement par SELECT … FOR UPDATE (37.6). En contrepartie, une transaction peut être annulée par un conflit ; l'application la recommence (37.5).

SituationComportement
Un SELECT pendant qu'une autre session écritne bloque jamais, ne voit jamais un état à moitié écrit, n'est jamais annulé par cette écriture
Deux transactions modifient la même lignela première garde la ligne ; la seconde reçoit 40001 tout de suite, sans attendre (sauf mise à jour différée, voir plus bas : les deux valident)
Une transaction écrit après avoir lu une ligne qu'une autre a modifiée et validée entre-temps40001 à son COMMIT
Deux transactions insèrent la même clé uniquela seconde reçoit 40001 tout de suite
SELECT … FOR UPDATE, UPDATE ou DELETE d'une ligne tenue par un FOR UPDATEattend la fin de la transaction qui la tient
Interblocage entre deux lectures verrouillantesdétecté aussitôt : la transaction qui fermerait le cycle reçoit 40P01, l'autre continue
DDL sur une table qu'une transaction ouverte a lue ou écriterefusé tout de suite (55P03, verrou non obtenu) ; la transaction ouverte n'est pas touchée

Les lecteurs lisent un instantané#

-- Session A                                   -- Session B
BEGIN;
SELECT stock FROM article WHERE id = 2;        -- 200
                                                UPDATE article SET stock = stock + 5 WHERE id = 2;
SELECT stock FROM article WHERE id = 2;        -- 200 : l'instantané de A ne bouge pas
COMMIT;
SELECT stock FROM article WHERE id = 2;        -- 205

Il en va de même d'une transaction ouverte en READ COMMITTED. Une transaction qui ne fait que lire n'est jamais annulée.

Le conflit : SQLSTATE 40001#

ERROR:  could not serialize access due to concurrent update
SQLSTATE : 40001

MIRAJ rend ce code (serialization_failure de PostgreSQL, classe 40 « annulation de transaction ») pour tout conflit de concurrence : écriture d'une ligne qu'une autre transaction tient, lecture périmée découverte à la validation. Un vrai interblocage (cycle d'attentes entre lectures verrouillantes, 37.6) rend 40P01 (deadlock_detected, message deadlock detected). Un pilote range les deux dans la même famille (psycopg2 : TransactionRollbackError, dont SerializationFailure et DeadlockDetected), et la réponse est la même : aucune donnée n'est perdue ni abîmée, cette transaction doit seulement être recommencée depuis son début. Un DDL (ALTER TABLE, DROP TABLE, CREATE INDEX, TRUNCATE…) refusé parce qu'une transaction ouverte tient la table n'est ni l'un ni l'autre : il rend 55P03 (lock_not_available, message could not obtain lock on relation: it is being written by an open transaction), comme un verrou que PostgreSQL n'obtiendrait pas dans le délai de lock_timeout — ici sans attendre. L'instruction se rejoue telle quelle une fois la transaction terminée.

-- Session A                                   -- Session B
BEGIN;                                          BEGIN;
UPDATE client SET ville = 'Lyon 3'
 WHERE nom = 'Claire Dupont';                   -- UPDATE 1
                                                UPDATE client SET ville = 'Lyon 7'
                                                 WHERE nom = 'Claire Dupont';
                                                -- ERROR 40001, immédiat
                                                ROLLBACK;
COMMIT;                                         -- COMMIT : 'Lyon 3'

Le conflit peut aussi n'apparaître qu'à la validation, quand ce que la transaction a lu n'est plus vrai :

-- Session A                                   -- Session B
BEGIN;                                          BEGIN;
SELECT stock FROM article WHERE id = 2;        -- 200
                                                UPDATE article SET stock = stock + 10
                                                 WHERE code = 'CAH-96';
                                                COMMIT;
UPDATE article SET stock = 0
 WHERE code = 'CLA-A4';                         -- UPDATE 1
COMMIT;                                         -- ERROR 40001 : la lecture de A est périmée

Après un 40001 ou un 40P01, la transaction est déjà annulée en entier, et le bloc reste en échec, comme dans PostgreSQL : toute instruction reçoit 25P02 jusqu'au ROLLBACK (un COMMIT rend ROLLBACK). Un ROLLBACK TO SAVEPOINT ne la rattrape pas (25P01) : il n'y a plus de point de reprise. L'application reprend donc la transaction à BEGIN, après le ROLLBACK.

COMMIT;      ERROR:  could not serialize access due to concurrent update   (40001)
SELECT 1;    ERROR:  current transaction is aborted, commands ignored until end of transaction block   (25P02)
ROLLBACK;    ROLLBACK

Hors d'un bloc, une instruction isolée qui entre en conflit est rejouée par le moteur (trois tentatives rapprochées) avant de rendre 40001 : elle n'échoue que si l'autre transaction tient encore la ligne à ce moment-là.

Mise à jour différée d'une ligne très sollicitée#

Un compteur, ou le stock d'un article vendu par toutes les caisses, est une ligne que beaucoup de transactions modifient en même temps. MIRAJ traite à part les UPDATE qui s'y prêtent : un tel UPDATE ne prend pas la ligne, il est recalculé au COMMIT sur la dernière valeur validée.

-- stock de l'article 1 : 500
-- Session A                                   -- Session B
BEGIN;                                          BEGIN;
UPDATE article SET stock = stock - 1
 WHERE id = 1;                                  -- UPDATE 1
                                                UPDATE article SET stock = stock - 2
                                                 WHERE id = 1;       -- UPDATE 1, pas de conflit
                                                COMMIT;              -- stock = 498
COMMIT;                                         -- stock = 497, aucune erreur

Tant qu'elle n'a pas validé, la transaction qui a fait l'UPDATE relit la ligne d'après son propre instantané ; une telle lecture rend l'UPDATE dépendant de ce qu'elle a lu, et le COMMIT reçoit alors 40001 si une autre transaction a modifié la ligne entre-temps.

Un UPDATE est différé quand il vise une ligne désignée par sa clé (égalités sur toutes les colonnes de la clé primaire ou d'une contrainte d'unicité), qu'il ne modifie ni une clé, ni une clé étrangère, ni une colonne d'identité, qu'il n'a ni sous-requête, ni fonction non déterministe (random()…), ni clause RETURNING, et que les déclencheurs de la table s'y prêtent. Sinon il prend la ligne, comme dans les exemples plus haut.

Ce que cela change pour l'application :

  • Erreurs au COMMIT. Si la ligne a disparu, ou si la condition du WHERE ne tient plus au moment de valider, le COMMIT rend 40001. Si le recalcul lui-même échoue, le COMMIT rend l'erreur correspondante — par exemple 23514 quand deux ventes concurrentes feraient passer le stock sous zéro alors qu'une contrainte CHECK (stock >= 0) l'interdit (new row for relation "article" violates check constraint "stock_positif"). Dans les deux cas, toute la transaction est annulée.
  • Un UPDATE qui porte sa condition (… WHERE id = 3 AND stock >= 2) n'échoue que si la condition ne tient plus au COMMIT : c'est la forme à préférer à « lire puis écrire ».
  • Réglage : SET deferred_update TO off rétablit la prise immédiate de la ligne pour la session (SHOW deferred_update rend alors 0) ; RESET deferred_update revient à la valeur par défaut (1, activé).

37.5 Réessayer une transaction#

Toute transaction qui écrit doit être prête à recevoir une erreur de la classe 40 et à recommencer en entier, après ROLLBACK, depuis BEGIN : pas seulement la dernière instruction. Avec psycopg2 :

import time
import psycopg2

conn = psycopg2.connect(host="127.0.0.1", port=7007, user="app", password="…", dbname="ventes")

def commander(conn, client_id, article_id, quantite, essais=5):
    for essai in range(1, essais + 1):
        try:
            with conn:                           # BEGIN … COMMIT, ROLLBACK si une exception sort
                with conn.cursor() as cur:
                    cur.execute("INSERT INTO commande (client_id) VALUES (%s) RETURNING id", (client_id,))
                    commande_id = cur.fetchone()[0]
                    cur.execute("INSERT INTO ligne_commande "
                                "SELECT %s, id, %s, prix FROM article WHERE id = %s",
                                (commande_id, quantite, article_id))
                    cur.execute("UPDATE article SET stock = stock - %s WHERE id = %s",
                                (quantite, article_id))
            return commande_id
        except psycopg2.errors.TransactionRollbackError:   # classe 40 : 40P01, 40001
            time.sleep(0.01 * essai)             # courte pause, puis nouvelle tentative
    raise RuntimeError("commande abandonnée après %d essais" % essais)

commander(conn, 1, 2, 5)

Règles pratiques :

  • tester la classe 40 (les deux premiers caractères du SQLSTATE) plutôt qu'un code précis ;
  • reprendre aussi les lectures : les valeurs lues pendant la tentative annulée ne valent plus ;
  • garder les transactions courtes, et préférer un UPDATE qui porte sa condition à un SELECT suivi d'un UPDATE ;
  • pour un « lire, décider, écrire » qui ne doit pas échouer, tenir la ligne avec SELECT … FOR UPDATE (37.6).

37.6 Lectures verrouillantes et verrous#

SELECT … FOR UPDATE#

SELECT … FOR {UPDATE | NO KEY UPDATE | SHARE | KEY SHARE} [NOWAIT | SKIP LOCKED];

La requête lit les lignes et les tient jusqu'à la fin de la transaction : aucune autre transaction ne peut les écrire, ni les tenir à son tour. FOR SHARE et FOR KEY SHARE laissent les autres lecteurs FOR SHARE les tenir aussi ; FOR NO KEY UPDATE se comporte comme FOR UPDATE.

BEGIN;
SELECT stock FROM article WHERE id = 3 FOR UPDATE;    -- la ligne est tenue
-- … contrôle du stock par l'application …
UPDATE article SET stock = stock - 1 WHERE id = 3;
COMMIT;                                                -- la ligne est rendue
  • Une ligne déjà tenue est attendue : un autre SELECT … FOR UPDATE, un UPDATE ou un DELETE de cette ligne attend la fin de la transaction qui la tient, puis s'exécute sur la valeur validée. Deux transactions qui prennent leurs lignes dans le même ordre ne reçoivent aucune erreur.
  • Délai : au-delà de lock_wait_timeout secondes (50 par défaut), l'instruction échoue en 55P03 (Lock wait timeout exceeded) ; dans un bloc, celui-ci passe en échec. SET lock_wait_timeout TO 5 change ce délai pour la session ; SET lock_timeout TO '500ms' (le paramètre de PostgreSQL, en millisecondes ou avec une unité) le remplace dès qu'il est posé. SET lock_timeout TO 0 signifie, comme dans PostgreSQL, attendre sans limite ; RESET lock_timeout rend le délai de lock_wait_timeout. Tant que lock_timeout n'a pas été posé, SHOW lock_timeout rend 0 mais c'est lock_wait_timeout qui borne l'attente.
  • NOWAIT : 55P03 tout de suite si une ligne est tenue.
  • SKIP LOCKED : les lignes tenues sont écartées du résultat — le motif « file de travaux ».
  • Interblocage (A tient 1 et demande 2, B tient 2 et demande 1) : la transaction qui fermerait le cycle reçoit 40P01 et est annulée ; l'autre obtient sa ligne.
  • Hors d'un bloc de transaction, la ligne n'est tenue que le temps de l'instruction.
-- Session A tient l'article 3 par FOR UPDATE.     Session B :
SELECT stock FROM article WHERE id = 3 FOR UPDATE NOWAIT;
-- ERROR:  Lock wait timeout exceeded; try restarting transaction   (55P03)
SELECT id, stock FROM article ORDER BY id FOR UPDATE SKIP LOCKED;
--  id | stock
-- ----+-------
--   1 |   500
--   2 |   200
-- (2 lignes)

FOR SHARE tenu par A laisse B prendre la ligne FOR KEY SHARE ou FOR SHARE, mais un FOR UPDATE ou FOR NO KEY UPDATE … NOWAIT de B rend 55P03.

LOCK TABLE#

LOCK [TABLE] nom [, …] [IN mode MODE] [NOWAIT] est accepté dans ses huit modes (ACCESS SHARE … ACCESS EXCLUSIVE). Hors d'un bloc de transaction, il est refusé comme dans PostgreSQL (25P01, LOCK TABLE can only be used in transaction blocks).

Deux modes posent un verrou, dans les deux modèles de concurrence, tenu jusqu'au COMMIT ou au ROLLBACK (XA COMMIT / XA ROLLBACK pour une branche XA, même préparée, y compris à travers un arrêt du serveur, 37.11) :

ModeLes autres transactions…
ACCESS EXCLUSIVE (le défaut, et celui d'un TRUNCATE dans un bloc)ne lisent ni n'écrivent plus la table
EXCLUSIVEla lisent encore, mais ne l'écrivent plus (SELECT … FOR UPDATE compris)

Le verrou n'est pris qu'une fois la table libre : LOCK TABLE attend la fin des autres transactions qui y tiennent des lignes, l'ont écrite ou en tiennent le verrou — et, pour ACCESS EXCLUSIVE seulement, de celles qui l'ont simplement lue (l'ACCESS SHARE de PostgreSQL, tenu jusqu'à la fin de la transaction ; EXCLUSIVE ne l'attend pas, comme dans PostgreSQL), dans les deux modèles de concurrence. Une lecture en auto-validation ne tient rien une fois finie. Une session qui trouve la table verrouillée attend de même la fin de la transaction qui la tient. L'attente est bornée par lock_timeout (puis 55P03, lock_not_available) ; avec NOWAIT, 55P03 tout de suite ; un cycle d'attentes rend 40P01 à l'une des transactions. La transaction qui tient le verrou continue de lire et d'écrire la table normalement.

BEGIN;
LOCK TABLE article IN ACCESS EXCLUSIVE MODE;   -- les autres sessions attendent
UPDATE article SET prix = prix * 1.02;
COMMIT;                                         -- verrou rendu

Les autres modes (ACCESS SHARE … SHARE ROW EXCLUSIVE) ne posent aucun verrou : l'isolation sérialisable de MIRAJ les rend inutiles. Une vue ou une table temporaire n'est pas verrouillée.

Verrous consultatifs#

Des verrous que l'application nomme elle-même, pour sérialiser un traitement (une clôture, un import) sans verrouiller de données.

FonctionEffet
pg_advisory_lock(clé)prend le verrou pour la session, en attendant s'il est tenu par une autre session
pg_try_advisory_lock(clé)le prend pour la session s'il est libre : t, sinon f, sans attendre
pg_advisory_unlock(clé)rend un verrou de session : t ; f avec l'avertissement you don't own a lock of type ExclusiveLock si la session ne le tenait pas
pg_advisory_xact_lock(clé)prend le verrou pour la transaction, en attendant s'il est tenu
pg_try_advisory_xact_lock(clé)le prend pour la transaction s'il est libre : t, sinon f

La clé est un bigint, ou deux integer (pg_try_advisory_lock(1, 2)). Un même verrou ne peut être tenu que par une session à la fois, qu'il ait été pris pour la session ou pour la transaction.

  • Les verrous de session sont cumulables (pris deux fois, ils se rendent deux fois) : ni COMMIT ni ROLLBACK ne les rendent, seuls pg_advisory_unlock et la fermeture de la session.
  • Les verrous de transaction sont rendus d'eux-mêmes à la fin de la transaction, validée ou annulée ; hors d'un bloc, à la fin de l'instruction.
SELECT pg_try_advisory_lock(42);     -- t
-- … traitement exclusif …
SELECT pg_advisory_unlock(42);       -- t

BEGIN;
SELECT pg_advisory_xact_lock(7);     -- une autre session : pg_try_advisory_lock(7) rend f
-- … traitement …
COMMIT;                              -- verrou 7 rendu

37.7 Paramètres de session : SET, SHOW, RESET#

SET [SESSION | LOCAL] nom {TO | =} {valeur | 'valeur' | DEFAULT};
SHOW nom;
SHOW ALL;
RESET nom;
RESET ALL;
  • SET change un paramètre pour la session. Fait dans un bloc de transaction, il est défait si la transaction est annulée (ROLLBACK, COMMIT en échec) et par un ROLLBACK TO SAVEPOINT vers un point posé avant lui. Une liste (SET nom TO a, b) est une seule valeur, le texte a, b.
  • SET LOCAL ne vaut que jusqu'à la fin de la transaction en cours, validée ou annulée ; un SET de session du même paramètre fait après lui l'emporte. Hors d'un bloc de transaction, il est sans effet et rend l'avertissement SET LOCAL can only be used in transaction blocks (25P01).
  • Un nom qui n'est pas un paramètre de PostgreSQL (ni une variable de MIRAJ) est refusé (42704, unrecognized configuration parameter "nom"), par SET comme par RESET (SHOW nom : 42704, Unknown system variable 'nom'). Tous les paramètres de configuration de PostgreSQL 16 et 17 sont reconnus, sans tenir compte de la casse : ceux qui sont sans objet ici sont mémorisés sans effet (tableau ci-dessous), si bien que les SET qu'émettent pg_dump, pg_restore, psql et les pilotes passent.
  • Un nom à point (SET monappli.locataire TO '42') est un paramètre personnalisé : mémorisé pour la session, relu par SHOW monappli.locataire et current_setting('monappli.locataire') (42704 tant qu'il n'a pas été posé), et soumis aux mêmes règles de transaction.
  • SHOW nom rend une ligne et une colonne, nommée comme le paramètre. Un nom inconnu donne 42704.
  • SHOW ALL rend tous les paramètres (name, setting, description), comme la table pg_catalog.pg_settings ; les variables de MIRAJ et les paramètres personnalisés posés y figurent aussi.
  • RESET nom et SET nom TO DEFAULT rendent au paramètre sa valeur du début de la session : celle des options de la connexion si elles le posent (40.5), sinon sa valeur par défaut. Cela vaut pour les paramètres de PostgreSQL comme pour les variables de MIRAJ (lock_wait_timeout, statement_timeout…) ; une variable de MIRAJ que les options ne posent pas reprend la valeur courante du serveur (un SET GLOBAL fait depuis le début de la session est repris).
  • RESET ALL rend à tous les paramètres leur valeur du début de la session, schéma courant (search_path) compris (37.8).
  • current_setting('nom') lit un paramètre dans une expression (42704 s'il est inconnu, NULL avec current_setting('nom', true)).
  • set_config('nom', 'valeur', is_local) pose un paramètre depuis une expression et rend la valeur : c'est un SET (is_local faux) ou un SET LOCAL (is_local vrai), avec les mêmes contrôles et les mêmes règles de transaction. Voir « set_config » ci-dessous.

Les paramètres sont de deux sortes : ceux de PostgreSQL, que les clients et pilotes envoient et relisent, et les variables de MIRAJ, qui règlent le moteur. Les uns comme les autres se lisent par SHOW.

Paramètres de PostgreSQL#

ParamètreValeur, effet
TimeZonefuseau de la session, voir 37.9
statement_timeoutdurée maximale d'une instruction, voir 37.9
search_pathschéma courant, voir 37.8
transaction_isolation, transaction_read_onlyvoir 37.3 ; lus par SHOW
application_nametexte libre, donné à la connexion ou par SET ; SHOW application_name, current_setting('application_name'), pg_settings et, pour chaque session, pg_stat_activity.application_name le rendent
client_encodingtoujours UTF8 ; une autre valeur est refusée (0A000) ; SET NAMES 'UTF8' est accepté
DateStyletoujours ISO, MDY ; un autre style de sortie est refusé (0A000)
standard_conforming_stringson
extra_float_digits, IntervalStyleacceptés ; SHOW rend 1 et postgres
server_version, server_version_num16.4, 160004 : la version de PostgreSQL annoncée aux clients
is_superuser, session_authorizationon pour un compte qui a tous les privilèges sur *.* (rôles actifs compris), nom du compte sous lequel la session agit ; ils suivent SET SESSION AUTHORIZATION et SET ROLE (38.4), et sont renvoyés au client quand ils changent
lock_timeoutattente maximale d'une ligne tenue (55P03 au-delà), en millisecondes ou avec une unité ('2s') ; 0 : sans limite ; jamais posé : celle de lock_wait_timeout (37.6)
session_replication_roleorigin (défaut), local ou replica. replica suspend pour la session les déclencheurs et les clés étrangères (contrôles et actions ON DELETE / ON UPDATE), comme pour l'application d'un flux de réplication ou le chargement d'un vidage ; SHOW foreign_key_checks rend alors 0. Réservé aux comptes qui ont SUPER ou SYSTEM_VARIABLES_ADMIN (42501 sinon). Les lignes chargées ainsi ne sont pas contrôlées au retour à origin
idle_in_transaction_session_timeoutune session restée sans rien envoyer dans un bloc de transaction (ouvert ou en échec) plus longtemps que ce délai est fermée par le serveur (FATAL, 25P03, terminating connection due to idle-in-transaction timeout) et sa transaction annulée ; 0 : aucun délai. Appliqué par le serveur réseau (sans objet en mode embarqué)
default_transaction_isolation, default_transaction_read_onlyniveau et mode des transactions suivantes de la session, voir 37.3
work_mem, synchronous_commit, enable_seqscan, jit, client_min_messages, row_security, check_function_bodies, xmloption, default_tablespace, default_table_access_method, transaction_timeout, log_statement, geqo*, *_cost, max_parallel_*…tous les autres paramètres de session de PostgreSQL : acceptés et mémorisés sans effet, SHOW, current_setting et pg_settings rendent la valeur posée (vide, ou la valeur usuelle de PostgreSQL pour les plus courants, tant qu'ils ne l'ont pas été)
server_version, server_encoding, integer_datetimes, is_superuser, block_size, wal_block_size…en lecture seule : SET rend 55P02 (parameter "nom" cannot be changed)
shared_buffers, max_connections, wal_level, port, listen_addresses…réglés au démarrage du serveur PostgreSQL : SET, SET LOCAL, RESET et set_config rendent 55P02 (parameter "nom" cannot be changed without restarting the server) ; dans les options de la connexion, ils la refusent. SHOW les lit (vide)
fsync, autovacuum, ssl, checkpoint_timeout, log_destination, archive_command…réglés dans le fichier de configuration de PostgreSQL : 55P02 de même (parameter "nom" cannot be changed now)
log_connections, post_auth_delay…réglés à l'ouverture de la connexion : SET rend 55P02 (parameter "nom" cannot be set after connection start)

Variables de MIRAJ utiles en session#

VariableDéfautRôle
autocommit1voir 37.1
lock_wait_timeout50attente maximale d'une ligne tenue, en secondes (37.6)
deferred_update1mise à jour différée (37.4)
sql_modecelui du serveurl'élément POSTGRESQL ne peut pas être retiré par SET : le langage est fixé au démarrage

SHOW VARIABLES LIKE 'lock%', @@lock_wait_timeout et les variables utilisateur (SET @v = 5, SELECT @v) restent disponibles comme extensions de MIRAJ.

SET statement_timeout TO '5s';
BEGIN;
SET LOCAL statement_timeout TO '30s';     -- pour cette transaction seulement
SET monappli.locataire TO '42';
SAVEPOINT s;
SET lock_timeout TO '2s';
ROLLBACK TO SAVEPOINT s;                  -- lock_timeout revient à 0
COMMIT;
SHOW statement_timeout;                   -- 5s
SHOW monappli.locataire;                  -- 42

set_config#

SELECT set_config('monappli.locataire', '42', false);   -- comme SET : pour la session
BEGIN;
SELECT set_config('monappli.locataire', '7', true);     -- comme SET LOCAL : pour la transaction
SELECT current_setting('monappli.locataire');           -- 7
COMMIT;
SELECT current_setting('monappli.locataire');           -- 42
  • Le nom est celui d'un paramètre de PostgreSQL, d'une variable de MIRAJ ou d'un paramètre personnalisé ; search_path, TimeZone, statement_timeout et role se posent ainsi comme par leur SET. Un nom inconnu rend 42704, un nom mal formé 42602, un paramètre du serveur 55P02, une valeur refusée 22023 ou 0A000.
  • Une valeur NULL rend au paramètre sa valeur du début de la session (comme RESET) ; la fonction rend alors NULL.
  • is_local vrai hors d'un bloc de transaction : la valeur ne vaut que pour l'instruction, sans avertissement ; rien n'en reste ensuite.
  • La valeur passe en paramètre lié (SELECT set_config('monappli.locataire', $1, true)) et la fonction s'emploie dans toute instruction, y compris dans le corps d'une fonction ou d'une procédure PL/pgSQL (PERFORM set_config(…)), dont les instructions suivantes lisent la valeur par current_setting.
  • Le paramètre est appliqué à la fin de l'instruction qui appelle set_config, si elle réussit (une instruction en échec ne pose rien).

Différences avec PostgreSQL#

  • set_config applique sa valeur à la fin de l'instruction et non au moment de l'appel : dans la même instruction SQL, current_setting peut encore rendre l'ancienne valeur (SELECT set_config('a.b', 'x', false), current_setting('a.b')) ; lisez-la par l'instruction suivante. Une valeur invalide (22023) est signalée à ce moment-là : en auto-validation, les écritures de l'instruction sont alors déjà validées.
  • session_replication_role = replica suspend tous les déclencheurs de l'utilisateur : il n'y a pas de déclencheur ENABLE REPLICA ou ENABLE ALWAYS qui s'exécuterait quand même. L'historique d'une table versionnée continue d'être tenu.
  • Une transaction XA (37.11) défait ses SET à XA ROLLBACK et met fin à ses SET LOCAL à sa fin, y compris après XA PREPARE — là où PREPARE TRANSACTION de PostgreSQL garde les SET de la transaction comme s'ils étaient validés.

37.8 Schéma courant et search_path#

Chaque base de MIRAJ est un schéma PostgreSQL, et il n'y a qu'une base PostgreSQL, miraj. La session a un schéma courant : celui où une table nommée sans schéma est cherchée et créée. Il est fixé à la connexion par le paramètre dbname (quand il nomme une base de MIRAJ), puis par :

SET search_path TO ventes;
SET search_path TO ventes, public;

SET search_path fait du premier schéma nommé qui existe (et que le compte peut lire) le schéma courant. Il n'y a pas de chemin à plusieurs étages : les schémas suivants de la liste ne sont pas consultés.

SET search_path TO ventes, archives;
SELECT count(*) FROM client;              -- ventes.client, jamais archives.client
SELECT count(*) FROM archives.client;     -- une table d'un autre schéma se nomme en entier
SHOW search_path;                         -- ventes
ÉcritureEffet
SET search_path TO a, bschéma courant : le premier de a, b qui existe
SET search_path TO inexistantaccepté ; le schéma courant ne change pas
SET search_path TO public, TO "$user", publicaccepté ; public désigne le schéma courant, qui ne change pas
SET search_path TO ''accepté ; le schéma courant ne change pas (pg_dump l'écrit set_config('search_path', '', false))
RESET search_path, SET search_path TO DEFAULT, RESET ALL, DISCARD ALLschéma courant du début de la session : celui de la connexion (dbname, search_path du message de démarrage) ou du dernier USE ; aucun si la session s'est ouverte sans base. Gardé tel qu'il est si ce schéma a été supprimé depuis
SHOW search_pathle schéma courant, seul
current_schema, current_schema()le schéma courant
current_schemas(true), current_schemas(false){pg_catalog,ventes}, {ventes}
current_database()miraj
USE ventesextension de MIRAJ : même effet que SET search_path TO ventes

pg_catalog passe avant tout schéma, comme dans PostgreSQL : SELECT * FROM pg_class lit la table du catalogue (40. Catalogue et outils). Un nom qualifié par public. désigne le schéma courant.


37.9 Fuseau horaire et délai d'instruction#

Fuseau de la session#

SET TIME ZONE 'Europe/Paris';
SET TimeZone TO 'UTC';
SET TIME ZONE -5;            -- décalage en heures
SET TIME ZONE LOCAL;         -- fuseau de la machine du serveur
SET TIME ZONE DEFAULT;       -- valeur par défaut du serveur
SHOW TIME ZONE;              -- ou SHOW timezone

Le fuseau de la session règle now(), la lecture et l'affichage des timestamptz, AT TIME ZONE et current_setting('TimeZone') (32. Types de données, 36. Fonctions SQL). Un fuseau inconnu est refusé :

ERROR:  invalid value for parameter "TimeZone": "Mars/Olympus"   (22023)

Le fuseau par défaut est celui du serveur ; un client peut en demander un autre dès l'ouverture de la connexion par ses options (options=-c TimeZone=Europe/Paris, voir 40.5), ou par le paramètre TimeZone du message de démarrage, que pgjdbc et Npgsql envoient d'eux-mêmes : la session prend alors le fuseau du client. Un fuseau inconnu du serveur y est ignoré. Un décalage écrit en texte (SET TIME ZONE '+02:00') se lit à l'est de Greenwich (l'heure de la session est alors en avance de deux heures sur UTC), à l'inverse de la notation POSIX de PostgreSQL : préférez un nom de fuseau. Après SET TIME ZONE LOCAL, SHOW TimeZone rend le nom du fuseau de la machine du serveur (par exemple Etc/GMT-1).

Durée maximale d'une instruction#

SET statement_timeout TO 10000;     -- millisecondes : SHOW rend 10s
SET statement_timeout TO 1500;      -- SHOW rend 1500ms
SET statement_timeout TO '2s';      -- ou '1min'
SHOW statement_timeout;             -- 2s
SELECT pg_sleep(3);
RESET statement_timeout;            -- valeur du serveur
ERROR:  Query was interrupted: execution time limit 2.0 sec exceeded   (57014)

RESET statement_timeout rend la durée maximale réglée sur le serveur (max_statement_time, 300 secondes sans autre réglage : SHOW statement_timeout rend alors 5min) ; SET statement_timeout TO 0 supprime toute limite pour la session.

Une instruction annulée par le client (Ctrl-C de psql, connection.cancel() de psycopg2) ou par KILL QUERY reçoit le même SQLSTATE 57014. Annulée pendant une attente (ligne tenue par une autre transaction, table occupée par l'instruction longue d'une autre session), elle n'écrit rien ; un COMMIT annulé avant d'avoir commencé ne valide rien et laisse le bloc en échec.

Heure d'une transaction#

Comme dans PostgreSQL, now(), current_timestamp, transaction_timestamp(), current_date, current_time, localtimestamp et les colonnes DEFAULT now() rendent, dans un bloc de transaction, l'heure du début de la transaction (son BEGIN) : toutes les lignes qu'elle écrit portent le même instant. statement_timestamp() rend l'heure du début de l'instruction et clock_timestamp() l'heure de l'horloge à chaque appel. Hors d'un bloc, chaque instruction est sa propre transaction : now() et statement_timestamp() y sont égaux.

BEGIN;
SELECT now() = statement_timestamp();
SELECT pg_sleep(1);
SELECT now() = statement_timestamp(), now() = transaction_timestamp(), now() < clock_timestamp();
COMMIT;
BEGIN
 now() = statement_timestamp()
-------------------------------
 t
(1 ligne)

 pg_sleep(1)
-------------
           0
(1 ligne)

 now() = statement_timestamp() | now() = transaction_timestamp() | now() < clock_timestamp()
-------------------------------+---------------------------------+---------------------------
 f                             | t                               | t
(1 ligne)

COMMIT

37.10 Curseurs, instructions préparées, remise à zéro#

Curseurs SQL#

DECLARE nom [SCROLL | NO SCROLL] CURSOR [WITH HOLD | WITHOUT HOLD] FOR requête;
FETCH [sens] [FROM | IN] nom;
MOVE [sens] [FROM | IN] nom;
CLOSE {nom | ALL};

sens : NEXT | PRIOR | FIRST | LAST | ABSOLUTE n | RELATIVE n | n | ALL
     | FORWARD [n | ALL] | BACKWARD [n | ALL]

Sans sens, FETCH nom lit la ligne suivante (NEXT). Les nombres sont signés (FETCH -1 recule d'une ligne, ABSOLUTE -1 est la dernière) ; 0 (FETCH 0, RELATIVE 0) relit la ligne courante.

Un curseur lit le résultat d'une requête par morceaux ; c'est ce qu'emploient les « curseurs nommés » de psycopg2 (conn.cursor(name='lecture')). Le résultat entier est calculé à DECLARE et gardé par le serveur ; la lecture se fait dans les deux sens, avec les mêmes positions que PostgreSQL (au-delà de la dernière ligne, FETCH PRIOR rend la dernière ; MOVE rend le nombre de lignes que FETCH aurait rendues).

BEGIN;
DECLARE c_art CURSOR FOR SELECT id, code FROM article ORDER BY id;
FETCH 2 FROM c_art;
FETCH NEXT FROM c_art;
FETCH ALL FROM c_art;
CLOSE c_art;
COMMIT;
BEGIN
DECLARE CURSOR
 id |  code
----+--------
  1 | STY-01
  2 | CAH-96
(2 lignes)

FETCH 2
 id |  code
----+--------
  3 | CLA-A4
(1 ligne)

FETCH 1
 id | code
----+------
(0 lignes)

FETCH 0
CLOSE CURSOR
COMMIT
  • Sans WITH HOLD, un curseur exige un bloc de transaction (25P01, DECLARE CURSOR can only be used in transaction blocks) et disparaît à sa fin.
  • WITH HOLD se déclare hors transaction et survit aux COMMIT ; fermez-le par CLOSE.
  • CLOSE ALL ferme tous les curseurs de la session.
  • Un curseur fermé ou inconnu : 34000 (cursor "c5" does not exist) ; dans un bloc, l'erreur le met en échec comme toute autre.
  • SCROLL et NO SCROLL ensemble : 42P11 (cannot specify both SCROLL and NO SCROLL).
  • Un curseur NO SCROLL refuse tout recul, FETCH 0 et le retour au début compris : 55000 (cursor can only scan forward). Sans SCROLL ni NO SCROLL, le curseur se lit dans les deux sens (PostgreSQL en décide selon le plan de la requête : déclarez SCROLL pour un programme qui doit fonctionner sur les deux).
BEGIN;
DECLARE c_dos SCROLL CURSOR FOR SELECT id FROM article ORDER BY id;
FETCH LAST FROM c_dos;        -- 3
FETCH BACKWARD 2 FROM c_dos;  -- 2, puis 1
FETCH ABSOLUTE 2 FROM c_dos;  -- 2
MOVE BACKWARD ALL FROM c_dos; -- MOVE 1
COMMIT;

Instructions préparées#

PREPARE nom [(types)] AS instruction;     -- paramètres $1, $2…
EXECUTE nom [(valeurs)];
DEALLOCATE [PREPARE] {nom | ALL};
PREPARE art_par_prix (numeric) AS
  SELECT code, prix FROM article WHERE prix >= $1 ORDER BY prix;
EXECUTE art_par_prix(2);
DEALLOCATE art_par_prix;
PREPARE maj_stock (int, int) AS UPDATE article SET stock = stock + $2 WHERE id = $1;
EXECUTE maj_stock(1, 0);
DEALLOCATE ALL;
PREPARE
  code  | prix
--------+------
 CAH-96 | 2.50
 CLA-A4 | 4.90
(2 lignes)

SELECT 2
DEALLOCATE
PREPARE
UPDATE 1
DEALLOCATE ALL

La liste de types est acceptée sans contraindre les paramètres. EXECUTE rend l'étiquette de l'instruction préparée (SELECT 2, UPDATE 1…). Un nom inconnu donne 26000 (Unknown prepared statement handler (art_par_prix) given to EXECUTE). Préparer deux fois le même nom est refusé (42P05, prepared statement "q" already exists) : libérer d'abord l'instruction par DEALLOCATE. Les pilotes préparent d'ordinaire par le protocole (Parse, Bind) plutôt que par ces instructions (31. Prise en main).

DISCARD#

InstructionEffet
DISCARD ALLlibère les instructions préparées, les curseurs, les tables temporaires et les abonnements LISTEN de la session, rend aux paramètres leur valeur du début de la session (comme RESET ALL) et rend le compte de la connexion après SET SESSION AUTHORIZATION (38.4) ; refusé dans un bloc de transaction (25001, DISCARD ALL cannot run inside a transaction block)
DISCARD TEMP, DISCARD PLANS, DISCARD SEQUENCESacceptés

Un groupe de connexions peut donc recycler une session par DISCARD ALL, comme avec PostgreSQL : le schéma courant redevient lui aussi celui du début de la session (37.8). Une application qui choisit son schéma par SET search_path après la connexion le repose donc après chaque DISCARD ALL, comme avec PostgreSQL ; donnez-le plutôt à la connexion (dbname, ou search_path dans les paramètres de démarrage).

Sessions en cours#

pg_catalog.pg_stat_activity liste les sessions (pid, usename, application_name, state, query) ; pg_backend_pid() rend le numéro de la sienne. application_name y est celui que la session a donné à la connexion ou par SET, relevé à la fin de sa dernière commande. Pour interrompre une session, les fonctions de PostgreSQL et l'instruction KILL de MIRAJ ont le même effet :

SELECT pid, usename, state, query FROM pg_stat_activity ORDER BY pid;
SELECT pg_cancel_backend(14);      -- interrompt l'instruction en cours de la session 14 : t
KILL QUERY 14;                     -- de même
SELECT pg_terminate_backend(14);   -- ferme la session 14 : t
KILL 14;                           -- de même
-- toutes les sessions au repos d'une application, sauf la sienne
SELECT pg_terminate_backend(pid) FROM pg_stat_activity
WHERE pid <> pg_backend_pid() AND application_name = 'rapport' AND state = 'idle';

pg_cancel_backend(pid) et pg_terminate_backend(pid) rendent t dès la demande partie, sans attendre son effet, et NULL pour un pid NULL. Un pid inconnu donne f avec l'avertissement PID 99 is not a PostgreSQL backend process (KILL répond 42704). La demande part à l'appel, ligne par ligne : elle n'est pas reprise si l'instruction échoue ensuite, ni par un ROLLBACK. pg_terminate_backend(pid, délai) attend au plus délai millisecondes que la session ait quitté le serveur, et rend f avec un avertissement si elle y est encore. Les droits sont ceux de KILL : un compte atteint ses propres sessions, toutes avec le privilège PROCESS (ou SUPER, CONNECTION_ADMIN) ; sinon 42501, permission denied to terminate process (ou to cancel query). Une session qui se vise elle-même reçoit aussitôt 57014 (pg_cancel_backend) ou l'erreur fatale 57P01 (pg_terminate_backend).

La session fermée par pg_terminate_backend ou KILL (ou KILL CONNECTION) reçoit, avant la fin de sa connexion, la même erreur fatale que de PostgreSQL : FATAL, 57P01, terminating connection due to administrator command. Elle la reçoit dans tous les états : en attente d'une commande, pendant une requête (abandonnée ; l'erreur fatale remplace celle de l'instruction), dans un bloc de transaction (annulé, ses verrous relâchés au plus tard à la fin du flux), pendant un COPY, pendant l'attente d'un verrou (l'instruction en attente n'écrit rien, rien n'est validé). Le serveur envoie la même erreur à toutes les connexions quand il s'arrête par SHUTDOWN. Une session restée sans rien envoyer plus longtemps que le délai d'inactivité du serveur (option --idle-timeout, 8 heures par défaut) reçoit FATAL, 57P05, terminating connection due to idle-session timeout. Le message n'est pas attendu par un client qui ne lit plus ce que le serveur lui envoie : sa connexion est fermée sans lui.


37.11 Transactions XA (validation en deux phases)#

La validation en deux phases s'écrit avec les instructions XA de MIRAJ, disponibles en langage PostgreSQL (les formes PREPARE TRANSACTION, COMMIT PREPARED de PostgreSQL ne le sont pas) :

XA START 'cmd-42';
UPDATE article SET stock = stock - 1 WHERE id = 1;
XA END 'cmd-42';
XA PREPARE 'cmd-42';
XA RECOVER;                 -- liste les branches préparées
XA COMMIT 'cmd-42';         -- ou XA ROLLBACK 'cmd-42'

Une branche préparée survit à la fermeture de sa session et à l'arrêt du serveur, propre ou brutal, jusqu'à ce qu'un coordinateur la valide ou l'annule — avec ses verrous de table (LOCK TABLE … IN [ACCESS] EXCLUSIVE MODE, TRUNCATE dans la branche), comme une transaction préparée de PostgreSQL : après un redémarrage, les autres sessions attendent toujours sa décision (lock_timeout, puis 55P03) ; une table qu'elle a vidée par TRUNCATE puis écrite est reprise vidée, avec les seules lignes écrites depuis. Une branche qui ne tient qu'un verrou de table, sans rien écrire, survit elle aussi. Les SET faits dans la branche suivent son sort comme ceux d'un bloc BEGIN : défaits par XA ROLLBACK, gardés par XA COMMIT, et les SET LOCAL finissent avec elle. La session qui tranche une branche préparée par une autre session (fermée depuis) garde ses propres paramètres. Les états, les règles et les limites sont décrits au chapitre 13.


37.12 Non pris en charge et écarts#

FormeRéponse ou comportement
PREPARE TRANSACTION 'x', COMMIT PREPARED, ROLLBACK PREPARED42601 (erreur de syntaxe) ; employez XA (37.11)
BEGIN ISOLATION LEVEL SNAPSHOT, SET TRANSACTION SNAPSHOT '…'42601
pg_advisory_lock_shared, pg_advisory_unlock_all42883 (FUNCTION … does not exist)
pg_cursors, pg_prepared_statements42P01 : ces vues n'existent pas
CHECKPOINT42601
SET client_encoding TO 'LATIN1'0A000 (the server only speaks UTF8)
SET DateStyle TO 'SQL, DMY'0A000 (only the ISO output style)
KILL QUERY d'une session inconnue42704 (Unknown thread id)

Écarts de comportement, détaillés dans ce chapitre :

  • les niveaux d'isolement ne relâchent rien : tous lisent un instantané fixe (37.3) ;
  • un conflit (40001) ou un interblocage (40P01) annule toute la transaction ; ROLLBACK TO SAVEPOINT ne la rattrape pas (37.4) ;
  • un écrivain ne s'attend pas derrière un autre : il échoue tout de suite, sauf pour une ligne tenue par FOR UPDATE (37.4, 37.6) ; NOWAIT et un délai dépassé rendent 55P03 ;
  • seules certaines instructions de définition s'annulent avec la transaction ; les autres la valident d'abord (37.1) ;
  • COMMIT, ROLLBACK hors transaction et BEGIN répété ne donnent pas d'avertissement ;
  • LOCK TABLE ne pose de verrou qu'en EXCLUSIVE et ACCESS EXCLUSIVE (37.6) ;
  • set_config appliqué à la fin de l'instruction, SET dans une routine (37.7) ;
  • un DDL refusé par une transaction ouverte rend 55P03 tout de suite, sans attendre (37.4) ;
  • TRUNCATE dans un bloc : annulable et vidé en bloc, table disque et table partitionnée de l'édition Cluster comprises ; seule une table partitionnée dont une partition est tenue par un autre nœud voit ses lignes supprimées une à une, plus lentement (34.6) ;
  • search_path ne retient qu'un schéma (37.8) ;
  • txid_current() rend un numéro positif, le même dans toute la transaction, mais propre à MIRAJ : il n'est ni croissant d'une transaction à l'autre, ni comparable à ceux de PostgreSQL.

La liste condensée de la grammaire est au chapitre 29.

Voir aussi#