Menu

Биндинг параметров в SQLite: ?, :name и безопасные значения

Как работает биндинг параметров в SQLite — позиционные плейсхолдеры, именованные параметры и правила, по которым значения безопасно передаются из приложения.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Биндинг — это то, как значения попадают в подготовленное выражение

Подготовленное выражение — это SQL с дырками. Биндинг (binding) — это акт заполнения этих дырок значениями: безопасно, по одному, через API драйвера, а не склейкой строк.

Форма всегда одна и та же: пишете SQL с плейсхолдерами, потом отдельно передаёте значения.

В CLI биндинг толком не показать (под шеллом нет приложения, которое биндило бы значения), но SQL выше — это ровно то, что отправляет ваше приложение. Знаки ? — это плейсхолдеры. Драйвер — sqlite3 в Python, better-sqlite3 в Node, rusqlite в Rust — заполняет их отдельным вызовом bind.

Ментальная модель: SQL — это рецепт, привязанные значения — ингредиенты. Они не пересекаются.

Позиционные плейсхолдеры: ?

Самый простой плейсхолдер — ?. Каждый из них соответствует следующему привязанному значению, по порядку.

INSERT INTO users (name, email) VALUES (?, ?);

В Python это:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (?, ?)",
    ("Rosa", "rosa@example.com"),
)

Первый ? получит "Rosa", второй — "rosa@example.com". Передадите слишком мало или слишком много — драйвер кинет ошибку ещё до выполнения.

Их можно ещё и пронумеровать явно — ?1, ?2, ?3 — что удобно, когда одно и то же значение встречается несколько раз:

SELECT ?1 AS greeting, ?1 AS still_the_same;

?1 переиспользует первое привязанное значение. Без нумерации пришлось бы биндить одно и то же дважды.

Именованные плейсхолдеры: :name

Как только у выражения становится больше двух-трёх дырок, позиционный биндинг превращается в игру в угадайку. Именованные параметры это снимают:

INSERT INTO users (name, email)
VALUES (:name, :email);

В Python:

cursor.execute(
    "INSERT INTO users (name, email) VALUES (:name, :email)",
    {"name": "Boris", "email": "boris@example.com"},
)

Порядок ключей в словаре не важен — важны только имена. SQLite принимает в роли префиксов и @name, и $name — все они работают одинаково. На практике подавляющее большинство кода использует :name.

Именованные параметры окупаются ровно с того момента, когда у вас UPDATE на пять столбцов или один и тот же параметр используется в WHERE и в RETURNING.

Биндинг NULL

Правильно вставлять NULL так: передайте через bind-API родное null-значение вашего языка. Перевод сделает драйвер:

INSERT INTO users (name, email) VALUES (?, ?);
-- Bind: ("Cyrus", None)   в Python
-- Bind: ["Cyrus", null]   в Node

SELECT id, name, email FROM users;

None, null, nil — как бы ваш язык это ни называл, драйвер превратит это в настоящий SQL NULL. Не биндьте строку "NULL" — она положит в столбец четырёхсимвольный текст "NULL". И не подставляйте слово NULL прямо в текст SQL — это сводит весь биндинг на нет.

То же правило работает с числами, BLOB-ами, датами: передавайте родное значение, дайте драйверу сделать привязку.

Переиспользование выражения с разными значениями

Биндинг естественным образом сочетается с подготовленными выражениями. Подготовили один раз, биндите-выполняете много раз. Парсер делает свою работу один раз, и база переиспользует скомпилированный план для каждого набора значений.

INSERT INTO users (name, email) VALUES (?, ?);
-- Bind ("Ada",   "ada@example.com")    -> execute
-- Bind ("Boris", "boris@example.com")  -> execute
-- Bind ("Cyrus", NULL)                 -> execute

SELECT id, name, email FROM users ORDER BY id;

Большинство драйверов оборачивают это в executemany (Python) или цикл .run() (Node). Так или иначе экономия идёт на парсинге — на одно выражение это копейки, но на тысячах строк уже заметно.

Не смешивайте стили в одном выражении

Технически SQLite разрешает позиционные и именованные плейсхолдеры в одном выражении. Не поддавайтесь.

-- Легально, но граната:
INSERT INTO users (name, email) VALUES (?, :email);

Читателю придётся одновременно держать в голове два API биндинга, и большинство драйверов криво поддерживают такую смесь. Берите один стиль на запрос: ? — на одно-два значения, :name — на всё остальное.

Типичная ловушка: биндинг — это не форматирование строк

Вся суть биндинга в том, что значения не идут через парсер SQL. Сравните две строки в Python:

# Неправильно — форматирование строк:
cursor.execute(f"SELECT * FROM users WHERE name = '{name}'")

# Правильно — биндинг параметров:
cursor.execute("SELECT * FROM users WHERE name = ?", (name,))

Первая строка собирает SQL конкатенацией. Если в name лежит "'; DROP TABLE users; --", база радостно разберёт и выполнит вшитую инъекцию. Вторая строка передаёт SQL и значение по разным каналам — значение биндится как строка целиком, и какие бы символы в нём ни были, для парсера это одна строка. Поэтому в каждом гайде вам и говорят: биндите. Это не вопрос стиля — это вопрос того, что видит парсер.

В тему инъекций глубже погрузимся на следующей странице.

Ещё одна ловушка: идентификаторы биндить нельзя

Плейсхолдеры работают для значений — строк, чисел, BLOB-ов, NULL-ов. Они не работают для имён таблиц, имён столбцов и SQL-ключевых слов:

-- Это НЕ делает того, что вы хотите:
SELECT * FROM ? WHERE id = ?;
-- Первый ? привяжется как строковый литерал, а не как имя таблицы.

Если действительно нужно динамическое имя таблицы или столбца (в коде приложения это редкость), проверьте его по белому списку и подставьте в SQL вручную — ни в коем случае не напрямую из пользовательского ввода. Для всего остального — биндьте.

Сквозной пример

Соберём всё вместе — небольшая таблица users, которую мы записываем и читаем целиком через биндинг:

В реальном коде и INSERT, и SELECT использовали бы плейсхолдеры. Просто в CLI нет приложения, которое могло бы биндить, поэтому литералы стоят на месте того, что обычно даёт биндинг.

Дальше: защита от SQL-инъекций

Биндинг параметров — это механизм. Почему он останавливает SQL-инъекции — и в каких немногих местах одного биндинга мало — об этом следующая страница.

Часто задаваемые вопросы

Что такое биндинг параметров в SQLite?

Биндинг параметров — это способ передать значения в подготовленное выражение отдельно от текста SQL. В SQL вы пишете плейсхолдер вроде ? или :name, а само значение отдаёте через bind-API драйвера. Привязанные значения SQLite трактует чисто как данные — они никогда не парсятся как SQL.

В чём разница между ? и :name в SQLite?

? — позиционный плейсхолдер: значения подставляются в порядке, в котором появляются. :name (а также @name, $name) — именованные плейсхолдеры: вы биндите по имени, а не по позиции. Именованные удобнее читаются и легче переставляются, когда параметров больше двух-трёх.

Как привязать NULL в SQLite?

Передайте через bind-API родное null/None/nil вашего языка — драйвер сам переведёт это в SQL NULL. Никогда не пишите строку 'NULL' и не подставляйте слово NULL прямо в текст SQL: смысл биндинга в том, чтобы значения вообще не доходили до парсера SQL.

Можно ли смешивать позиционные и именованные параметры в одном выражении?

SQLite это позволяет, но не делайте так. Выражение, в котором смешаны ? и :name, тяжело читать и легко привязать неправильно. Берите один стиль на запрос — обычно именованные, как только параметров больше двух-трёх.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ