Базы данных

От папок с CSV к общей базе группы: SQL на SQLite, индексы и планы, транзакции, Python и PostgreSQL, эксплуатация

Вячеслав Федоров

Лекция 13

Разработка и применение ПО

в физических исследованиях

SQL

Содержание

От папок с файлами к общей базе группы

01

Зачем СУБД

02

Модель и SQL

03

Индексы и планы

04

Транзакции

05

Python и базы

06

Эксплуатация и выбор

Цель лекции

Перейти от папок с файлами к базе данных, которой можно доверять

Тот, кто понимает модель данных, индексы и транзакции, проектирует схему журнала измерений, пишет запросы, которые отвечают за миллисекунды, не теряет записанное при сбоях и одновременной работе, восстанавливает базу из резервной копии и выбирает СУБД по задаче, а не по моде.

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

журнал измерений

experiments

кампания: id, name,
setup, started_at

runs

заход: id, experiment_id,
run_number, energy_mev,
operator, started_at

measurements

точка: id, run_id,
channel, t, value

1

N

1

N

2 кампании, 41 заход (40 с измерениями), 5 каналов по 5000 точек, 100 Гц: 1 000 000 измерений

Для физика

В синтетическом журнале сигнал BPM задан пропорциональным энергии пучка: 0,02 на МэВ с шумом.

Пояснение

Тот же журнал проходит через SQLite, PostgreSQL, SQLAlchemy и pandas.

Зачем СУБД

Пределы файлов, OLTP, OLAP и временные ряды, различия систем, встраиваемая и серверная архитектура

01

Зачем СУБД

Модель и SQL

Индексы и планы

Транзакции

Python и базы

Эксплуатация и выбор

Файлы

и СУБД

01

Одновременный доступ

скрипт дописывает файл, анализ читает недописанную строку

02

Целостность

в столбце температуры «error», ссылка на удалённый заход

03

Поиск

ради одного канала одного захода читаются все файлы

04

Восстановление

сбой посреди записи оставляет файл испорченным

ЗадачаСредство СУБД
одновременный доступтранзакции, блокировки, версии строк
целостностьтипы, ключи, NOT NULL, UNIQUE
поискязык SQL и индексы
восстановлениежурнал WAL и резервные копии

Для физика

Папка 2026-08-20/ с двадцатью файлами и result_final_v2.csv — знакомая история данных установки.

OLTP, OLAP

и временные ряды

НагрузкаЗапросыПримерТипичные СУБД
OLTPмного коротких транзакций, по нескольку строкзапись захода, оплата в магазинеPostgreSQL, MySQL, SQLite
OLAPредкие тяжёлые запросы по миллиардам строкстатистика архива пучка за сезонClickHouse, DuckDB
временные рядыпоток точек «время — значение», агрегаты по окнамвакуум, температуры, токи магнитовInfluxDB, TimescaleDB

Пояснение

Нагрузка задаёт устройство системы: строки или столбцы на диске, транзакции, способ вставки.

Для физика

Журнал запусков — OLTP, анализ архива за сезон — OLAP, медленный контроль ускорителя — временные ряды.

Различия

СУБД

SQLitePostgreSQLClickHouseValkey, Redis
Объёмфайл до сотен ГБТБ на серверПБ на кластереобъём памяти
Нагрузкавстроенная OLTPOLTPOLAPключ — значение
ЯзыкSQLSQLдиалект SQLкоманды GET, SET, SCAN
ТранзакцииACID, сериализуемыеACID, уровни изоляцииатомарная вставка блокаMULTI/EXEC без отката
Репликациянет встроеннойфизическая и логическаятаблицы Replicated, Keeperасинхронная на реплики

Пояснение

SQLite не заменит кластер, а кластер ради сотни байт не разворачивают: систему выбирают по нагрузке и объёму.

Встраиваемая

и клиент-серверная

SQLite — библиотека

процесс python3 daq.py

код программы
и модуль sqlite3

lab.db, lab.db-wal

файлы на локальном диске

PostgreSQL — сервер, процесс на соединение

psql

сервис FastAPI

скрипт анализа

процесс 1

процесс 2

процесс 3

общая память

буферы
страниц,
блокировки,
снимки

файлы таблиц и журнал WAL

Пояснение

Новое соединение с PostgreSQL — новый процесс; пул соединений, например PgBouncer, переиспользует их.

Для физика

SQLite — база одного исследователя и его скриптов; PostgreSQL — общая база группы с правами доступа.

Модель и SQL

Таблицы и ключи, нормализация, CREATE TABLE, выборка, соединение и агрегация на SQLite

02

Зачем СУБД

Модель и SQL

Индексы и планы

Транзакции

Python и базы

Эксплуатация и выбор

Реляционная

модель

Таблица runs: строка — заход, столбец — атрибут

idexperiment_idrun_numberenergy_mev
1115,0
2125,5
3123120,0
4124125,0

Пояснение

Столбец id — первичный ключ, его выдаёт база; experiment_id — внешний ключ на experiments.id.

01

Таблица

отношение: строки — объекты, столбцы — атрибуты с типом

02

Первичный ключ

однозначно задаёт строку; обычно суррогатный id

03

Внешний ключ

ссылка на ключ другой таблицы, которую проверяет СУБД

04

Многие ко многим

промежуточная таблица с двумя внешними ключами

Нормализация

Одна широкая таблица: кампания, энергия и оператор в каждой точке

runкампанияEоператорtvalue
31Ресурсные испытания20,0Петрова0,000,406
31Ресурсные испытания20,0Петрова0,010,403
31Ресурсные испытания20,0Петрова0,020,402

Важно

Опечатка в фамилии оператора — правка миллиона строк, а сбой на середине оставляет противоречие.

01

1НФ

атомарные значения, без повторяющихся групп

02

2НФ

неключевые столбцы зависят от всего ключа

03

3НФ

и только от ключа: энергия — свойство захода, а не точки

Пояснение

Денормализация — сознательное дублирование ради быстрой аналитики; начинают с нормализованной схемы.

CREATE TABLE

и ограничения

CREATE TABLE runs (
    id            INTEGER PRIMARY KEY,
    experiment_id INTEGER NOT NULL
                  REFERENCES experiments(id),
    run_number    INTEGER NOT NULL,
    energy_mev    REAL NOT NULL,
    operator      TEXT,
    started_at    TEXT NOT NULL,
    UNIQUE (experiment_id, run_number)
);
 
PRAGMA foreign_keys = ON;  -- в каждом соединении

01

PRIMARY KEY

уникален; INTEGER PRIMARY KEY — номер строки, ключ другого типа в SQLite допускает NULL

02

NOT NULL, UNIQUE

пропуск или повтор — ошибка записи

03

REFERENCES

внешний ключ; SQLite проверяет его после PRAGMA

04

STRICT

строгая проверка типов в SQLite с версии 3.37

Важно

Без PRAGMA foreign_keys = ON ссылка на несуществующую кампанию проходит без ошибки.

SELECT

и JOIN

SELECT run_number, energy_mev
FROM runs
WHERE energy_mev > 10
ORDER BY started_at DESC
LIMIT 10;
 
SELECT e.name, r.run_number, m.t, m.value
FROM measurements AS m
JOIN runs AS r ON r.id = m.run_id
JOIN experiments AS e
    ON e.id = r.experiment_id
WHERE m.channel = 'BPM01';

Логический порядок выполнения:

01

FROM, JOIN

источники строк и условие соединения

02

WHERE

отбор строк

03

GROUP BY, HAVING

группы и отбор групп

04

SELECT

столбцы результата

05

ORDER BY, LIMIT

сортировка и ограничение

Пояснение

LEFT JOIN сохраняет строки левой таблицы без пары: так находят заходы без измерений.

GROUP BY

и HAVING

SELECT r.run_number, r.energy_mev,
       COUNT(*)     AS n,
       AVG(m.value) AS mean
FROM measurements AS m
JOIN runs AS r ON r.id = m.run_id
WHERE m.channel = 'BPM01'
GROUP BY r.id
HAVING COUNT(*) >= 100
ORDER BY r.energy_mev;

01

Агрегаты

COUNT, SUM, AVG, MIN, MAX сворачивают группу в одно значение

02

GROUP BY

признак группы: заход, канал, час

03

WHERE и HAVING

до группировки — по строкам, после — по агрегатам

Для физика

Сводка по заходам считается в базе: в программу приходят 40 строк, а не миллион.

Демонстрация:

журнал измерений

01

make_lab.py: схема и миллион строк одной транзакцией

02

JOIN трёх таблиц: заход 31 с названием кампании

03

GROUP BY: средний сигнал BPM01 по заходам

04

LEFT JOIN: заход без измерений

Что наблюдается

Миллион строк — за 0,9 с, файл 33,4 МиБ; средний сигнал растёт с энергией: 0,47, 0,48, 0,49 при 23,5–24,5 МэВ; без измерений — заход 41.

root@lab — запись со стенда

Индексы и планы

B-дерево, план и время запроса, цена индекса, планировщик и статистика, EXPLAIN ANALYZE

03

Зачем СУБД

Модель и SQL

Индексы и планы

Транзакции

Python и базы

Эксплуатация и выбор

Индекс:

B-дерево

корень: 14 · 28

5 · 9

18 · 23

32 · 36

1–4

5–8

9–13

…

…

лист 5–8: (7, 'BPM01') → rowid 150001 … 155000; ключи в листьях упорядочены

Индекс (run_id, channel): ключи упорядочены по run_id, затем по channel

Пояснение

Поиск — спуск от корня к листу: три-четыре страницы вместо просмотра миллиона строк.

Для физика

Индекс (run_id, channel) ускоряет поиск по run_id и по паре, по channel — лишь перебором run_id.

Демонстрация:

индекс и план

01

Запрос: средний сигнал BPM01 захода 7

02

План и время без индекса

03

Индекс (run_id, channel) и размер файла

04

План и время с индексом; сортировка по t

Что наблюдается

Без индекса — SCAN за 19,5 мс, с индексом — SEARCH за 0,39 мс, в 50 раз быстрее; файл вырос с 34 до 50 МиБ; ORDER BY t требует временного B-дерева.

root@lab — запись со стенда

Цена и выбор

индекса

01

Место

индекс (run_id, channel) — около 16 МиБ к 34 МиБ таблицы

02

Запись

каждая вставка обновляет все индексы таблицы

03

Селективность

условие на большую долю строк быстрее выполнить полным просмотром

04

Порядок столбцов

(run_id, channel, t) убирает и сортировку по t

-- точки канала сразу по порядку времени
CREATE INDEX idx_m_run_channel_t
ON measurements (run_id, channel, t);
 
-- он же покрывает внешний ключ run_id;
-- runs (experiment_id) покрыт индексом UNIQUE

Важно

Индекс на каждый столбец замедляет запись и раздувает базу: индексы строят под частые запросы.

Планировщик

и статистика

01

Варианты

полный просмотр или индекс, способ и порядок соединений

02

Стоимость

страница подряд — 1, вразброс — 4

03

Селективность

доля строк под условием — по статистике таблицы

04

ANALYZE

собирает статистику; в PostgreSQL его запускает автоочистка

-- SQLite: строк в таблице и на значение
SELECT idx, stat FROM sqlite_stat1;
-- idx_city | 400001 200
 
-- PostgreSQL: частые значения столбца
SELECT most_common_vals, n_distinct
FROM pg_stats
WHERE tablename = 'users'
  AND attname = 'city';

Важно

После массовой загрузки статистика устаревает: ANALYZE выполняют сразу после неё.

Демонстрация:

статистика ANALYZE

01

200 000 пользователей из Москвы, индекс по городу

02

Запрос без статистики и после ANALYZE

03

Ещё 200 000 жителей 2000 городов без ANALYZE

04

ANALYZE в том же и в новом соединении

Что наблюдается

Без статистики индекс медленнее просмотра: 11,7 мс против 8,1 мс; устаревшая статистика ведёт к SCAN ради одной строки — 13,7 мс; новое соединение — SEARCH за 0,01 мс.

root@lab — запись со стенда

EXPLAIN ANALYZE

в PostgreSQL

01

Дерево узлов

Seq Scan, Index Scan, Bitmap Heap Scan, Sort, Hash Join

02

Оценка и факт

rows против actual rows: многократное расхождение — устаревшая статистика

03

Buffers

страницы по 8 КиБ из кеша (hit) и с диска (read)

04

Execution Time

время на сервере без передачи результата

EXPLAIN (ANALYZE, BUFFERS)
SELECT avg(value) FROM measurements
WHERE run_id = 7 AND channel = 'BPM01';

Важно

EXPLAIN ANALYZE выполняет запрос: UPDATE и DELETE проверяют внутри BEGIN … ROLLBACK.

Пояснение

Sort Method: external merge Disk — сортировке не хватило work_mem (4 МБ на узел).

Демонстрация:

EXPLAIN ANALYZE

01

Таблица на миллион строк в PostgreSQL

02

План без индекса: параллельный просмотр

03

CREATE INDEX по (run_id, channel)

04

План с индексом: Index Scan

Что наблюдается

Без индекса — Parallel Seq Scan по 7353 страницам за 20,2 мс; с индексом — Index Scan по 45 страницам за 0,52 мс, почти в 40 раз быстрее.

root@lab — запись со стенда

Транзакции

ACID и цена фиксации, уровни изоляции, блокировки и WAL в SQLite, MVCC в PostgreSQL

04

Зачем СУБД

Модель и SQL

Индексы и планы

Транзакции

Python и базы

Эксплуатация и выбор

Транзакция

и ACID

def save_run(con, run_row, points):
    cur = con.cursor()
    try:
        cur.execute('INSERT INTO runs … '
                    'VALUES (?, ?, ?, ?)', run_row)
        run_id = cur.lastrowid
        cur.executemany('INSERT INTO measurements … '
                        'VALUES (?, ?, ?, ?)',
                        [(run_id, *p) for p in points])
        con.commit()     # всё
    except sqlite3.Error:
        con.rollback()   # или ничего
        raise

01

Atomicity

всё или ничего: сбой откатывает сделанное

02

Consistency

ограничения соблюдены до и после

03

Isolation

чужие промежуточные состояния не видны

04

Durability

после COMMIT данные переживут отключение питания

Пояснение

Конструкция with con: фиксирует при выходе из блока и откатывает при исключении, но соединение не закрывает.

Демонстрация:

откат и фиксация

01

save_run: заход 42 с двумя точками

02

Заход 43 с пустым значением во второй точке

03

with con: и повтор номера захода

04

1000 вставок: фиксация каждой и одна транзакция

Что наблюдается

Ошибка во второй точке откатывает и сам заход 43; фиксация каждой вставки — 776 мс, одна транзакция — 1,8 мс, в 430 раз быстрее.

root@lab — запись со стенда

Уровни

изоляции

УровеньГрязное чтениеНеповторяемое чтениеФантомыАномалия сериализации
READ UNCOMMITTEDвозможновозможновозможнывозможна
READ COMMITTEDнетвозможновозможнывозможна
REPEATABLE READнетнетвозможнывозможна
SERIALIZABLEнетнетнетнет

Пояснение

По умолчанию: PostgreSQL — READ COMMITTED, MySQL InnoDB — REPEATABLE READ, SQLite — SERIALIZABLE.

Важно

На строгих уровнях PostgreSQL может прервать транзакцию ошибкой сериализации: её повторяют.

Блокировки

и WAL в SQLite

журнал DELETE

старые страницы — в lab.db-journal,
новые — в lab.db при фиксации

COMMIT ждёт ухода всех читателей

читатели и писатель блокируют друг друга

журнал WAL

новые страницы дописываются
в lab.db-wal

читатели видят снимок, писатель не ждёт

контрольная точка переносит страницы в lab.db

Пояснение

Писатель всегда один: второй ждёт timeout (в sqlite3 — 5 с) и получает database is locked.

Важно

WAL не работает на сетевом диске (NFS, SMB): все процессы должны быть на одной машине.

Демонстрация:

блокировки и WAL

01

Два соединения: писатель A и читатель B

02

Запись B во время транзакции A

03

COMMIT A при открытом чтении B

04

Режим DELETE и режим WAL

Что наблюдается

Запись B ждёт 0,5 с и получает database is locked в обоих режимах; COMMIT при открытом чтении проходит только в WAL, а B до конца своей транзакции видит 2 строки.

root@lab — запись со стенда

MVCC

в PostgreSQL

страница таблицы runs

версия 1: energy = 5.0

xmin = 745, xmax = 746 (удалена в 746)

версия 2: energy = 5.5

xmin = 746, xmax = 0 (действующая)

снимок, взятый до фиксации 746

видит версию 1

снимок, взятый после фиксации 746

видит версию 2

Пояснение

UPDATE — новая версия и отметка xmax у старой; мёртвую версию убирает VACUUM, когда её не видит ни один снимок.

Важно

Долгая транзакция удерживает старый снимок: мёртвые версии накапливаются, таблица и индексы разрастаются.

Демонстрация:

MVCC и изоляция

01

Сеанс A: строка energy = 5.0, затем UPDATE

02

Сеанс B на уровне READ COMMITTED

03

Сеанс B на уровне REPEATABLE READ

04

Свой UPDATE в B после UPDATE в A

Что наблюдается

READ COMMITTED: B видит новую версию, energy=5.5, и его UPDATE даёт 6,5; REPEATABLE READ: B видит старую версию с xmax=746, а его UPDATE — could not serialize access.

root@lab — запись со стенда

Python и базы

DB-API: sqlite3 и psycopg, SQL-инъекции и параметры, ORM SQLAlchemy, pandas и форматы файлов

05

Зачем СУБД

Модель и SQL

Индексы и планы

Транзакции

Python и базы

Эксплуатация и выбор

DB-API:

sqlite3 и psycopg

import sqlite3
 
con = sqlite3.connect('lab.db')
with con:              # транзакция
    con.executemany(
        'INSERT INTO measurements'
        ' (run_id, channel, t, value)'
        ' VALUES (?, ?, ?, ?)', points)
rows = con.execute(
    'SELECT t, value FROM measurements'
    ' WHERE run_id = ?', (7,)).fetchall()

import psycopg
 
with psycopg.connect(dsn) as conn:
    rows = conn.execute(
        'SELECT t, value FROM measurements'
        ' WHERE run_id = %s', (7,)).fetchall()
    with conn.cursor().copy(
            'COPY measurements (run_id,'
            ' channel, t, value) FROM STDIN'
            ) as copy:
        for row in points:
            copy.write_row(row)

Пояснение

PEP 249: connect, execute, fetchall, commit одинаковы; различаются знаки параметров — ? и %s.

Важно

Соединение с PostgreSQL дорогое: сервис берёт его из пула (psycopg_pool), а не открывает на запрос.

SQL-инъекция

и параметры

channel = input('Канал: ')
 
# так нельзя: запрос склеен с чужим текстом
cur.execute('SELECT t, value FROM measurements '
            f"WHERE channel = '{channel}'")
# ввод: ' OR '1'='1
# условие: channel = '' OR '1'='1' — все строки
 
# правильно: текст запроса и данные раздельно
cur.execute('SELECT t, value FROM measurements '
            'WHERE channel = ?', (channel,))

01

Всегда параметры

и для своих данных, и для чисел

02

Имена — не параметры

таблицу и столбец берут из белого списка

03

executescript

выполнит все операторы склеенной строки

04

Права

сервис работает от роли без права на DROP

Демонстрация:

SQL-инъекция

01

Обычный ввод BPM01

02

Ввод ' OR '1'='1

03

Ввод с DROP TABLE через execute

04

Тот же ввод через executescript

Что наблюдается

' OR '1'='1 — склейка отдаёт все 1 000 000 строк, параметр — 0; execute отказывается выполнять второй оператор, а executescript удаляет measurements.

root@lab — запись со стенда

ORM

SQLAlchemy 2.x

class Run(Base):
    __tablename__ = 'runs'
    id: Mapped[int] = mapped_column(
        primary_key=True)
    energy_mev: Mapped[float]
    measurements: Mapped[list['Measurement']] = \
        relationship(back_populates='run')
 
engine = create_engine('sqlite:///orm.db')
stmt = (select(Measurement.channel, Run.energy_mev)
        .join(Run).where(Run.energy_mev > 10.0))
for channel, energy in session.execute(stmt):
    print(channel, energy)

01

Классы и связи

таблицы — классы, строки — объекты, ключи — атрибуты

02

Тот же SQL

запрос строится выражениями; echo=True печатает SQL

03

N+1

связь в цикле — запрос на объект; selectinload — два запроса

04

Миграции

изменения схемы оформляет Alembic

Пояснение

PostgreSQL: create_engine('postgresql+psycopg://…') и установленный драйвер psycopg.

Демонстрация:

ORM и запрос N+1

01

Модели Run и Measurement, три захода

02

SQL, построенный select(), и результат

03

Ленивая загрузка и selectinload

04

Тот же скрипт с адресом PostgreSQL

Что наблюдается

Один код для SQLite и PostgreSQL: SQL различается только записью параметра, ? и %(energy_mev_1)s; ленивая загрузка — 4 запроса, selectinload — 2.

root@lab — запись со стенда

pandas:

read_sql и to_sql

df = pd.read_sql(
    '''SELECT r.energy_mev,
              AVG(m.value) AS mean_signal
       FROM measurements AS m
       JOIN runs AS r ON r.id = m.run_id
       WHERE m.channel = 'BPM01'
       GROUP BY r.id''', con)
k, b = np.polyfit(df.energy_mev,
                  df.mean_signal, 1)
df.to_sql('run_summary', con,
          if_exists='replace', index=False)

ФорматДля чего
CSVобмен с любыми программами
SQLiteжурнал: запись по строке, выборка по индексу
Parquetархив для анализа: столбцы, сжатие

Важно

Таблицу to_sql создаёт без ключей и ограничений: схему постоянных данных пишут вручную.

Демонстрация:

pandas и Parquet

01

read_sql: сводка по заходам в DataFrame

02

Прямая через точки, to_sql и схема

03

Миллион строк в CSV, SQLite и Parquet

04

Чтение целиком и одного канала

Что наблюдается

Наклон 0,0200 на МэВ восстановлен; Parquet — 4,5 МиБ и 16 мс на чтение всего, SQLite — 725 мс на всё, но 3,6 мс на один канал по индексу.

root@lab — запись со стенда

Эксплуатация и выбор

Резервные копии и PITR, репликация, масштабирование, мониторинг, нереляционные хранилища, выбор СУБД

06

Зачем СУБД

Модель и SQL

Индексы и планы

Транзакции

Python и базы

Эксплуатация и выбор

Резервные

копии

СпособЧто получаетсяОграничение
cp файла базыкопия, если к базе никто не подключёнв работе и в WAL — неполная копия
backup(), VACUUM INTOсогласованная копия SQLite в работеодна машина, одна база
pg_dumpлогическая копия базына момент начала, без PITR
pg_basebackup и архив WALфизическая копия и журнал: PITRвесь кластер, та же версия

Важно

Копия без проверенного восстановления копией не считается: восстановление репетируют.

Для физика

Правило 3-2-1: три копии на двух видах носителей, одна из них — вне здания.

Демонстрация:

копия базы в WAL

01

writer.py: 500 строк в базе WAL, соединение открыто

02

cp основного файла

03

backup() и VACUUM INTO

04

Копия после завершения писателя

Что наблюдается

Пока писатель работает, данные в wal.db-wal (28 872 байта), а копия cp — без таблицы; backup() и VACUUM INTO — по 500 строк; после закрытия журнал перенесён в wal.db.

root@lab — запись со стенда

Демонстрация:

pg_dump и pg_restore

01

DROP TABLE в транзакции и ROLLBACK

02

pg_dump всей базы в архив

03

DROP TABLE без транзакции

04

pg_restore в новую базу и проверка

Что наблюдается

DROP TABLE внутри транзакции откатывается вместе с ней; архив pg_dump занимает 8,0 МиБ; после необратимого DROP TABLE восстановленная база снова содержит 1 000 000 строк.

root@lab — запись со стенда

WAL:

PITR и реплики

основной сервер

пишет WAL раньше,
чем файлы таблиц

WAL

архив WAL и базовая копия

восстановление на момент (PITR)

физическая реплика

поток WAL; только чтение; та же версия

логическая репликация

изменения строк; другая версия или СУБД

Для физика

Ошибочный DROP TABLE в 18:03: из ночной копии и архива WAL база восстанавливается на 18:02:59.

Важно

Реплика не заменяет копию: ошибочная команда доходит до неё за доли секунды.

Масштабирование

01

Пул соединений

PgBouncer: тысячи клиентов на десятки процессов сервера

02

Реплики для чтения

отчёты и графики — с реплики, запись — на основной

03

Секционирование

PARTITION BY RANGE (t): месяц — секция

04

Шардирование

данные по ключу на нескольких серверах

Для физика

Временной ряд датчиков секционируют по месяцам: архив старше срока хранения удаляется одной командой.

Пояснение

Встроенное шардирование — у ClickHouse, MongoDB, YDB; в PostgreSQL — расширение Citus или своё в приложении.

Важно

Шардирование планируют заранее: переделка работающей базы в десятки терабайт занимает месяцы.

Мониторинг

базы

01

Ресурсы

процессор, память, место и inode, операции диска в секунду

02

Запросы

число в секунду, задержка p50 и p99, доля по типам

03

Ошибки

тревога по порогу, а не по каждой записи журнала

04

Признаки СУБД

медленные запросы, мёртвые версии, отставание реплик

-- самые дорогие запросы
SELECT calls, mean_exec_time, query
FROM pg_stat_statements
ORDER BY total_exec_time DESC
LIMIT 5;
 
-- мёртвые версии строк
SELECT relname, n_dead_tup
FROM pg_stat_user_tables;

Важно

Нижние пороги: нагрузка, упавшая до нуля, — тоже авария.

Нереляционные

хранилища

ТипПримерРольОграничение
ключ — значение в памятиValkey, Redisкеш, очереди, счётчикипотеря записей после снимка RDB; блокировка сервера командой KEYS *
документыMongoDBразнородные метаданныесхема — в коде или в валидаторе $jsonSchema
столбцыClickHouseаналитика миллиардов строквставка партиями от тысяч строк
временные рядыInfluxDB, TimescaleDBдатчики, медленный контрольсроки хранения и прореживание

Для физика

Данные ускорительного комплекса: PostgreSQL — строгие метаданные, MongoDB — разнородные измерения, Redis — текущие состояния устройств.

Выбор

СУБД

01

Скучная технология

десятилетия в работе, известные ошибки, специалисты

02

Копии и PITR

восстановление на момент и его проверка

03

Объём и рост

поместится ли на один сервер

04

Нагрузки

транзакции и аналитика — в разных системах

05

Истории неудач

не только успехи в блоге разработчиков

06

Безопасность

аутентификация, TLS, права по умолчанию

Пояснение

Один исследователь и его скрипты — SQLite; общая база группы — PostgreSQL; вторая СУБД — когда первая измеримо не справляется.

Для физика

Тип JSONB в PostgreSQL покрывает потребность в документах для большинства задач лаборатории.

Итоги

лекции

01

СУБД берёт на себя одновременный доступ, целостность, поиск и восстановление — то, с чем не справляются файлы.

02

Схема с ключами и ограничениями — документация, которую база проверяет сама; SQL описывает результат, а не перебор.

03

Индекс превращает просмотр миллиона строк в спуск по B-дереву; план выбирается по статистике.

04

Транзакция — всё или ничего; изоляцию дают блокировки в SQLite и версии строк в PostgreSQL.

05

Значения — только параметрами; копия базы — только с проверенным восстановлением.

Упражнения

и задание

01

Схема своей установки

три-четыре таблицы с ключами и импорт накопленных CSV

02

План запроса

поиск медленного запроса, индекс и проверка плана

03

Изоляция

повторение опыта MVCC на уровне SERIALIZABLE

04

Копия и восстановление

pg_dump, восстановление в новую базу, сверка строк

Пояснение

Задание курса: «Деплой стартапа „Котики в мир“» — приложение, контейнеры, сеть и база.

Для физика

Журнал своей установки в SQLite — первый шаг от папок с CSV к общей базе группы.

Источники

1

Книга курса: главы «Базы данных» и «Работа с базами данных»

phys-dev.github.io/soft-dev-book/dev/bd.html

2

Лекция «Базы данных», КИТ 2024, Young&&Yandex; Е. Дюков

youtube.com/watch?v=Y0uqMf2vGpg

3

Документация PostgreSQL 17

postgresql.org/docs/17

4

Документация SQLite

sqlite.org/docs.html

5

Kleppmann M. Designing Data-Intensive Applications. – O'Reilly, 2017; рус. пер.: Клеппман М. Высоконагруженные приложения. – СПб.: Питер, 2018

dataintensive.net

6

Документация SQLAlchemy 2 и psycopg 3

docs.sqlalchemy.org/en/21, psycopg.org/psycopg3/docs

7

Winand M. Use The Index, Luke: SQL Indexing and Tuning

use-the-index-luke.com

8

Рогов Е. В. PostgreSQL 18 изнутри. – М.: ДМК Пресс, 2026

postgrespro.ru/education/books/internals

Спасибо за внимание

Вопросы

phys-dev.github.io/soft-dev-book

COMMIT