Раздел 35 · SQLite с нуля

Особенности SQLite и работа из кода

junior~25 мин

без входаВойди, чтобы отмечать прогресс, вести заметки и решать задачи в редакторе. войти

Двадцать три урока мы разговаривали с базой «Пенки» через утилиту sqlite3. Но гости кофейни не открывают терминал: заказы оформляет программа на кассе, а бонусы считает мобильное приложение. В первом уроке мы говорили, что SQLite живёт внутри программы как библиотека. Сегодня последний урок раздела: собираем особенности SQLite в одну памятку и подключаемся к базе из Python и Node.js.

Памятка: чем SQLite не похож на остальных

Если после SQLite ты сядешь за PostgreSQL или MySQL, вот что окажется непривычным. Каждый пункт мы уже встречали.

Тип у значения, а не у столбца. В столбце volume у «Пенки» лежат целые, текст и дробь, и SQLite не возражает. Это динамическая типизация:

sqlite> SELECT name, volume, typeof(volume) FROM drinks WHERE id IN (5, 6, 9);
╭────────┬────────┬────────────────╮
│  name  │ volume │ typeof(volume) │
╞════════╪════════╪════════════════╡
│ Раф    │    350 │ integer        │
│ Какао  │ 300 мл │ text           │
│ Тархун │    0.5 │ real           │
╰────────┴────────┴────────────────╯

Строгую проверку типов включают ключевым словом STRICT в конце CREATE TABLE.

Двойные кавычки для имён, одинарные для строк. По стандарту SQL в двойных кавычках пишут имена столбцов и таблиц. Старые версии SQLite, если столбца с таким именем нет, тихо считали двойные кавычки строкой. Утилита sqlite3 так больше не делает:

sqlite> SELECT name FROM drinks WHERE name = "Латте";
Parse error near line 1: no such column: "Латте" - should this be a string literal in single-quotes?
  SELECT name FROM drinks WHERE name = "Латте";
                         error here ---^

А вот библиотека, которую встроили в программу, может вести себя по-старому: это зависит от того, как её собрали. Например, модуль sqlite3 из Python 3.13 с SQLite 3.51.2 на том же запросе молча вернёт латте, а node:sqlite откажется с той же ошибкой. Пиши строки в одинарных кавычках, и вопрос не возникнет.

Внешние ключи выключены. Каждое новое соединение начинает с выключенной проверкой, как мы видели в уроке 12:

sqlite> PRAGMA foreign_keys;
╭──────────────╮
│ foreign_keys │
╞══════════════╡
│            0 │
╰──────────────╯

Программа должна выполнить PRAGMA foreign_keys = ON сразу после подключения, каждый раз.

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

sqlite> CREATE TABLE tags (code TEXT PRIMARY KEY, title TEXT);
sqlite> INSERT INTO tags VALUES (NULL, 'первый'), (NULL, 'второй');
sqlite> SELECT rowid, code, title FROM tags;
╭───────┬──────┬────────╮
│ rowid │ code │ title  │
╞═══════╪══════╪════════╡
│     1 │ NULL │ первый │
│     2 │ NULL │ второй │
╰───────┴──────┴────────╯

Лечится явным NOT NULL у такого ключа или таблицей STRICT.

Остальное коротко:

  • отдельного типа для дат нет, даты хранят текстом ISO 8601 и считают функциями date и strftime (урок 19);
  • ALTER TABLE умеет немного, сложные изменения схемы делают пересборкой таблицы (урок 11);
  • пишет в файл одно соединение за раз (урок 23);
  • пользователей и прав нет, доступ к данным равен доступу к файлу.

Модуль sqlite3 в Python

В Python работа с SQLite встроена в стандартную библиотеку, ставить ничего не нужно. Положим файл penka.db с базой «Пенки» рядом с программой и спросим кофейные напитки:

import sqlite3

con = sqlite3.connect("penka.db")
cur = con.execute(
    "SELECT name, price FROM drinks WHERE category = ? ORDER BY price",
    ("кофе",),
)
for name, price in cur.fetchall():
    print(name, price)
con.close()
$ python3 drinks.py
Эспрессо 120
Американо 150
Капучино 190
Латте 210
Раф 240

Разберём по шагам:

  1. sqlite3.connect открывает файл базы, а если его нет, создаёт пустой. Получаем соединение.
  2. con.execute выполняет запрос и возвращает курсор.
  3. Знак ? в тексте запроса это параметр. Значение "кофе" передаётся отдельным кортежем.
  4. fetchall отдаёт строки списком кортежей.

Транзакция из кода

Оформим заказ для Лены: строка в orders и две позиции в order_items. Всё должно пройти вместе, поэтому используем блок with con:. Он сам делает COMMIT, если внутри всё прошло, и ROLLBACK, если вылетела ошибка. Параметры здесь именованные, через двоеточие. Во второй позиции мы нарочно ошиблись и повторили напиток:

import sqlite3

con = sqlite3.connect("penka.db")
try:
    with con:
        cur = con.execute(
            "INSERT INTO orders (customer_id, staff_id, ordered_at, paid)"
            " VALUES (:guest, :barista, :at, 'карта')",
            {"guest": 3, "barista": 4, "at": "2026-10-07 09:30:00"},
        )
        order_id = cur.lastrowid
        con.execute("INSERT INTO order_items VALUES (?, ?, ?)", (order_id, 3, 1))
        con.execute("INSERT INTO order_items VALUES (?, ?, ?)", (order_id, 3, 2))
except sqlite3.IntegrityError as error:
    print("заказ отменён:", error)

count = con.execute("SELECT count(*) FROM orders").fetchone()[0]
print("заказов в базе:", count)
con.close()
$ python3 order.py
заказ отменён: UNIQUE constraint failed: order_items.order_id, order_items.drink_id
заказов в базе: 12

Вторая позиция нарушила составной первичный ключ, и откатилась вся транзакция, вместе со строкой в orders. Заказов осталось двенадцать, никакого заказа-призрака без напитков. Именно этот явный откат после ошибки мы делали руками в прошлом уроке. Если во второй позиции поставить напиток 5, программа напечатает заказов в базе: 13. Свойство cur.lastrowid отдаёт rowid только что вставленной строки, то есть номер нового заказа.

SQL-инъекция

Соблазн велик: зачем ?, если можно вклеить значение в строку запроса? Посмотри, что будет, если «имя гостя» пришло из поля ввода и злоумышленник написал туда кусок SQL:

import sqlite3

con = sqlite3.connect("penka.db")
name = "x' OR '1'='1"

unsafe = f"SELECT name, phone FROM customers WHERE name = '{name}'"
print("склейка:", len(con.execute(unsafe).fetchall()), "строк")

safe = con.execute("SELECT name, phone FROM customers WHERE name = ?", (name,))
print("параметр:", len(safe.fetchall()), "строк")
con.close()
$ python3 search.py
склейка: 6 строк
параметр: 0 строк

После склейки база получила вот такой запрос, и условие '1'='1' истинно для каждой строки:

sqlite> SELECT name, phone FROM customers WHERE name = 'x' OR '1'='1';
╭────────┬──────────────────╮
│  name  │      phone       │
╞════════╪══════════════════╡
│ Маша   │ +7 900 111-22-33 │
│ Пётр   │ NULL             │
│ Лена   │ +7 900 444-55-66 │
│ Дима   │ +7 900 777-88-99 │
│ Света  │ NULL             │
│ Кирилл │ +7 900 000-11-22 │
╰────────┴──────────────────╯

Злоумышленник вытащил телефоны всех гостей. Это SQL-инъекция: уязвимости больше тридцати лет, а её до сих пор находят в живых программах. С параметром тот же текст стал обычной строкой для сравнения, и гостя с таким странным именем не нашлось. Правило без исключений: значения от пользователя попадают в запрос только через параметры.

node:sqlite в Node.js

В Node.js 22.5 появился встроенный модуль node:sqlite. Класс DatabaseSync работает синхронно, как и сам SQLite:

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync('penka.db');
const byCategory = db.prepare('SELECT name, price FROM drinks WHERE category = ? ORDER BY price');
for (const row of byCategory.all('чай')) {
  console.log(row.name, row.price);
}
const guest = db.prepare('SELECT name, bonus FROM customers WHERE id = :id').get({ id: 1 });
console.log(guest);
const result = db.prepare('UPDATE customers SET bonus = bonus + 5 WHERE id = ?').run(1);
console.log(result);
db.close();
$ node drinks.mjs
Матча-латте null
Чай с чабрецом 130
[Object: null prototype] { name: 'Маша', bonus: 120 }
{ changes: 1, lastInsertRowid: 0 }

prepare готовит запрос один раз, а потом его можно выполнять с разными параметрами. all отдаёт все строки массивом объектов, get только первую, run выполняет команду без результата и сообщает, сколько строк изменилось. NULL из базы приходит как null, и при сортировке по возрастанию матча-латте без цены оказалась первой: так работает NULL в ORDER BY, как в уроке 8. Вывод снят на Node.js 24.

Попробуй сам

В песочнице видна SQL-сторона программ: те запросы, которые они отправляют в базу. Сравни запрос с параметром, подставленным правильно, и склеенный запрос злоумышленника:

Второй запрос показывает, во что превращается значение с параметром: кавычки внутри него удвоены, и всё это одна строка. Третий это склейка, и он возвращает всех гостей.

Проверь себя

Задание

Дальше

Раздел закончен: ты умеешь создавать базу, писать запросы, связывать таблицы, считать агрегаты, ускорять поиск индексами и работать с SQLite из кода. Следующий шаг это серверная база. В разделе про PostgreSQL тот же SQL работает на сервере с сотнями одновременных клиентов, а раздел SQL для аналитика учит отвечать запросами на вопросы бизнеса.

домашка

Домашка