На этом курсе мы будем проектировать реляционные базы данных с помощью СУБД PostgreSQL
Также будут разбираться другие СУБД, реализующие другие модели данных, а также работа с контейнерами.
Модель данных - это концептуальное описание структуры данных, их взаимосвязей, а также правил и ограничений, регулирующих работу с данными. Система управления БД реализует определенную модель данных
Можно выделить такие модели данных:
Модели данных помогают нам преобразовать объекты доменной области в понятные компьютеру образы.
Нормализация базы данных - это процесс структурирования данных в базе данных в соответствии с определенными правилами, направленный на устранение избыточности данных и обеспечение их целостности. Основная идея нормализации заключается в разбиении больших таблиц на несколько меньших, логически связанных между собой, с целью обеспечения целостности и согласованности данных
Нормальные формы ведут за собой потери производительности при агрегации данных. В каких-то случаях применяется денормализация - обратный процесс, целью которого является увеличение избыточности и производительности
На курсе “Базы данных” мы изучали виды ключей записей в реляционной БД:
Суперключ - набор одного или нескольких атрибутов, которые в совокупности позволяют уникально идентифицировать каждую запись в таблице
Потенциальный ключ - минимальный суперключ, который не содержит избыточных атрибутов
Первичный ключ - один из потенциальных ключей, выбранный для уникальной идентификации записей в таблице
Внешний ключ - атрибут или множество атрибутов, которые соответствуют потенциальному ключу некоторого (может быть того же самого) отношения
Также ключ может быть естественным и суррогатным
Естественный ключ - это атрибут (или комбинация), который уже присутствует в данных и имеет смысл с точки зрения предметной области
Пример: адрес электронной почты в базе клиентов
Плюсы: экономия памяти
Минусы: может измениться, может быть неопределенной длины
Суррогатный ключ - искусственно созданный уникальный идентификатор
Плюсы: не может потребоваться его менять, занимает мало места
Минусы: избыточные данные
Роль суррогатного ключа может принять автоинкремент (увеличенное на 1 значение ключа предыдущей созданной записи) или UUID
Также в PostgreSQL существуют такие типы данных:
Целочисленные типы - smallint, int, bigint, (serial, big serial)
С плавающей точкой - real, double, money, decimal
Строки - varchar, char, text
Дата и время - date, time, timestamp, interval
Логический - bool
Геометрические типы - point, path, line
Как уже говорилось, модели данных со своими преимуществами и недостатками имеют свои области применения, в которых они будут наиболее эффективны
По статистике реляционные базы данных имеют наибольшую популярность в разработке
Реляционная модель организует данные в виде двухмерных таблиц со строгой структурой. Данные вносятся как строки, колонки которых соответствуют определенным типам данных. Между таблицами, отношениями, можно настроить семантические связи. Реляционные базы данных используют структурированный язык запросов SQL и соответствуют ACID свойствам
Преимущества реляционных баз данных:
Благодаря строгой структуре и ACID свойствами, обеспечивается целостность данных
Использование SQL как стандарта облегчает разработку, миграцию и поддержку
Возможность выполнения сложные агрегирующих запросов
Большое сообщество
Недостатки:
Трудно хранить слабоструктурированные данные
Горизонтальное масштабирование реляционных БД зачастую сложнее реализовать, чем в NoSQL решениях
При большом количестве взаимосвязанных таблиц схемы становятся громоздким, что бьет по производительности
Применения реляционных БД:
OLTP-системы (Online transaction processing, транзакции небольшого размера обрабатываются с минимальным временем отклика)
Системы с высокими требованиям к целостности данных
Данные имеют строгую структуру
Распространенные реализации: PostgreSQL, MySQL, SQLite, MariaDB, Microsoft SQL Server, Oracle DB, YandexDB
В колоночных БД вместо того, чтобы хранить каждую запись целиком, данные организуются по столбцам, то есть значения одного атрибута хранятся последовательно, что позволяет оптимизировать запросы над конкретными наборами данных. Колоночные БД считаются NoSQL
Преимущества:
Оптимизация запросов
Если атрибут имеет маленький домен (например, перечисление), то, храня их последовательно, можно кодировать их более оптимально, что позволяет сэкономить память
Недостатки:
Операции, характерные OLTP, работают менее эффективно
При частых изменениях данных могут возникать проблемы с синхронизацией
Применение:
Big Data
OLAP-системы (Online Analytical Processing)
Бизнес-аналитика (отчеты, дашборды)
Исторические данные
Реализации: Cassandra, ScyllaDB, Google BigQuery, ClickHouse (колоночная реляционная СУБД)
Данные хранятся в виде документов (обычно представляющиеся в формате BSON), а не в виде строк таблицы. Документы объединяются в коллекции (по аналогии с таблицами, но без обязательного соответствия единой схеме). Причисляется к NoSQL
Преимущества:
Возможность изменения структуры документов без необходимости модифицировать общую схему БД
Документы в формате JSON или BSON естественным образом отображаются на объекты в языках программирования
Простота горизонтальной масштабируемости
Недостатки:
Применение:
Хранение логов
OLTP-системы
Разработка MVP
Неструктурированные данные
Обработка больших объемов данных
Реализации: MongoDB, Firebase Realtime Database, Elasticsearch, Amazon DynamoDB, CouchDB
Вместо таблиц и кортежей в графовой БД объектами являются узлы, ребра и их свойства. Графовые БД считаются NoSQL
Преимущества:
Запросы на поиск путей, обход графа, анализ связей выполняются быстро
Нет жесткой структуры
Легко добавлять новые типы связей и узлов без изменения общей схемы
Недостатки:
Применение:
Социальные сети
Системы рекомендаций
Управление цепочками поставок
Фрод-анализ (поиск мошеннических активностей)
Реализации: Neo4j, Dgraph
Данные в “ключ-значение” БД хранятся в виде ключ-значение
Преимущества:
Операции получаения, вставки, удаления работают за O(1)
Быстрые и требуют мало ресурсов
Недостатки:
Сложно агрегировать данные или составлять выборку
Слабая структура
Применение:
Кэширование данных
Хранение сессионных данных
Реализация очередей
Хранение конфигурационных данных
Реализации: Redis, Memcached, Etcd, Kafka
Данные упорядочены по времени (в данном случае ключ - это метка времени)
Преимущества:
Высокая скорость записи
Легко делать агрегацию и интерполяцию
Хранение больших объемов данных
Анализ данных за определенный промежуток времени
Недостатки:
Применение:
Мониторинг и логирование
Финансовые рынки
Интернет вещей
Реализации: InfluxDB, TimescaleDB, Prometheus
Данные хранятся в виде векторов вещественных чисел
Преимущества:
Недостатки:
Применение: кластеры, техническое зрение, машинное обучение
Реализации: Pinecone, Milvus
Данных хранятся в виде файлах (текст, аудио, видео и др.) в файловой системе. Помимо этого каждый файл хранит метаданные. Доступ к файлам осуществляется по их именам и путям в файловой системы
Преимущества:
Интеграция с ОС и стандартными файловыми менеджерами
Возможность выстраивания сложной модели доступа
Возможность сделать CDN (Content Delivery Network)
Недостатки:
Применение: хранение и распространение файлов
Реализации: Amazon S3, MinIO
При проектировании баз данных надо правильно уметь читать и составлять функциональные требования. Функциональные требования - это такой документ, который формально описывает сущности бизнес-логики, их свойства/связи и действия, которые они совершают или над которыми они совершают
Разберем функциональные требования для аналога Twitter:
В PostgreSQL используется snake_case
, отношения называются в множественном числе, а атрибуты в единственном.
В первичных и внешних ключах указывается название отношения, чтобы избежать коллизии при объединении отношений, например, user_id
и chat_id
Многое-ко-многим
Тривиальной реализацией связи многое-ко-многим будет являться атрибут первого отношения, в котором будут указаны первичные ключи второго отношения (в формате JSON, через запятую или по-другому), однако это будет не оптимально
Правильно будет сделать отдельное отношение с двумя столбцами, в котором первичным ключом будет пара из ключей первого и второго отношений
Вертикальное разделение
Вместо того, чтобы добавлять новые атрибуты в исходное отношение, мы можем создать новое отношение с этим атрибутами. Например:
user_id | username | |
---|---|---|
user_id | big_string | big_binary | biography |
---|---|---|---|
Если во втором отношении поместить малоиспользуемые атрибуты, то мы получим выигрыш в оптимизации
Фиксированный набор значений
Фиксированный набор значений можно реализовать несколькими способами:
ENUM
в PostgreSQLCONSTRAINT
в PostgreSQLEAV (Entity-Attribute-Value)
EAV - отношение с тремя атрибутами:
EAV позволяет хранить данные с динамически изменяемой структурой, когда новые атрибуты могут добавлять без необходимости изменения структуры
Полиморфные связи
Допустим, что комментарии можем ставить и на посты, и на картинки. Тогда отношение с комментариями можем представить так:
comment_id | content | commantable_id | commantable_type |
---|---|---|---|
Здесь commentable_type
определяет, из какого отношения commentable_id
будет являться ключом. Однако такой паттерн делает невозможным использование внешнего ключа
Список смежных вершин
Каждый кортеж будет хранить в себе указатель на родителя, тем самым моделируя дерево.
Пример:
name | id | parent |
---|---|---|
food | 1 | null |
vegetable | 2 | 1 |
potato | 3 | 2 |
tomato | 4 | 2 |
fruit | 5 | 1 |
apple | 6 | 5 |
banana | 7 | 5 |
Вложенное множество
Вложенное множество (Nested set) - еще один способ моделирования дерева. Дерево можно представить как вложенное множество:
Вместо хранения указателя на родителя, мы будем хранить указатели на левый и правый концы отрезков:
name | left_end | right_end |
---|---|---|
Одежда | 1 | 22 |
Мужская | 2 | 9 |
Женская | 10 | 21 |
Костюмы | 3 | 8 |
Брюки | 4 | 5 |
Куртки | 6 | 7 |
Платья | 11 | 16 |
Юбки | 17 | 18 |
Блузки | 19 | 20 |
Вечерние платья | 12 | 13 |
Сарафаны | 14 | 15 |
Благодаря этому, мы можем с легкостью определить, является ли какой-либо узел потомком другого узла. Изменять структуру такого дерева - задача нетривиальная, поэтому вложенные множества подойдут для тех деревьев, которые будут чаще всего просматриваемые, нежели изменяемые
Материализованный путь
Материализованный путь (Materialized path) - еще один способ моделирования дерева, в которым для каждого узла мы храним его путь из корня. Например:
name | path |
---|---|
food | 1 |
vegetable | 1.1 |
potato | 1.1.1 |
tomato | 1.1.2 |
fruit | 1.2 |
apple | 1.2.1 |
banana | 1.2.2 |
При помощи инструкции LIKE
в SQL мы можем найти всех потомков какого-либо узла. Как и вложенные множества, такой паттерн рекомендуется применять для почти неизменяемых деревьев
При запуске PostgreSQL сервере первым делом создается процесс Postmaster. Postmaster - главный процесс, который принимает входящие подключения от клиентов, управляет созданием и завершением дочерних процессов
Для каждого нового подключения Postmaster порождает отдельный процесс, который обрабатывает SQL-запросы, выполняет планирование и возвращает результаты клиенту. Такой подход изолирует сессии и повышает стабильность системы
PostgreSQL использует дополнительные фоновые процессы для выполнения служебных задач:
Checkpointer периодически сбрасывает измененные данные из общей памяти (shared buffers) на диск. Это снижает время на восстановление после сбоев и оптимизирует запись: данные сначала попадают в буфер, а затем периодически записываются на диск
WAL Writer обеспечивает запись журналов предзаписи (Write-Ahead Logging, WAL), что гарантирует, что изменения сначала фиксируются в журнале, прежде чем быть записанными на диск. WAL Writer используется для обеспечения надежности и возможности восстановления
Background Writer фоново записывает изменённые страницы из буферного кэша на диск, помогая поддерживать актуальность данных и снижать пик нагрузок при массовых запросах
Stats Collector собирает статистику, ведёт учет выполнения запросов, использования ресурсов и другой информации, необходимой для работы планировщика запросов и оптимизации системы
Autovacuum - процесс очистки (об этом далее)
PostgreSQL выделяет область общей памяти, используемую для:
Данные хранятся в виде файлов на диске. Для взаимодействия с хранилищем данных используются специализированные модули доступа и загрузчик данных
PostgreSQL обрабатывает запросы так:
Прием запроса. Запрос поступает через клиент к дочернему процессу, созданному Postmaster
Парсинг. SQL-текст разбивается на токены и строится синтаксическое дерево. Здесь производится проверка на ошибки
Рерайтинг. Запрос преобразуется с учетом представлений, правил и триггеров
Планирование и оптимизация. Планировщик запросов анализирует различные стратегии выполнения, используя собранную статистику, и выбирает наиболее оптимальный план
Исполнение. Выполнение плана запроса: данные извлекаются, обрабатываются через узлы (фильтрация, сортировка, соединения и т.д.) и формируется результат
Возврат результатов. После выполнения запроса результаты возвращаются клиенту. Для операций, изменяющий данные, делается коммит изменений
PostgreSQL реализует механизм MVCC (multiversion concurrency control, многоверсионный контроль согласованности), позволяющий обеспечить согласованность данных при одновременном доступе множества пользователей
Внутри себя PostgreSQL хранит счетчик транзакций, который можно получить функцией txid_current()
. При выполнении транзакции этот счетчик увеличивается. Каждая запись в отношении имеет системные атрибуты xmin
и xmax
xmin
xmax
xmax
, а новой присваивается в поле xmin
Таким образом, эти атрибуты задают жизненный цикл каждого снимка каждой записи, чтобы соблюдать целостность данных.
Помимо них есть еще cmin
и cmax
. Они обозначают жизненный цикл самой записи и не затрагиваются при обновлении записи
Так как MVCC требует очень много памяти на диске, нужны инструменты, очищающие неактуальные данные. В PostgreSQL есть:
Vacuum - команда для очистки базы данных, которая удаляет “мертвые” строки (устаревшие версии записей, помеченные как удаленные) и сбрасывает счетчик транзакций, участвующий в механизме MVCC, тем самым предотвращая его переполнение
Autovacuum - автоматическая версия процесса очистки, которая определяет необходимость очистки таблиц на основании внутренней статистики и автоматически запускает процесс Vacuum для удаления устаревших данных и реорганизации таблиц, что помогает поддерживать оптимальную производительность базы данных
Вместо этой лекции была встреча с Олегом Сергеевичем Бартуновым, сооснователем компании Postgres Professional, развивающей форк PostgreSQL для enterprise-решений. Здесь же представлено краткое содержание того, что на ней было - крайне рекомендуется послушать все самим
Первые данные появились, когда человек захотел с кем-то пообщаться или что-то сохранить. Например, астрономия. Астрономические наблюдения требуют больших объемов хранения данных
Сначала примитивными базам данных были шумерские таблички для учета казны и налогом, потом появились каталоги по авторам в библиотеках. Файлы стали первой попыткой организовать данные
Первой настоящей базой данных стала IMS (Information Management System, другое название DB1). IBM создала ее по тендеру для ракеты Saturn V. Она была иерархической и хранила в себе состояние компонентов ракеты
После этого Кодд выпускает свою статью о реляционной модели данных и реляционной алгебре. После этого, Дональд Чемберлин и Рэймонд Бойс создают язык SEQUEL, который изменит свое название на SQL
В период с 75-ого по 82-ой создаются прототипы реляционной СУБД System R (или же DB2)
До этого, узнав о статье Кодда, ученые из университета Беркли Майкл Стоунбрейкер и Юджин Вонг создают свою СУБД под названием Ingres (Interactive Graphics Retrieval System)
Намного позднее Стоунбрейкер с 1986 по 1994 создает реляционную СУБД Postgres (производное от Post Ingres). Postgres изначально распространялся с открытым исходным кодом. Изначальный язык запрос POSTQUEL был заменен на SQL - с тех пор она стала называться PostgreSQL
Олег Бартунов начал вносить вклад в код PostgreSQL в 1995 году. С тех пор благодаря ему и другим разработчикам из России PostgreSQL обзавелся интернационализацией, поддержкой локалей, полнотекстовым поиском, jsonb. В частности благодаря Вадиму Михееву в PostgreSQL появился WAL, MVCC, vacuum и другие фичи
Сейчас же компания Postgres Professional является вторым вкладчиком в мире в кодовую базу PostgreSQL
Контейнеризация - абстракция вокруг группы процессов, позволяющая их изолировать и предоставляющая виртуализацию на уровне операционной системы. В отличии от полной виртуализации операционной системы, контейнеризация не загружают все ядро ОС целиком, а лишь использует механизмы ядра Linux - в частности для этого используются две фичи Linux: контрольные группы (cgroups) и пространства имен (namespaces)
Пространства имен - набор инструментов (функции ядра) для контроля того, что видит отдельный процесс и какие действия может выполнять как процесс, так и пользователь внутри пространства имен
Раньше процесс можно было ограничивать только по процессорному времени. С появлением контрольных групп стало возможным ограничивать использование ресурсов сетевых, памяти и так далее. Контрольные группы включают в себя контроллеры для учета и управления ресурсами
Самая популярная утилита для управления контейнерами - Docker.
Docker, помимо реализации пространства имен и контрольных групп, включает в себя управление образами, CLI для работы с контейнером и прочее
Образ (Image) - это по сути архив с предустановленными заранее программами и библиотеками для работы приложения.
Образ может определяться специальным файлом с названием Dockerfile
, например:
# Делаем форк на основе готового образа с установленным Python
FROM python:3.12
WORKDIR /usr/local/app
# Устанавливаем зависимости приложения
COPY requirements.txt ./
RUN pip install --no-cache-dir -r requirements.txt
# Копируем исходники
COPY src ./src
EXPOSE 5000
# Создает нового пользователя, чтобы не запускать приложение от лица root
RUN useradd app
USER app
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8080"]
Каждая строчка в файле Dockerfile
называется слоем (layer). Чтобы собрать из Dockerfile
полноценный образ, воспользуемся командой
docker build -t my-python-app .
В Docker Desktop можно посмотреть на слои образа:
Также можно посмотреть публичные образы в Docker Hub.
Собранный образ теперь можно запустить в контейнере.
В файле docker-compose.yml
можно указать конфигурацию контейнера
# Версия параметров Compose
version: "2"
services:
postgres:
# Имя образа (в данном случае с готовым PostgreSQL 14)
image: postgres:14
# Имя контейнера
container_name: postgres_db
# Переназначение портов между контейнером и хостом
ports:
- "5432:5432"
# Привязка файловой системы контейнера с файловой системой хоста
volumes:
- ./scripts:/docker-entrypoint-initdb.d
- ./pgdata:/var/lib/postgresql/data
# Переменные среды: имя, пароль и название БД в PostgreSQL
environment:
POSTGRES_USER: myuser
POSTGRES_PASSWORD: mypass
POSTGRES_DB: mydb
Чтобы запустить контейнер, воспользуемся командой
docker compose up
в каталоге с docker-compose.yml
. Флаг -d
запустит контейнер в фоновом режиме. Команда
docker compose down
прервет выполнение в контейнере, а команда
docker compose stop
остановит контейнер с возможностью возобновления. При помощи
docker ps
можно посмотреть все запущенные контейнеры.
В примере выше мы связали локальную папку ./scripts
с /docker-entrypoint-initdb.d
. При запуске PostgreSQL выполнит все .sql
и .sh
-скрипты, находящиеся в ней, таким образом, можно задать начальную схему базы данных
Миграция описывает последовательность изменений, которые необходимо применить к базе данных. Эти изменения могут включать создание новых таблиц, изменение существующих, добавление или удаление столбцов, создание индексов, изменение ограничений и так далее
Миграция должна удовлетворять свойству идемпотентности - свойство объекта или операции при повторном применении операции к объекту давать тот же результат, что и при первом. Для осуществления этого в PostgreSQL есть фраза IF NOT EXISTS
Скрипты с миграциями лучше всего версионировать, например, называть:
00001.sql
, 00002.sql
0.0.1.sql
, 0.1.0.sql
(семантическое версионирование)20250326_193204.sql
, 20250330_074332.sql
Помимо скрипта применения миграции рекомендуется иметь скрипт для отката. Тогда прямые изменения называют up-миграцией, а обратные - down-миграцией
Миграции можно генерировать при помощи:
описания на xml и подобных языках (например, как в утилитах для деплоя баз данных Liquibase и Flyway)
ООП-кода и фреймворков, делающих ORM-преобразования; это позволяет синхронизировать доменные модели со схемой базы данных, однако теряется контроль над сырыми SQL-запросами (например, Django в Python или Hibernate, Spring в Java)
Также хорошей практикой будет иметь служебную табличку в базе данных, которая хранит список примененных миграций
Во время совершения миграций могут возникнуть сложности, такие как нарушение обратной совместимости или повышенная нагрузка на БД
Например, операция
ALTER TABLE table_name ADD COLUMN SET DEFAULT;
является небезопасной, приводящая к даунтауму, так как она блокирует всю таблицу, пока ставит дефолтные значения каждой записи. Правильным решением будет:
ALTER TABLE table_name ADD COLUMN column_name data_type;
ALTER TABLE table_name ALTER COLUMN column_name SET DEFAULT default_value;
Чтобы изменить схему таблицы и переместить даные в ходе миграции, можно поступить так:
Такие ETL-инструменты (от Extract-Transform-Load) как Apache NiFi, Pentaho Data Integration, Apache Airflow могут автоматически производить миграции данных
Транзакция - последовательность операций с базой данных, которая выполняется как единое целое. Транзакция либо завершается успешно, либо полностью отменяется, чтобы гарантировать целостность данных
Транзакция должна соблюдать свойства ACID:
Чтобы соблюдать согласованность и целостность данных, в PostgreSQL можно установить ограничения на атрибуты:
NOT NULL
- значение атрибута не может принимать NULLUNIQUE
- значения атрибута должны быть уникальныPRIMARY KEY
- первичный ключ (по сути NOT NULL
+ UNIQUE
)FOREIGN KEY
- внешний ключ (значение атрибута должна быть ключом в другом отношении)CHECK
- проверка условийEXCLUSION CONSTRAINTS
- расширенные условияЧтобы соблюдать изоляцию транзакций, в PostgreSQL существуют 4 уровня изоляции:
Уровень изоляции | Грязное чтение | Неповторяемое чтение | Фантомное чтение | Аномалия сериализации |
---|---|---|---|---|
Незавершенное чтение | Разрешено, но не в PostgreSQL | Возможно | Возможно | Возможно |
Завершенное чтение | Невозможно | Возможно | Возможно | Возможно |
Воспроизводимое чтение | Невозможно | Невозможно | Разрешено, но не в PostgreSQL | Возможно |
Сериализуемость | Невозможно | Невозможно | Невозможно | Невозможно |
Незавершенное чтение (или Read uncommitted) позволяет только одной транзакции изменять кортеж (не используется в PostgreSQL)
Завершенное чтение (или Read committed) по умолчанию включено в PostgreSQL - транзакция видит только закоммиченные изменения
Воспроизводимое чтение (или Repeatable read) запрещает транзакциям изменять кортежи, которые читает другая транзакция
Сериализуемость (или Serializable) запрещает добавлять кортежи в отношении, если другая транзакция может их прочитать
Для управления транзакция в PostgreSQL есть такие инструкции:
Для начала транзакции:
BEGIN;
Для начала транзакции с определенным уровнем:
BEGIN ISOLATION LEVEL READ UNCOMMITTED;
BEGIN ISOLATION LEVEL READ COMMITTED;
BEGIN ISOLATION LEVEL REPEATABLE READ;
BEGIN ISOLATION LEVEL SERIALIZABLE;
Для коммита обновлений:
COMMIT;
Для отката:
ROLLBACK;
Опишем проблемы, которые решают данные уровни изоляции:
Грязная запись - аномалия, при которой обе транзакции записывают данные в один и тот же кортеж до коммита. Если какая-либо из них откатиться, то потеряется запись другой:
Транзакция 1 | Транзакция 2 | |
---|---|---|
BEGIN; |
BEGIN; |
|
UPDATE accounts SET balance=200 WHERE id=1; |
||
UPDATE accounts SET balance=300 WHERE id=1; |
||
COMMIT; |
Транзакция 1 коммитит 300 | |
ROLLBACK; |
Откат до 100, хотя транзакция 1 выполнилась успешно |
Потерянное обновление - такая аномалия возникает, когда две транзакции читают одну и ту же строку таблицы, затем одна транзакция обновляет эту строку, а после этого вторая транзакция тоже обновляет ту же строку, не учитывая изменений, сделанных первой транзакцией:
Транзакция 1 | Транзакция 2 | |
---|---|---|
BEGIN; |
BEGIN; |
Обе читают исходное значение |
SELECT balance FROM accounts WHERE id=1; |
Транзакция 1 читает 100 | |
SELECT balance FROM accounts WHERE id=1; |
Транзакция 2 читает 100 | |
UPDATE accounts SET balance=100+50 WHERE id=1; |
Транзакция 1 записывает 150 | |
UPDATE accounts SET balance=100+20 WHERE id=1; |
Транзакция 2 записывает 120 | |
COMMIT; |
||
COMMIT; |
Обновление транзации 1 потеряно |
Грязное чтение - такая аномалия возникает, когда транзакция читает еще не зафиксированные изменения, сделанные другой транзакцией:
Транзакция 1 | Транзакция 2 | |
---|---|---|
BEGIN; |
BEGIN; |
|
UPDATE accounts SET balance=200 WHERE id=1; |
||
SELECT balance FROM accounts WHERE id = 1; |
Транзакция 2 читает 200 | |
ROLLBACK; |
Откат от 200 к 100 | |
SELECT balance FROM accounts WHERE id = 1; |
Транзакция 2 читает 100 |
Неповторяющееся чтение возникает, когда транзакция читает одну и ту же строку два раза, а в промежутке между чтениями вторая транзакция изменяет ее, таким образом результат чтения разный:
Транзакция 1 | Транзакция 2 | |
---|---|---|
BEGIN; |
BEGIN; |
|
SELECT balance FROM accounts WHERE id=1; |
Транзакция 1 читает 100 | |
UPDATE accounts SET balance=200 WHERE id=1; |
||
COMMIT; |
Коммит значения 200 | |
SELECT balance FROM accounts WHERE id=1; |
Транзакция 1 читает 200 в той же транзакции |
Фантомное чтение возникает, когда между чтениям кортежей в одной транзакции другая транзакция добавляет новый кортеж:
Транзакция 1 | Транзакция 2 | |
---|---|---|
BEGIN; |
BEGIN; |
|
SELECT * FROM accounts WHERE balance > 100; |
Транзакция 1 видит 2 строки | |
INSERT INTO accounts(id, balance) VALUES (3, 300); |
||
COMMIT; |
||
SELECT * FROM accounts WHERE balance > 100; |
Tранзакция 1 теперь видит 3 строки |
Например, здесь
IF (SELECT amount FROM accounts WHERE id = 1) >= 1000 THEN
UPDATE accounts SET amount = amount - 1000 WHERE id = 1;
END IF;
может возникнуть фантомное чтение или грязное чтение. Чтобы исправить, можно:
использовать UPDATE ON CONFLICT
использовать SELECT ... FOR UPDATE
использовать другой уровень изоляции
Индекс - объект базы данных, предназначенные для ускорения поиска и выборки строк из таблицы. Помимо этого индексы могут служить для поддержания некоторых ограничений целостности
Индекс устанавливает соответствие между ключом (например, значением проиндексированного столбца) и строками таблицы, в которых этот ключ встречается. Строки идентифицируются с помощью TID (Tuple ID), который состоит из номера блока файла и позиции строки внутри блока
Несмотря на то, что индекс ускоряет поиск записи внутри таблицы (а также выборку, сортировку, агрегирование), изменение записей (а также создание новой, удаление и другие) в таблице требует перестройку индекса
Индекс может быть реализован как:
B-дерево
Хеш-таблица
GiST (Generalized Search Tree)
BRIN (Block Range INdex)
Хеш-функция - функция, преобразующая массив входных данных произвольного размера в выходную битовую строку определённого (установленного) размера в соответствии с определённым алгоритмом
Так как хеш-функция может дать все, что угодно, то единственная операция, которую поддерживает хеш-индекс, - поиск по условию равенства
Хеш-индекс устроен так:
Изначально есть одна корзина, в которую складыываются все кортежи
После того, как она достигнет определенного размера, она делится на две части: в первую складываются кортежи с хеш-ключом, оканчивающимся на 0, во вторую на 1
По мере их увеличения они снова делятся пополам, и дальше смотрятся последние 2 бита и так далее
Элементы корзин упорядочены по хеш-кодам ключей, а подходящие идентификаторы эффективно находятся двоичным поиском
До версии PostgreSQL 10 хеш-индексы не журналировались
Недостатки: кластеризация таблицы по хешу нет, упорядочить кортежи нельзя, а также не работает с null-атрибутами
B-дерево - сбалансированное дерево поиска
B-деревом называется дерево, удовлетворяющее следующим свойствам:
1
до 2t-1
ключей. Любой другой узел содержит от t-1
до 2t-1
ключей.
Листья не являются исключением из этого правила. Здесь t
- параметр дерева, не меньший 2
(и обычно принимающий значения от 50
до 2000
).У листьев потомков нет. Любой другой узел, содержащий ключи K_1
, …, K_n
, содержит n+1
потомков. При этом
(-∞,K_1)
2 ≤ i ≤ n
, i
-й потомок и все его потомки содержат ключи из интервала (K_{i-1},K_i)
,(n+1)
-й потомок и все его потомки содержат ключи из интервала (K_n, ∞)
Каждый узел дерева содержит несколько элементов, состоящих из ключа индексирования и ссылки
B-дерево является очень распространенным
GiST - фреймворк, который позволяет создавать пользовательские реализации для различных видов данных
Например: для географических данных, полнотекстового поиска, интервалов, деревьев
GiST реализует B-дерево, R-дерево и другие деревья
Идея R-дерева состоит в том, что плоскость разбивается на прямоугольники, которые в сумме покрывают все индексируемые точки
Индексная запись хранит ограничивающий прямоугольник, а предикат можно сформулировать так: точка лежит внутри данного ограничивающего прямоугольника
Также GiST используется для выбора из набора документов те, которые соответствуют поисковому запросу (то есть полнотекстовый поиск, Full Text Search). Чтобы полнотекстовый поиск работал быстро, его нужно поддержать индексом. Поскольку индексируются не сами документы, а значения типа tsvector, есть два варианта: построить индекс по выражению, с приведением типа, или создать отдельный столбец типа tsvector и индексировать его
GIN — это обобщённый инвертированный индекс. Он строит обратную карту: ключ → список документов (или позиций). Применяется там, где у одной записи много значений, каждое из которых нужно индексировать. GIN используется для полнотекстового поиска, индексации массивов, индексации JSONB