Modifications de données et transactions#
Les requêtes SELECT lisent la base sans la modifier. Les commandes INSERT, UPDATE et DELETE constituent le langage de manipulation des données (DML). Regroupées dans des transactions, elles permettent de garantir la cohérence de la base même en cas d’erreur ou de panne.
conn = sqlite3.connect(":memory:")
conn.execute("PRAGMA journal_mode=WAL")
conn.executescript("""
CREATE TABLE clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
nom TEXT NOT NULL,
email TEXT UNIQUE,
solde REAL NOT NULL DEFAULT 0.0
);
CREATE TABLE comptes (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_id INTEGER REFERENCES clients(id),
type TEXT NOT NULL, -- 'courant' ou 'epargne'
solde REAL NOT NULL DEFAULT 0.0
);
CREATE TABLE transactions_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
compte_src INTEGER,
compte_dst INTEGER,
montant REAL,
statut TEXT,
horodatage TEXT DEFAULT (datetime('now'))
);
INSERT INTO clients(nom, email, solde) VALUES
('Alice Martin', 'alice@example.com', 0),
('Bob Dupont', 'bob@example.com', 0),
('Carla Petit', 'carla@example.com', 0);
INSERT INTO comptes(client_id, type, solde) VALUES
(1, 'courant', 5000),
(1, 'epargne', 12000),
(2, 'courant', 3200),
(3, 'courant', 800);
""")
pd.read_sql("SELECT * FROM comptes", conn)
| id | client_id | type | solde | |
|---|---|---|---|---|
| 0 | 1 | 1 | courant | 5000.0 |
| 1 | 2 | 1 | epargne | 12000.0 |
| 2 | 3 | 2 | courant | 3200.0 |
| 3 | 4 | 3 | courant | 800.0 |
INSERT#
Définition 77
La commande INSERT INTO ajoute une ou plusieurs lignes dans une table. Trois variantes principales :
INSERT INTO t VALUES (...)— ligne unique avec toutes les colonnes.INSERT INTO t (col1, col2) VALUES (...), (...)— lignes multiples, colonnes explicites.INSERT INTO t SELECT ...— insertion à partir d’une requête.
# INSERT ligne unique
conn.execute("INSERT INTO clients(nom, email, solde) VALUES ('David Noir', 'david@example.com', 0)")
# INSERT multiple
conn.executemany(
"INSERT INTO comptes(client_id, type, solde) VALUES (?,?,?)",
[(4, 'courant', 2500), (4, 'epargne', 7000)]
)
conn.commit()
pd.read_sql("SELECT * FROM clients", conn)
| id | nom | solde | ||
|---|---|---|---|---|
| 0 | 1 | Alice Martin | alice@example.com | 0.0 |
| 1 | 2 | Bob Dupont | bob@example.com | 0.0 |
| 2 | 3 | Carla Petit | carla@example.com | 0.0 |
| 3 | 4 | David Noir | david@example.com | 0.0 |
# INSERT ... SELECT : créer une table de sauvegarde depuis une requête
conn.execute("""
CREATE TABLE IF NOT EXISTS comptes_archive AS
SELECT *, datetime('now') AS archive_date
FROM comptes WHERE solde = 0
""")
# INSERT INTO table existante depuis SELECT
conn.execute("""
INSERT INTO transactions_log(compte_src, compte_dst, montant, statut)
SELECT NULL, id, solde, 'initialisation'
FROM comptes
""")
conn.commit()
pd.read_sql("SELECT * FROM transactions_log", conn)
| id | compte_src | compte_dst | montant | statut | horodatage | |
|---|---|---|---|---|---|---|
| 0 | 1 | None | 1 | 5000.0 | initialisation | 2026-03-31 22:46:59 |
| 1 | 2 | None | 2 | 12000.0 | initialisation | 2026-03-31 22:46:59 |
| 2 | 3 | None | 3 | 3200.0 | initialisation | 2026-03-31 22:46:59 |
| 3 | 4 | None | 4 | 800.0 | initialisation | 2026-03-31 22:46:59 |
| 4 | 5 | None | 5 | 2500.0 | initialisation | 2026-03-31 22:46:59 |
| 5 | 6 | None | 6 | 7000.0 | initialisation | 2026-03-31 22:46:59 |
Remarque 38
En SQLite, INSERT OR REPLACE INTO combine un INSERT avec un remplacement automatique de la ligne existante si la contrainte d’unicité est violée. Cela équivaut à un DELETE + INSERT. À ne pas confondre avec INSERT OR IGNORE qui ignore silencieusement les doublons.
# INSERT OR REPLACE : si l'email existe déjà, la ligne est remplacée
conn.execute("""
INSERT OR REPLACE INTO clients(nom, email, solde)
VALUES ('Alice Martin-Leblanc', 'alice@example.com', 0)
""")
conn.commit()
pd.read_sql("SELECT * FROM clients WHERE email='alice@example.com'", conn)
| id | nom | solde | ||
|---|---|---|---|---|
| 0 | 5 | Alice Martin-Leblanc | alice@example.com | 0.0 |
UPDATE#
Définition 78
La commande UPDATE modifie les valeurs d’une ou plusieurs colonnes pour les lignes qui satisfont la clause WHERE. Sans WHERE, toutes les lignes de la table sont modifiées — une erreur classique et souvent catastrophique.
Remarque 39
Bonne pratique : avant d’exécuter un UPDATE, formuler d’abord le SELECT correspondant pour vérifier les lignes ciblées. Cette habitude prévient les mises à jour accidentelles sur l’ensemble de la table.
# UPDATE simple : augmentation de 5% du solde du compte épargne d'Alice
conn.execute("""
UPDATE comptes
SET solde = solde * 1.05
WHERE type = 'epargne' AND client_id = (
SELECT id FROM clients WHERE email = 'alice@example.com'
)
""")
conn.commit()
pd.read_sql("""
SELECT cl.nom, co.type, co.solde
FROM comptes co
JOIN clients cl ON co.client_id = cl.id
ORDER BY cl.nom, co.type
""", conn)
| nom | type | solde | |
|---|---|---|---|
| 0 | Bob Dupont | courant | 3200.0 |
| 1 | Carla Petit | courant | 800.0 |
| 2 | David Noir | courant | 2500.0 |
| 3 | David Noir | epargne | 7000.0 |
Exemple 29
UPDATE avec sous-requête : mettre à jour le solde agrégé d’un client à partir de la somme de ses comptes.
conn.execute("""
UPDATE clients
SET solde = (
SELECT COALESCE(SUM(solde), 0)
FROM comptes
WHERE comptes.client_id = clients.id
)
""")
conn.commit()
pd.read_sql("SELECT nom, solde FROM clients ORDER BY nom", conn)
| nom | solde | |
|---|---|---|
| 0 | Alice Martin-Leblanc | 0.0 |
| 1 | Bob Dupont | 3200.0 |
| 2 | Carla Petit | 800.0 |
| 3 | David Noir | 9500.0 |
DELETE#
Définition 79
La commande DELETE FROM supprime les lignes satisfaisant la clause WHERE. Sans WHERE, elle vide la table entière (mais conserve la structure, contrairement à DROP TABLE). En SQLite, TRUNCATE TABLE n’existe pas ; on utilise DELETE FROM table sans condition.
# DELETE ciblé : supprimer les logs d'initialisation
conn.execute("DELETE FROM transactions_log WHERE statut = 'initialisation'")
conn.commit()
print("Lignes restantes dans transactions_log :",
pd.read_sql("SELECT COUNT(*) AS n FROM transactions_log", conn).iloc[0,0])
Lignes restantes dans transactions_log : 0
Remarque 40
Si des contraintes de clé étrangère avec ON DELETE CASCADE sont définies, la suppression d’une ligne parente déclenche automatiquement la suppression des lignes enfants associées. En SQLite, les contraintes de clé étrangère doivent être activées explicitement avec PRAGMA foreign_keys = ON.
UPSERT — INSERT … ON CONFLICT#
Définition 80
Un UPSERT (update or insert) insère une ligne si elle n’existe pas encore, ou la met à jour si une contrainte d’unicité est violée.
SQLite :
INSERT OR REPLACEouINSERT ... ON CONFLICT(col) DO UPDATE SET ...PostgreSQL :
INSERT ... ON CONFLICT (col) DO UPDATE SET ...(syntaxe standard moderne)
# UPSERT SQLite : ON CONFLICT DO UPDATE (syntaxe moderne >= SQLite 3.24)
conn.execute("""
INSERT INTO clients(nom, email, solde)
VALUES ('Eva Blanc', 'eva@example.com', 1500)
ON CONFLICT(email)
DO UPDATE SET nom = excluded.nom, solde = excluded.solde
""")
conn.commit()
# Deuxième appel avec la même clé : déclenche le DO UPDATE
conn.execute("""
INSERT INTO clients(nom, email, solde)
VALUES ('Eva Blanc-Dupont', 'eva@example.com', 2000)
ON CONFLICT(email)
DO UPDATE SET nom = excluded.nom, solde = excluded.solde
""")
conn.commit()
pd.read_sql("SELECT * FROM clients WHERE email='eva@example.com'", conn)
| id | nom | solde | ||
|---|---|---|---|---|
| 0 | 6 | Eva Blanc-Dupont | eva@example.com | 2000.0 |
Transactions — BEGIN, COMMIT, ROLLBACK#
Définition 81
Une transaction est une séquence d’opérations DML traitée comme une unité indivisible. En SQL :
BEGIN(ouBEGIN TRANSACTION) démarre la transaction.COMMITvalide définitivement toutes les modifications.ROLLBACKannule toutes les modifications depuis le dernierBEGIN.
Théorème 4
Les propriétés ACID garantissent la fiabilité des transactions :
Atomicité : la transaction réussit entièrement ou échoue entièrement — pas d’état intermédiaire.
Cohérence : la base passe d’un état valide à un autre état valide ; toutes les contraintes sont respectées.
Isolation : les transactions concurrentes ne s’interfèrent pas ; chacune voit un état cohérent.
Durabilité : une fois validée (
COMMIT), une transaction survit à une panne système.
def virement(conn, compte_src_id, compte_dst_id, montant):
"""
Effectue un virement entre deux comptes.
La transaction garantit l'atomicité : soit les deux mises à jour
réussissent, soit aucune n'est appliquée.
"""
try:
# SQLite en Python : isolation_level=None pour contrôle manuel
conn.execute("BEGIN")
# 1. Vérifier le solde suffisant
solde_src = conn.execute(
"SELECT solde FROM comptes WHERE id = ?", (compte_src_id,)
).fetchone()[0]
if solde_src < montant:
raise ValueError(f"Solde insuffisant : {solde_src} < {montant}")
# 2. Débiter le compte source
conn.execute(
"UPDATE comptes SET solde = solde - ? WHERE id = ?",
(montant, compte_src_id)
)
# 3. Créditer le compte destination
conn.execute(
"UPDATE comptes SET solde = solde + ? WHERE id = ?",
(montant, compte_dst_id)
)
# 4. Logger la transaction
conn.execute(
"INSERT INTO transactions_log(compte_src, compte_dst, montant, statut) VALUES (?,?,?,'OK')",
(compte_src_id, compte_dst_id, montant)
)
conn.execute("COMMIT")
print(f"Virement de {montant} € de compte {compte_src_id} vers {compte_dst_id} : OK")
except Exception as e:
conn.execute("ROLLBACK")
conn.execute(
"INSERT INTO transactions_log(compte_src, compte_dst, montant, statut) VALUES (?,?,?,?)",
(compte_src_id, compte_dst_id, montant, f"ECHEC: {e}")
)
conn.commit()
print(f"Virement annulé (ROLLBACK) : {e}")
# Virement réussi : compte 1 (Alice courant) → compte 3 (Bob courant)
virement(conn, 1, 3, 1000)
# Virement échoué : compte 4 (Bob courant) n'a que 3200 + 1000 - tentative de 5000
virement(conn, 3, 2, 9999)
pd.read_sql("""
SELECT co.id, cl.nom, co.type, co.solde
FROM comptes co
JOIN clients cl ON co.client_id = cl.id
ORDER BY cl.nom, co.type
""", conn)
Virement de 1000 € de compte 1 vers 3 : OK
Virement annulé (ROLLBACK) : Solde insuffisant : 4200.0 < 9999
| id | nom | type | solde | |
|---|---|---|---|---|
| 0 | 3 | Bob Dupont | courant | 4200.0 |
| 1 | 4 | Carla Petit | courant | 800.0 |
| 2 | 5 | David Noir | courant | 2500.0 |
| 3 | 6 | David Noir | epargne | 7000.0 |
pd.read_sql("SELECT * FROM transactions_log ORDER BY id", conn)
| id | compte_src | compte_dst | montant | statut | horodatage | |
|---|---|---|---|---|---|---|
| 0 | 7 | 1 | 3 | 1000.0 | OK | 2026-03-31 22:46:59 |
| 1 | 8 | 3 | 2 | 9999.0 | ECHEC: Solde insuffisant : 4200.0 < 9999 | 2026-03-31 22:46:59 |
Niveaux d’isolation#
Définition 82
Les niveaux d’isolation contrôlent ce qu’une transaction peut voir des modifications faites par les transactions concurrentes. Du moins restrictif au plus restrictif :
READ UNCOMMITTED : peut lire des données non encore validées (dirty reads).
READ COMMITTED : ne lit que les données validées ; peut voir des modifications entre deux lectures (non-repeatable reads).
REPEATABLE READ : garantit des lectures cohérentes au sein d’une transaction ; peut subir des phantom reads.
SERIALIZABLE : isolation totale — équivalent à une exécution séquentielle des transactions.
Remarque 41
SQLite implémente l’isolation de niveau SERIALIZABLE par défaut via un système de verrous. En mode WAL (Write-Ahead Logging), les lecteurs ne bloquent pas les écrivains, offrant un bon compromis performance/cohérence. PostgreSQL utilise READ COMMITTED par défaut.
Exemple 30
Un dirty read se produit quand une transaction T2 lit une valeur modifiée par T1 avant que T1 n’ait validé. Si T1 fait un ROLLBACK, T2 a lu une donnée qui n’a jamais existé officiellement dans la base.
Savepoints#
Définition 83
Un savepoint est un point de sauvegarde nommé à l’intérieur d’une transaction. Il permet un ROLLBACK TO SAVEPOINT nom partiel, sans annuler toute la transaction. Syntaxe : SAVEPOINT nom_point, ROLLBACK TO SAVEPOINT nom_point, RELEASE SAVEPOINT nom_point.
# Démonstration de SAVEPOINT
conn.execute("BEGIN")
conn.execute("UPDATE comptes SET solde = solde + 100 WHERE id = 1")
conn.execute("SAVEPOINT avant_erreur")
conn.execute("UPDATE comptes SET solde = solde - 999999 WHERE id = 2") # Erreur volontaire
# On revient au savepoint sans annuler la première opération
conn.execute("ROLLBACK TO SAVEPOINT avant_erreur")
conn.execute("RELEASE SAVEPOINT avant_erreur")
conn.execute("COMMIT")
pd.read_sql("""
SELECT co.id, cl.nom, co.type, co.solde
FROM comptes co
JOIN clients cl ON co.client_id = cl.id
WHERE co.id IN (1,2)
ORDER BY co.id
""", conn)
| id | nom | type | solde |
|---|
Visualisation — timeline d’une transaction#
Résumé#
Définition 84
Récapitulatif des commandes DML et des transactions :
INSERT INTOajoute des lignes (simple, multiple, depuis SELECT, UPSERT).UPDATE SET ... WHEREmodifie des lignes existantes — toujours vérifier leWHERE.DELETE FROM ... WHEREsupprime des lignes — sansWHERE, vide la table.INSERT OR REPLACE/ON CONFLICT DO UPDATE: UPSERT SQLite.BEGIN/COMMIT/ROLLBACKencadrent une transaction atomique.Les propriétés ACID garantissent cohérence et fiabilité.
Les niveaux d’isolation contrôlent la visibilité inter-transactions (READ COMMITTED, SERIALIZABLE…).
SAVEPOINTpermet des retours arrière partiels au sein d’une transaction.
Commande |
Action |
Risque sans WHERE |
|---|---|---|
|
Ajoute des lignes |
N/A |
|
Modifie des lignes |
Modifie TOUTES les lignes |
|
Supprime des lignes |
Vide la table entière |
|
Annule la transaction courante |
N/A |