Материал учебного циклаОткрытый материал

PostgreSQL: как устроена реляционная база данных

Введение в PostgreSQL: сервер и клиент, таблицы, ключи, связи, SQL, ограничения, JOIN, транзакции, индексы, JSONB, роли, резервные копии и подключение из FastAPI.

Инфраструктура и DevOps #FastAPI #PostgreSQL #SQL #базы данных #индексы #транзакции
Учебный цикл Основы API и backend-разработки Материал 5 из 8

PostgreSQL: как устроена реляционная база данных

Приложению нужно хранить данные дольше, чем работает один процесс Python.

Переменная:

orders = []

существует только в памяти программы. После перезапуска список исчезнет. Если приложение запущено в нескольких процессах, у каждого будет собственная копия.

Файл сохраняет данные, но быстро появляются дополнительные требования:

  • найти все заказы конкретного пользователя;
  • одновременно изменить несколько связанных записей;
  • не допустить повторяющийся email;
  • безопасно работать из нескольких процессов;
  • восстановиться после сбоя;
  • ограничить права доступа;
  • сделать резервную копию.

Эти задачи решает система управления базами данных.

PostgreSQL - объектно-реляционная система управления базами данных. Она хранит данные, выполняет SQL-запросы, контролирует целостность и координирует параллельную работу клиентов.

Приложение
↓
SQL-запрос
↓
PostgreSQL
↓
данные и результат

PostgreSQL не является просто файлом с таблицами. Это отдельный серверный процесс со своей памятью, журналом изменений, системой транзакций, планировщиком запросов и механизмами доступа.


База данных, СУБД и SQL

Эти понятия связаны, но не совпадают.

База данных

База данных - организованный набор данных.

Например:

пользователи
заказы
товары
платежи

СУБД

СУБД управляет данными:

  • сохраняет;
  • читает;
  • изменяет;
  • проверяет ограничения;
  • обслуживает одновременные запросы;
  • ведёт журнал;
  • управляет правами.

PostgreSQL является СУБД.

SQL

SQL - язык запросов к реляционным базам данных.

SELECT id, total
FROM orders
WHERE user_id = 42
ORDER BY created_at DESC;

Разработчик указывает, какие строки нужны. PostgreSQL сам выбирает план выполнения.


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

PostgreSQL работает по клиент-серверной модели.

Сервер:

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

Клиентом может быть:

  • psql;
  • FastAPI-приложение;
  • скрипт Python;
  • панель администрирования;
  • аналитический инструмент.

Строка подключения обычно выглядит так:

postgresql://user:password@host:5432/database
user - роль PostgreSQL

host - адрес сервера

5432 - стандартный порт

database - база, к которой подключается клиент

Один работающий экземпляр PostgreSQL может управлять несколькими базами данных.


Сущности, атрибуты , кортежиб домены и отношения

Перед проектированием таблиц нужно определить, какие объекты предметной области должна хранить база данных.

Сущности - Отношения

Такой объект называют сущностью. Сущностью может быть пользователь, заказ, товар, платёж или учебный курс.

У сущности есть атрибуты - свойства, которыми она описывается.

Сущность «пользователь» - id, имя, email, телефон
Сущность «заказ» - id, дата создания, сумма, статус
Сущность «товар» - id, название, цена

В реляционной модели каждая сущность представляется отношением. На практике отношение реализуется в виде таблицы.

Например, сущность «пользователь» представляется отношением users, а каждый конкретный пользователь - отдельной строкой этой таблицы.

users - отношение, представляющее пользователей
id, name, email - атрибуты отношения
одна строка users - один экземпляр сущности «пользователь»

Одна таблица должна описывать одну сущность или одну самостоятельную связь между сущностями. Если в одной таблице смешиваются пользователи, заказы и товары, одни и те же факты начинают повторяться, а операции добавления, обновления и удаления могут приводить к аномалиям.

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

Атрибуты - Столбцы

Атрибут описывает свойство объекта:

id
name
email
created_at

У каждого атрибута есть домен и ограничения.

Кортежи - Строки

Кортеж представляет экземпляр:

42 | Сергей | user@example.com | 2026-08-01 12:00:00+00

Домены - Типы данных

Тип ограничивает допустимые значения и определяет доступные операции.

Распространённые типы:

integer, bigint - целые числа

numeric - точные десятичные значения

text, varchar - строки

boolean - true / false

date, timestamp, timestamptz - дата и время

uuid - идентификатор

jsonb - структурированный JSON

array - массив значений

Для денег часто используют numeric, если нужна точная десятичная арифметика.

Для событий обычно удобен timestamptz, поскольку он представляет конкретный момент времени.

Правильный тип помогает базе:

  • проверять значения;
  • хранить их эффективно;
  • выбирать операторы;
  • строить индексы;
  • предотвращать ошибки.

Ограничения

Ограничения переносят часть правил целостности на уровень базы.

NOT NULL

email text NOT NULL

UNIQUE

email text UNIQUE

CHECK

price numeric CHECK (price > 0)

FOREIGN KEY

Проверяет существование связанной строки.

Почему недостаточно проверки в Python:

два процесса одновременно проверили email
-
оба решили, что значение свободно
-
оба попытались вставить строку

Уникальное ограничение разрешает такую гонку внутри базы: одна операция проходит, другая получает конфликт.

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


Ключи таблицы

Ключ - атрибут или набор атрибутов, по которым можно однозначно определить строку таблицы.

Например, у пользователя могут быть следующие атрибуты:

id
email
phone
passport_number

Некоторые из них потенциально способны однозначно идентифицировать пользователя. Но не каждый уникальный на текущем наборе данных столбец автоматически является хорошим ключом.

Например, имена пользователей могут случайно не повторяться:

Сергей
Анна
Иван

Но это не делает name ключом, поскольку два пользователя могут иметь одинаковое имя.

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

Суперключ

Суперключ - любой набор атрибутов, значения которого однозначно определяют строку.

Например:

id
id + email
id + email + name

Если id уже уникален, то любой набор, содержащий id, тоже будет уникальным.

Но дополнительные атрибуты в таком наборе избыточны.

Потенциальный ключ

Потенциальный ключ, или candidate key, - минимальный суперключ.

Минимальный означает, что из него нельзя удалить атрибут, сохранив однозначную идентификацию строки.

Для таблицы пользователей потенциальными ключами могут быть:

id
email
passport_series + passport_number

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

Потенциальных ключей может быть несколько.

Например:

id - внутренний идентификатор
email - уникальный адрес пользователя

Оба значения могут однозначно определять строку, если это гарантируется правилами системы.

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

Первичный ключ - один из потенциальных ключей, выбранный как основной идентификатор строки.

CREATE TABLE users (
    id bigint GENERATED ALWAYS AS IDENTITY,
    email text NOT NULL,

    PRIMARY KEY (id),
    UNIQUE (email)
);

В этом примере:

id - первичный ключ
email - альтернативный потенциальный ключ

Ограничение PRIMARY KEY закрепляет выбор на уровне базы данных.

PostgreSQL требует, чтобы значения первичного ключа:

  • не были NULL;
  • не повторялись.

Для проверки этого PostgreSQL автоматически создаёт уникальный индекс. ([PostgreSQL][1])

Но наличие ограничения ещё не означает, что ключ хорошо выбран с точки зрения предметной области.

Например, технически можно написать:

PRIMARY KEY (name)

Однако это будет плохой моделью, если имена могут повторяться или изменяться.

То есть нужно разделять два вопроса:

Логический вопрос - действительно ли атрибут идентифицирует объект
Технический вопрос - обеспечивает ли база уникальность и отсутствие NULL

Альтернативный ключ

Потенциальные ключи, которые не были выбраны первичными, называют альтернативными.

Обычно они закрепляются ограничением UNIQUE.

CREATE TABLE users (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    email text NOT NULL UNIQUE
);

Здесь email остаётся потенциальным ключом, хотя первичным выбран id.

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

Первичный ключ может состоять из нескольких столбцов.

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

CREATE TABLE enrollments (
    student_id bigint NOT NULL,
    course_id bigint NOT NULL,
    enrolled_at timestamptz NOT NULL DEFAULT now(),

    PRIMARY KEY (student_id, course_id)
);

Здесь ни student_id, ни course_id по отдельности не идентифицируют строку:

student_id - один студент может проходить несколько курсов
course_id - на одном курсе может быть несколько студентов
student_id + course_id - однозначно определяют запись

Такой ключ называется составным.

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

Внешний ключ

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

Пусть есть таблицы пользователей и заказов:

CREATE TABLE users (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name text NOT NULL
);
CREATE TABLE orders (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id bigint NOT NULL,
    total numeric(12, 2) NOT NULL,

    FOREIGN KEY (user_id)
        REFERENCES users(id)
);

Здесь:

users.id - referenced key, ключ, на который ссылаются
orders.user_id - foreign key, внешний ключ

Важно:

user_id является внешним ключом не сам по себе, а потому что для него объявлено ограничение FOREIGN KEY.

Без ограничения база технически позволила бы записать:

user_id = 999999

даже если такого пользователя не существует.


Аномалии данных

Если структура таблиц спроектирована неудачно, обычные операции с данными могут приводить к противоречиям. Такие проблемы называют аномалиями.

Основные виды:

Аномалия вставки - данные нельзя добавить без другой, ещё не существующей информации
Аномалия обновления - одно логическое значение приходится изменять в нескольких строках
Аномалия удаления - удаление одной строки приводит к потере другой полезной информации

Аномалия вставки

Аномалия вставки возникает, когда новый факт нельзя сохранить независимо.

Например, если данные о пользователе и его заказе находятся в одной таблице, добавить пользователя без заказа может оказаться невозможно.

Проблема появляется потому, что одна строка одновременно описывает несколько разных сущностей.

Аномалия обновления

Аномалия обновления возникает, когда одно значение хранится в нескольких местах.

Например, имя пользователя повторяется во всех его заказах. После изменения имени нужно обновить каждую связанную строку.

Если обновилась только часть строк, база начинает содержать противоречивые данные.

Аномалия удаления

Аномалия удаления возникает, когда удаление одного факта случайно удаляет другой.

Например, если информация о пользователе хранится только вместе с его заказами, удаление последнего заказа может привести к потере данных самого пользователя.

Эти аномалии обычно уменьшают разделением сущностей по отдельным таблицам и установлением связей между ними.

users - данные пользователей
orders - данные заказов
user_id - связь заказа с пользователем

Так пользователь хранится один раз, а заказы только ссылаются на него.


Нормализация базы данных

Нормализация - это способ организовать данные в реляционной базе так, чтобы уменьшить дублирование, избежать противоречий и упростить обновление информации.

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

Например:

order_id client_name client_phone product_1 product_2 product_3
101 Анна +79990000001 Ноутбук Мышь NULL
102 Анна +79990000001 Клавиатура NULL NULL

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

Нормализация помогает разделить данные на связанные таблицы:

  • clients - клиенты;
  • orders - заказы;
  • products - товары;
  • order_items - товары внутри заказов.

При этом таблицы связываются с помощью идентификаторов и внешних ключей.

Первая нормальная форма - 1НФ

Таблица находится в первой нормальной форме, если:

  • каждая ячейка содержит одно значение;
  • в таблице нет повторяющихся групп столбцов;
  • каждую строку можно однозначно определить.

Структура с полями product_1, product_2 и product_3 нарушает 1НФ. Количество товаров в заказе заранее неизвестно, поэтому добавлять отдельный столбец для каждого товара нельзя.

Неправильный вариант:

order_id products
101 Ноутбук, Мышь, Клавиатура

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

После приведения к 1НФ каждый товар занимает отдельную строку:

order_id product_id quantity
101 15 1
101 27 1
101 31 2

Теперь можно получить все товары заказа, посчитать их количество или найти заказы с конкретным товаром обычным SQL-запросом.

Важно понимать, что значение должно быть атомарным относительно задачи. Например, полный адрес можно хранить одной строкой, если приложение всегда отображает его целиком. Если нужно отдельно фильтровать данные по городу, улице или индексу, адрес стоит разделить на соответствующие поля.

Вторая нормальная форма - 2НФ

Таблица находится во второй нормальной форме, если:

  1. она уже соответствует 1НФ;
  2. каждый неключевой столбец зависит от всего составного первичного ключа, а не только от его части.

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

Рассмотрим таблицу товаров в заказах:

order_id product_id order_date product_name quantity
101 15 2026-08-02 Ноутбук 1
101 27 2026-08-02 Мышь 2

Предположим, что строка определяется парой:

order_id + product_id

Однако отдельные поля зависят только от части этого ключа:

  • order_date зависит только от order_id;
  • product_name зависит только от product_id;
  • quantity зависит от сочетания order_id и product_id.

Получается, что в одной таблице смешаны данные заказа, товара и позиции заказа.

Для приведения к 2НФ таблицу следует разделить.

Таблица заказов:

order_id order_date
101 2026-08-02

Таблица товаров:

product_id product_name
15 Ноутбук
27 Мышь

Таблица позиций заказа:

order_id product_id quantity
101 15 1
101 27 2

Теперь каждое поле хранится рядом с той сущностью, от которой оно действительно зависит.

Если таблица использует простой первичный ключ из одного столбца, частичной зависимости от ключа возникнуть не может. Поэтому нарушение 2НФ обычно рассматривают именно в таблицах с составными ключами.

Третья нормальная форма - 3НФ

Таблица находится в третьей нормальной форме, если:

  1. она соответствует 2НФ;
  2. неключевые поля не зависят друг от друга.

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

Рассмотрим таблицу клиентов:

client_id client_name city_id city_name
1 Анна 77 Москва
2 Иван 78 Санкт-Петербург
3 Мария 77 Москва

Первичный ключ здесь - client_id.

Поле city_id зависит от клиента, но city_name зависит уже не от клиента, а от city_id:

client_id → city_id → city_name

Такая зависимость называется транзитивной.

Из-за нее название города повторяется в каждой строке. Если потребуется переименовать город или исправить ошибку, придется обновлять множество записей.

Для приведения таблицы к 3НФ данные о городах выносятся отдельно.

Таблица клиентов:

client_id client_name city_id
1 Анна 77
2 Иван 78
3 Мария 77

Таблица городов:

city_id city_name
77 Москва
78 Санкт-Петербург

Теперь название города хранится в одном месте, а таблица клиентов содержит только ссылку на соответствующую запись.

Какие проблемы решает нормализация

Правильно нормализованная структура защищает базу от нескольких типов аномалий.

Аномалия обновления

Когда одно значение хранится в нескольких строках, его приходится изменять сразу во всех местах.

Например, если телефон клиента записан в каждом заказе, часть строк может остаться со старым номером.

Аномалия добавления

Иногда невозможно добавить одну сущность без другой.

Например, в общей таблице заказов и товаров нельзя сохранить новый товар, пока его никто не заказал.

Аномалия удаления

Удаление одной записи может случайно уничтожить другую информацию.

Например, если данные о клиенте существуют только внутри заказов, удаление последнего заказа приведет к потере информации о самом клиенте.

После нормализации клиенты, заказы и товары существуют независимо друг от друга.

Нормализация не означает максимальное количество таблиц

Цель нормализации заключается не в том, чтобы разбить базу на как можно больше частей. Каждая таблица должна представлять понятную сущность или связь между сущностями.

Для большинства прикладных систем достаточно привести структуру к третьей нормальной форме:

  • 1НФ устраняет списки и повторяющиеся группы внутри строки;
  • 2НФ устраняет зависимости от части составного ключа;
  • 3НФ устраняет зависимости неключевых полей друг от друга.

Более высокие формы тоже существуют:

  • нормальная форма Бойса - Кодда, или BCNF, усиливает требования 3НФ для сложных функциональных зависимостей;
  • 4НФ устраняет многозначные зависимости;
  • 5НФ работает со сложными зависимостями соединения таблиц;
  • 6НФ применяется редко, преимущественно в специализированных и темпоральных базах данных.

В обычных CRM, интернет-магазинах, внутренних сервисах и системах автоматизации чаще всего достаточно корректной 3НФ. Более высокие формы становятся важны, когда структура данных содержит сложные отношения, которые нельзя безопасно выразить обычным разделением сущностей.

Когда допустима денормализация

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

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

SELECT SUM(quantity * price)
FROM order_items
WHERE order_id = 101;

Но в системе с большим количеством запросов готовое значение total_amount иногда дополнительно сохраняют в таблице orders.

Это может ускорить чтение, но создает новую обязанность: итоговая сумма должна обновляться при любом изменении состава заказа.

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


Отношения между таблицами

Теперь типы отношений можно связать с ключами.

Один к одному

Одна строка первой таблицы соответствует не более чем одной строке второй.

CREATE TABLE user_profiles (
    user_id bigint PRIMARY KEY,

    FOREIGN KEY (user_id)
        REFERENCES users(id)
);

user_id одновременно:

  • первичный ключ user_profiles;
  • внешний ключ на users.

Уникальность не позволяет создать два профиля для одного пользователя.

Один ко многим

Один пользователь может иметь много заказов.

orders.user_id

не является уникальным:

order 1 - user_id 42
order 2 - user_id 42
order 3 - user_id 42

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

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

Заказ содержит много товаров, а товар может входить во многие заказы.

Используется промежуточная таблица:

CREATE TABLE order_items (
    order_id bigint NOT NULL,
    product_id bigint NOT NULL,
    quantity integer NOT NULL,

    PRIMARY KEY (order_id, product_id),

    FOREIGN KEY (order_id)
        REFERENCES orders(id),

    FOREIGN KEY (product_id)
        REFERENCES products(id)
);

Здесь составной первичный ключ:

order_id + product_id

не позволяет дважды добавить одну и ту же пару заказа и товара.


Ссылочная целостность

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

Ссылочная целостность означает, что значение внешнего ключа должно соответствовать существующей строке связанной таблицы либо быть NULL, если это разрешено моделью.

Нарушение возникает, когда дочерняя строка ссылается на несуществующую родительскую строку.

orders.user_id = 42
users.id = 42 отсутствует

Такая ссылка называется висячей. Связанную строку также могут называть orphan record.

Ограничение FOREIGN KEY не позволяет:

  • создать ссылку на несуществующую строку;
  • изменить внешний ключ на недопустимое значение;
  • удалить или изменить родительскую строку так, чтобы нарушилась связь.

Но при удалении или изменении родительской строки база должна знать, что делать с зависимыми строками. Это задаётся ссылочными действиями.


Действия при удалении и обновлении

Правила задаются через:

ON DELETE ...
ON UPDATE ...

Основные варианты:

NO ACTION - не выполнять автоматическое действие и проверить целостность ограничения
RESTRICT - сразу запретить операцию при наличии зависимых строк
CASCADE - распространить удаление или изменение на связанные строки
SET NULL - установить внешний ключ в NULL
SET DEFAULT - установить значение внешнего ключа по умолчанию

NO ACTION

Операция не выполняет автоматических изменений зависимых строк.

Если после неё нарушается ссылочная целостность, PostgreSQL отклоняет операцию.

Это стандартное поведение по умолчанию.

RESTRICT

Запрещает удаление или изменение родительской строки, если на неё существуют ссылки.

RESTRICT близок к NO ACTION, но проверка выполняется сразу и не может быть отложена до конца транзакции.

CASCADE

Автоматически распространяет операцию на зависимые строки.

ON DELETE CASCADE - удалить связанные строки
ON UPDATE CASCADE - обновить значения внешних ключей

Каскад подходит, когда дочерняя запись не имеет самостоятельного смысла без родительской.

SET NULL

При удалении или изменении родительской строки внешний ключ получает NULL.

Это возможно только тогда, когда столбец допускает NULL.

SET DEFAULT

Внешний ключ получает значение по умолчанию.

При этом новое значение всё равно должно соответствовать правилам внешнего ключа.


Основные SQL-операции

INSERT

INSERT INTO users (name, email)
VALUES ('Сергей', 'user@example.com')
RETURNING id, name, email;

SELECT

SELECT id, name, email
FROM users
WHERE id = 42;

UPDATE

UPDATE users
SET name = 'Sergey'
WHERE id = 42
RETURNING *;

DELETE

DELETE FROM users
WHERE id = 42;

Без WHERE изменение затронет все строки.


JOIN

Данные часто разделены между таблицами.

SELECT
    orders.id,
    orders.total,
    users.name
FROM orders
JOIN users
    ON users.id = orders.user_id;

Основные варианты:

INNER JOIN - только совпавшие строки

LEFT JOIN - все строки слева и совпадения справа

Реляционная модель не требует хранить имя пользователя в каждом заказе. Заказ хранит user_id, а актуальные данные получаются соединением.


Агрегация

SELECT
    user_id,
    COUNT(*) AS orders_count,
    SUM(total) AS total_amount,
    AVG(total) AS average_order
FROM orders
GROUP BY user_id;

GROUP BY формирует группы строк.

WHERE фильтрует строки до группировки. HAVING фильтрует уже сформированные группы.

HAVING COUNT(*) >= 5

Транзакции

Транзакция объединяет несколько операций в одну логическую единицу.

Перевод между счетами:

  1. Списать сумму с первого счёта.
  2. Начислить сумму второму.
  3. Зафиксировать оба изменения вместе.
BEGIN;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;

UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;

COMMIT;

Если возникает ошибка:

ROLLBACK;

Свойства транзакций часто описывают как ACID:

Atomicity - все операции или ни одной

Consistency - ограничения остаются выполненными

Isolation - параллельные транзакции не создают некорректный результат

Durability - после COMMIT результат должен пережить сбой

Индексы

CREATE INDEX idx_orders_user_id
ON orders(user_id);

Индекс хранит дополнительную структуру, которая помогает быстрее находить строки.

Распространённые типы:

  • B-tree;
  • GIN;
  • GiST;
  • BRIN.

B-tree подходит для большинства сравнений и сортировок.

GIN часто применяют для массивов, полнотекстового поиска и jsonb.

Индекс имеет цену:

  • занимает место;
  • замедляет запись;
  • требует обслуживания;
  • может не использоваться планировщиком.

Поэтому индекс создают под реальные запросы, а не на каждый столбец.


Составные и частичные индексы

CREATE INDEX idx_orders_user_created
ON orders(user_id, created_at DESC);

Такой индекс подходит для:

WHERE user_id = 42
ORDER BY created_at DESC

Порядок столбцов важен.

Частичный индекс:

CREATE INDEX idx_active_orders
ON orders(created_at)
WHERE status = 'active';

Он содержит только строки, удовлетворяющие условию.


EXPLAIN

SQL описывает результат, а планировщик выбирает способ выполнения.

EXPLAIN
SELECT *
FROM orders
WHERE user_id = 42;

План может содержать:

Seq Scan
Index Scan
Nested Loop
Hash Join
Merge Join

EXPLAIN ANALYZE реально выполняет запрос и показывает фактические показатели:

EXPLAIN (ANALYZE, BUFFERS)
SELECT ...

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


JSONB

PostgreSQL умеет хранить JSON.

CREATE TABLE events (
    id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    event_type text NOT NULL,
    payload jsonb NOT NULL
);

Запрос:

SELECT *
FROM events
WHERE payload->>'order_id' = 'order_1045';

jsonb полезен для:

  • интеграционных payload;
  • метаданных;
  • данных с изменяемым набором полей.

Но критичные поля для связей, ограничений, сортировки и частого поиска обычно лучше хранить отдельными столбцами.


Роли и права

PostgreSQL использует роли.

CREATE ROLE app_user
LOGIN
PASSWORD 'secret';

Права выдаются явно:

GRANT CONNECT ON DATABASE app TO app_user;
GRANT USAGE ON SCHEMA public TO app_user;
GRANT SELECT, INSERT, UPDATE ON orders TO app_user;

Приложение не должно без необходимости подключаться как суперпользователь.


PostgreSQL и FastAPI

FastAPI не содержит встроенного драйвера базы данных.

Используют:

  • psycopg;
  • asyncpg;
  • SQLAlchemy;
  • SQLModel.

Типовая архитектура:

FastAPI endpoint
↓
service
↓
repository
↓
драйвер или ORM
↓
PostgreSQL

Приложение обычно использует пул соединений.

Схема базы должна изменяться через миграции. В Python-проектах с SQLAlchemy часто используют Alembic.


Когда PostgreSQL подходит

PostgreSQL особенно полезен, если нужны:

  • связанные данные;
  • транзакции;
  • ограничения целостности;
  • JOIN и агрегации;
  • несколько одновременных клиентов;
  • надёжное долговременное хранение;
  • JSON рядом с реляционными полями.

Примеры:

  • CRM;
  • интернет-магазин;
  • система заказов;
  • платежи;
  • backend FastAPI;
  • аналитические витрины.

Типичные ошибки

  • хранить всё в одной таблице;
  • не создавать ограничения;
  • индексировать каждый столбец;
  • выполнять запросы по одному в цикле;
  • держать транзакцию открытой слишком долго;
  • использовать SELECT * без необходимости;
  • подключаться суперпользователем;
  • не проверять восстановление backup;
  • менять production-схему вручную без миграций.

Итог

PostgreSQL - серверная реляционная СУБД, которая хранит данные и обеспечивает их согласованность.

database - база внутри СУБД

schema - пространство имён

table - отношение строк и столбцов

primary key - идентификатор строки

foreign key - внешняя связь между таблицами

transaction - единая группа изменений

index - структура для ускорения доступа

role - пользователь и права

После PostgreSQL логично изучить Redis, который решает задачи быстрого временного хранения, кэша, счётчиков и событий. Затем обе системы можно объединить с FastAPI через Docker Compose.

Продолжить

  1. 01 Redis: как работает быстрое хранилище данных в памяти Введение в Redis: ключи, структуры данных, TTL, кэширование, eviction, транзакции, Pub/Sub, Streams, persistence, репликация и подключение к FastAPI.
  2. 02 Docker: как контейнеризировать приложение и управлять его окружением Подробное введение в Docker: образы, контейнеры, Dockerfile, слои, порты, volumes, networks, Compose, health checks, безопасность, отладка и контейнеризация FastAPI.

Обратная связь

Материал был полезен?

Нашли ошибку или неточность? Сообщить →