Двадцать три урока мы разговаривали с базой «Пенки» через утилиту 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
Разберём по шагам:
sqlite3.connectоткрывает файл базы, а если его нет, создаёт пустой. Получаем соединение.con.executeвыполняет запрос и возвращает курсор.- Знак
?в тексте запроса это параметр. Значение"кофе"передаётся отдельным кортежем. 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 для аналитика учит отвечать запросами на вопросы бизнеса.
домашка