Le binding : comment passer des valeurs à une requête préparée
Une requête préparée, c'est du SQL avec des trous dedans. Le binding consiste à remplir ces trous avec des valeurs — proprement, une par une, en passant par l'API du driver plutôt qu'en bricolant des concaténations de chaînes.
Le principe reste toujours le même : on écrit le SQL avec des placeholders, puis on transmet les valeurs à part.
En CLI, on ne peut pas vraiment montrer le binding de paramètres (le shell n'a pas de code applicatif derrière), mais le SQL au-dessus correspond exactement à ce que votre application envoie. Les ? sont des placeholders. Votre driver — sqlite3 en Python, better-sqlite3 en Node, rusqlite en Rust — les remplit via un appel bind séparé.
Le modèle mental est simple : la requête SQL, c'est la recette ; les valeurs bindées, ce sont les ingrédients. Les deux ne se mélangent jamais.
Placeholders positionnels : ?
Le placeholder le plus basique en SQLite, c'est ?. Chaque ? correspond à la prochaine valeur que vous bindez, dans l'ordre.
INSERT INTO users (name, email) VALUES (?, ?);
En Python, ça donne :
cursor.execute(
"INSERT INTO users (name, email) VALUES (?, ?)",
("Rosa", "rosa@example.com"),
)
Le premier ? reçoit "Rosa", le second "rosa@example.com". Si vous passez trop ou trop peu de valeurs, le driver lève une erreur avant même d'exécuter la requête.
Vous pouvez aussi les numéroter explicitement avec ?1, ?2, ?3 — pratique quand la même valeur revient plusieurs fois :
SELECT ?1 AS salutation, ?1 AS toujours_le_meme;
?1 réutilise la première valeur liée. Sans numérotation, il faudrait binder la même valeur deux fois.
Les paramètres nommés : :name
Dès qu'une requête préparée comporte plus de deux ou trois trous à remplir, le binding positionnel devient vite un jeu de devinettes. Les paramètres nommés règlent le problème :
INSERT INTO users (name, email)
VALUES (:name, :email);
En Python :
cursor.execute(
"INSERT INTO users (name, email) VALUES (:name, :email)",
{"name": "Boris", "email": "boris@example.com"},
)
L'ordre des clés dans le dictionnaire n'a aucune importance — seuls les noms comptent. SQLite accepte aussi @name et $name comme préfixes alternatifs, qui fonctionnent exactement de la même façon. Cela dit, :name reste de loin le plus utilisé.
Les paramètres nommés deviennent vraiment intéressants dès que vous écrivez un UPDATE avec cinq colonnes, ou une requête qui réutilise la même valeur dans le WHERE et le RETURNING.
Binder NULL en SQLite
Pour insérer un NULL, la bonne approche consiste à passer la valeur null de votre langage via l'API de binding. Le driver se charge de la conversion :
INSERT INTO users (name, email) VALUES (?, ?);
-- Liaison : ("Cyrus", None) en Python
-- Liaison : ["Cyrus", null] en Node
SELECT id, name, email FROM users;
None, null, nil, peu importe le nom dans votre langage — le pilote le convertit en véritable NULL SQL. Ne bindez surtout pas la chaîne "NULL" : vous stockeriez le texte de quatre caractères "NULL". Et n'injectez pas non plus le mot NULL directement dans le SQL — ça contourne tout l'intérêt du binding.
La règle vaut aussi pour les nombres, les blobs, les dates : passez la valeur native, et laissez le pilote s'occuper du binding.
Réutiliser une requête préparée avec des valeurs différentes
Le binding des paramètres va de pair avec les requêtes préparées (prepared statements). On prépare une fois, puis on bind et on exécute autant de fois que nécessaire. Le parseur fait son boulot une seule fois, et la base réutilise le plan compilé à chaque nouveau jeu de valeurs.
INSERT INTO users (name, email) VALUES (?, ?);
-- Lier ("Ada", "ada@example.com") -> exécuter
-- Lier ("Boris", "boris@example.com") -> exécuter
-- Lier ("Cyrus", NULL) -> exécuter
SELECT id, name, email FROM users ORDER BY id;
La plupart des drivers encapsulent ça dans un executemany (Python) ou une boucle .run() (Node). Dans les deux cas, ce que vous économisez, c'est le coût du parsing — minime pour une seule requête, mais ça devient vite significatif quand vous insérez des milliers de lignes.
Ne mélange pas les styles dans une même requête
Techniquement, SQLite accepte les placeholders positionnels et nommés au sein d'une même requête. Évitez-le.
-- Légal mais piégeux :
INSERT INTO users (name, email) VALUES (?, :email);
Devoir jongler mentalement avec deux API de binding en même temps, c'est pénible — et la plupart des drivers gèrent mal la forme mixte. Tenez-vous-en à un seul style par requête : ? quand il n'y a qu'une ou deux valeurs, :name pour tout le reste.
Piège classique : le binding n'est pas du formatage de chaîne
Tout l'intérêt du binding paramétré, c'est justement que les valeurs ne passent pas par l'analyseur SQL. Comparez ces deux lignes Python :
# Faux — formatage de chaîne :
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")
# Correct — liaison de paramètres :
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))
La première ligne construit la requête SQL par concaténation. Si name vaut "'; DROP TABLE users; --", la base de données interprète et exécute joyeusement l'instruction injectée. La seconde ligne, elle, fait passer le SQL et la valeur par deux canaux distincts : la valeur est liée en tant que chaîne, point final, peu importe les caractères qu'elle contient. Voilà pourquoi tous les tutos vous répètent qu'il faut binder les paramètres : ce n'est pas une question de style, c'est une question de ce que voit le parseur.
On creusera l'aspect injection SQL à la page suivante.
Autre piège : on ne peut pas binder les identifiants
Les placeholders fonctionnent pour les valeurs — chaînes, nombres, blobs, NULL. Mais pas pour les noms de tables, les noms de colonnes ou les mots-clés SQL :
-- Ceci ne fait PAS ce que vous voulez :
SELECT * FROM ? WHERE id = ?;
-- Le premier ? se lie comme une chaîne littérale, pas comme un nom de table.
Si vous avez réellement besoin d'un nom de table ou de colonne dynamique (cas rare dans une application), valide-le contre une liste blanche puis concatène-le vous-même dans le SQL — jamais directement à partir d'une saisie utilisateur. Pour tout le reste, utilise le binding.
Un exemple complet
Mettons tout ça en pratique : une petite table users écrite et lue intégralement via des requêtes paramétrées sqlite :
Dans du vrai code, les INSERT comme le SELECT passeraient tous par des placeholders. Le CLI, lui, n'a pas d'application derrière pour faire le binding : les littéraux jouent donc le rôle de ce que produirait un binding.
La suite : se protéger des injections SQL
Le binding des paramètres, c'est le mécanisme. Pourquoi ça bloque les injections SQL — et les quelques cas où le binding seul ne suffit pas — c'est le sujet de la page suivante.
Questions fréquentes
C'est quoi le binding de paramètres en SQLite ?
Le binding, c'est le fait de passer les valeurs à une requête préparée séparément du texte SQL. Vous mettez un placeholder du style ? ou :name dans la requête, puis vous transmettez la vraie valeur via l'API de bind du driver. SQLite considère les valeurs liées comme de la pure donnée — elles ne sont jamais analysées comme du SQL.
Quelle différence entre ? et :name dans SQLite ?
? est un placeholder positionnel : les valeurs sont liées dans l'ordre où elles apparaissent. :name (ainsi que @name et $name) sont des placeholders nommés : on lie par nom plutôt que par position. Les paramètres nommés sont plus lisibles et bien plus faciles à réorganiser dès qu'on dépasse deux ou trois valeurs.
Comment binder une valeur NULL en SQLite ?
Il suffit de passer la valeur null/None/nil de votre langage à l'API de binding — le driver la convertit automatiquement en NULL SQL. N'écrivez surtout pas la chaîne 'NULL' et n'interpolez jamais le mot NULL dans le texte SQL. Tout l'intérêt du binding, c'est justement de garder les valeurs hors du parser SQL.
Peut-on mélanger paramètres positionnels et nommés dans une même requête ?
SQLite l'autorise techniquement, mais évitez. Une requête qui mêle ? et :name devient vite illisible et propice aux erreurs de binding. Choisissez un seul style par requête — généralement les paramètres nommés dès qu'il y a plus de deux ou trois valeurs.