Каждый, кто хоть раз разбирался в три часа ночи с упавшим продом, знает: большинство катастроф в базах данных это не сбой железа и не космические лучи. Это решения, принятые на этапе проектирования схемы. «Потом поправим», «в приложении проверим», «а зачем тут индекс?» каждая из этих фраз обходилась командам в часы даунтайма и миллионы потерянных строк.
Ниже 25 правил, которые я собрал из опыта работы с высоконагруженными системами. Это не теория из учебника — это грабли, на которые уже наступили до вас. Каждое правило сопровождается примером «как надо» и «как не надо», чтобы разница была наглядной.
I. Фундамент схемы
1. Всегда используйте суррогатный первичный ключ
Критичность: максимальная
Натуральные ключи (email, ИНН, username) меняются. Когда это произойдёт, вам предстоит каскадное обновление миллионов строк и всех FK-ссылок. Суррогатные ключи (BIGSERIAL / UUID) не меняются никогда.
-- ✅ Правильно CREATE TABLE users ( id BIGSERIAL PRIMARY KEY, email TEXT NOT NULL UNIQUE, created_at TIMESTAMPTZ DEFAULT NOW() ); -- ❌ Неправильно CREATE TABLE users ( email TEXT PRIMARY KEY -- катастрофа, ждущая своего часа );
2. Каждая таблица ОБЯЗАНА иметь created_at и updated_at
Критичность: максимальная
Без таймстемпов вы не сможете отлаживать продовые инциденты, строить аудит-трейл или делать инкрементальный ETL. Используйте TIMESTAMPTZ (с временной зоной), а не TIMESTAMP.
-- ✅ Правильно created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(), updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW() -- ❌ Неправильно -- Никаких колонок с временными метками вообще -- Или TIMESTAMP без временной зоны
3. Используйте TIMESTAMPTZ, а не TIMESTAMP
Критичность: высокая
TIMESTAMP молча отбрасывает информацию о часовом поясе. Когда ваши серверы приложений работают в разных часовых поясах, сохранённое время становится двусмысленным и неконсистентным. TIMESTAMPTZ хранит всё в UTC внутри.
-- ✅ Правильно event_time TIMESTAMPTZ NOT NULL -- ❌ Неправильно event_time TIMESTAMP -- часовой пояс потерян навсегда
4. Используйте TEXT вместо VARCHAR(n)
Критичность: обычная
В PostgreSQL TEXT и VARCHAR дают идентичную производительность. VARCHAR(n) лишь добавляет CHECK-ограничение, которое придётся мигрировать, когда требования изменятся. Для реальной валидации используйте CHECK.
-- ✅ Правильно name TEXT NOT NULL, CONSTRAINT chk_name_len CHECK(length(name) <= 255) -- ❌ Неправильно name VARCHAR(255) -- придётся менять при изменении требований
5. Используйте BIGINT / BIGSERIAL для ID, а не INT
Критичность: высокая
INT имеет максимум ~2.1 миллиарда. Такие компании, как Slack и Digg, уже упирались в этот потолок. BIGINT стоит всего на 4 байта больше на строку, но вмещает 9.2 квинтиллиона значений.
-- ✅ Правильно id BIGSERIAL PRIMARY KEY -- ❌ Неправильно id SERIAL PRIMARY KEY -- бомба замедленного действия
II. Связи и внешние ключи
6. ВСЕГДА определяйте явные внешние ключи
Критичность: максимальная
Без FK-ограничений «осиротевшие» записи будут молча накапливаться. Логика приложения — не замена: краши, баги и race condition будут создавать несогласованность.
-- ✅ Правильно user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE -- ❌ Неправильно user_id BIGINT -- "мы будем проверять в коде приложения"
7. Выбирайте ON DELETE осознанно
Критичность: максимальная
По умолчанию стоит RESTRICT (блокирует удаление). CASCADE автоматически удаляет дочерние записи. SET NULL сохраняет строку. Неверный выбор либо блокирует операции, либо молча уничтожает данные.
-- ✅ Правильно -- Зависимые данные: CASCADE REFERENCES orders(id) ON DELETE CASCADE -- Необязательная ссылка: SET NULL REFERENCES users(id) ON DELETE SET NULL -- Критичные данные: RESTRICT (по умолчанию) REFERENCES accounts(id) -- блокирует удаление -- ❌ Неправильно -- Никогда не думать о поведении ON DELETE
8. Используйте таблицы связей (junction tables) для M:N
Критичность: максимальная
Никогда не используйте массивы или строки с разделителями-запятыми для связей «многие-ко-многим». Таблицы связей индексируемы, поддерживают запросы и могут хранить метаданные связи (например, роль, дату присвоения).
-- ✅ Правильно CREATE TABLE user_roles ( user_id BIGINT REFERENCES users(id), role_id BIGINT REFERENCES roles(id), granted_at TIMESTAMPTZ DEFAULT NOW(), PRIMARY KEY (user_id, role_id) ); -- ❌ Неправильно role_ids INTEGER[] -- нельзя JOIN, нельзя FK roles TEXT -- 'admin,editor' ?
9. Индексируйте каждую FK-колонку
Критичность: высокая
PostgreSQL НЕ создаёт индексы автоматически для FK (в отличие от MySQL). Без индекса каждое удаление/обновление родителя вызывает sequential scan дочерней таблицы, а это блокировка на уровне таблицы.
-- ✅ Правильно CREATE INDEX idx_orders_user_id ON orders(user_id); -- ❌ Неправильно -- FK без индекса = seq scan при JOIN
10. Предпочитайте мягкое удаление для критичных бизнес-данных
Критичность: высокая
Жёсткое удаление необратимо и ломает аудит-трейл. Добавьте колонку deleted_at и фильтруйте в запросах. Используйте partial-индексы для сохранения производительности.
-- ✅ Правильно deleted_at TIMESTAMPTZ DEFAULT NULL; -- Partial index: только активные строки CREATE INDEX idx_users_active ON users(email) WHERE deleted_at IS NULL; -- ❌ Неправильно DELETE FROM users WHERE id = 42; -- удалено навсегда, нет аудит-трейла
III. Нормализация и целостность данных
11. Нормализуйте до 3NF как минимум, денормализуйте осознанно
Критичность: максимальная
Начинайте с нормализации. Каждая денормализация — это осознанный компромисс: скорость чтения ценой сложности записи и рисков несогласованности. Документируйте, почему вы денормализовали.
-- ✅ Правильно: единый источник правды orders.user_id → users.id → users.email -- Денормализация ТОЛЬКО если замерено: -- "Добавил email в таблицу orders для -- отчёта по биллингу: запрос ускорился -- с 800мс до 12мс на 50M строк" -- ❌ Неправильно -- Дублирование user_name, user_email -- в каждой таблице "для удобства"
12. Используйте NOT NULL по умолчанию, NULL — только намеренно
Критичность: максимальная
NULL вводит трёхзначную логику. NULL != NULL, сравнения с NULL возвращают NULL, агрегаты молча пропускают NULL. Каждая nullable-колонка требует COALESCE повсюду.
-- ✅ Правильно status TEXT NOT NULL DEFAULT 'pending', deleted_at TIMESTAMPTZ -- NULL = намеренно -- ❌ Неправильно name TEXT, -- nullable случайно price NUMERIC -- NULL или 0? кто знает
13. Используйте CHECK-ограничения для валидации данных
Критичность: высокая
Валидация на уровне приложения обходится через миграции, скрипты и прямой доступ к БД. Ограничения в БД — это последний рубеж обороны, и они работают всегда.
-- ✅ Правильно CONSTRAINT chk_price_positive CHECK (price > 0), CONSTRAINT chk_status_valid CHECK (status IN ('active','inactive','suspended')) -- ❌ Неправильно -- "валидация обрабатывается в API" -- *в БД price = -500 и status = 'yolo'*
14. Используйте NUMERIC для денег, никогда FLOAT/DOUBLE
Критичность: максимальная
Арифметика с плавающей точкой: 0.1 + 0.2 = 0.30000000000000004. Для финансовых данных NUMERIC(precision, scale) даёт точную десятичную математику. Или храните копейки как BIGINT.
-- ✅ Правильно price NUMERIC(12,2) NOT NULL, balance NUMERIC(15,2) NOT NULL -- или: price_cents BIGINT NOT NULL -- ❌ Неправильно price FLOAT -- $0.30000000000000004 price DOUBLE PRECISION -- та же проблема
15. Используйте ENUM осторожно — предпочитайте CHECK или справочные таблицы
Критичность: обычная
PostgreSQL ENUM-ы нельзя легко изменить — можно добавить значения, но нельзя удалить или переименовать без пересоздания типа. CHECK-ограничения или справочные таблицы гораздо гибче.
-- ✅ Правильно: CHECK constraint status TEXT NOT NULL CHECK(status IN ('draft','published')) -- Или: справочная таблица для большого числа значений REFERENCES statuses(code) -- ❌ Неправильно CREATE TYPE status AS ENUM( 'draft','published' ); -- трудно изменить позже
IV. Индексирование и производительность
16. Создавайте индексы для каждого WHERE, JOIN и ORDER BY
Критичность: максимальная
Без индексов PostgreSQL делает sequential scan — читает каждую строку. На таблице в 100М строк это разница между 5 мс и 5 минутами.
-- ✅ Правильно -- Составной индекс для типичного запроса CREATE INDEX idx_orders_user_status ON orders(user_id, status) WHERE deleted_at IS NULL; -- ❌ Неправильно -- "Добавим индексы, когда станет медленно" -- (3 часа ночи, прод горит)
17. Используйте partial-индексы для индексации только нужных строк
Критичность: высокая
Если 95% строк имеют статус completed и вы запрашиваете только pending, полный индекс — пустая трата места. Partial-индексы компактнее, быстрее и экономят память.
-- ✅ Правильно CREATE INDEX idx_orders_pending ON orders(created_at) WHERE status = 'pending'; -- ❌ Неправильно CREATE INDEX idx_orders_created ON orders(created_at); -- индексирует ВСЕ 100M строк ради 5% запросов
18. Используйте EXPLAIN ANALYZE перед деплоем запросов
Критичность: высокая
Нельзя угадать производительность запроса. EXPLAIN ANALYZE показывает фактический план выполнения, оценки строк и время. Seq Scan на большой таблице = добавьте индекс.
-- ✅ Правильно EXPLAIN ANALYZE SELECT * FROM orders WHERE user_id = 42 AND status = 'pending'; -- ❌ Неправильно -- Деплоим запрос в прод -- "На деве с 10 строками вроде нормально"
19. Используйте пулинг соединений (PgBouncer)
Критичность: максимальная
Каждое соединение PostgreSQL стоит ~10 МБ RAM. 1000 прямых соединений = 10 ГБ только на коннекты. PgBouncer мультиплексирует соединения и обслуживает тысячи клиентов с минимальным оверхедом.
-- ✅ Правильно App → PgBouncer (порт 6432) → PostgreSQL pool_mode = transaction max_client_conn = 1000 default_pool_size = 20 -- ❌ Неправильно App → PostgreSQL (напрямую, 500 коннектов) -- OOM killer вступает в чат
V. Миграции и операционная работа
20. Никогда не меняйте колонки в продакшене без плана миграции
Критичность: максимальная
ALTER TABLE может заблокировать таблицу на часы при больших объёмах данных. Всегда: добавьте новую колонку → бэкфилл → переключите чтения → удалите старую. Никогда не переименовывайте «на месте».
-- ✅ Правильно -- Шаг 1: Добавляем новую колонку (мгновенно) ALTER TABLE users ADD COLUMN name_new TEXT; -- Шаг 2: Бэкфилл батчами -- Шаг 3: Переключаем приложение на name_new -- Шаг 4: Удаляем старую колонку -- ❌ Неправильно ALTER TABLE users RENAME COLUMN name TO full_name; -- приложение мгновенно ломается
21. UUID v7 — для распределённых систем, BIGSERIAL — для одного узла
Критичность: высокая
BIGSERIAL проще и компактнее (8 байт против 16). Но в распределённых системах / микросервисах UUID избавляет от координации. UUIDv7 сортируется по времени и дружелюбен к индексам (в отличие от v4).
-- ✅ Правильно -- Один инстанс PostgreSQL: id BIGSERIAL PRIMARY KEY -- Распределённая система / микросервисы: id UUID PRIMARY KEY DEFAULT gen_random_uuid() -- v4 -- Или генерируйте UUIDv7 на стороне приложения -- ❌ Неправильно -- UUID v4 как кластерный PK на огромных таблицах -- Рандомные вставки = постоянные page splits
22. Всегда используйте транзакции для многошаговых операций
Критичность: максимальная
Без явных транзакций каждый оператор автоматически коммитится. Если шаг 2 из 3 упадёт, у вас будут неполные данные. Оборачивайте связанные операции в BEGIN/COMMIT.
-- ✅ Правильно BEGIN; UPDATE accounts SET balance = balance - 100 WHERE id = 1; UPDATE accounts SET balance = balance + 100 WHERE id = 2; COMMIT; -- ❌ Неправильно UPDATE accounts SET balance = balance - 100 WHERE id = 1; -- крэш здесь = деньги испаряются UPDATE accounts SET balance = balance + 100 WHERE id = 2;
23. Партиционируйте большие таблицы (100M+ строк)
Критичность: высокая
Партиционируйте по времени (range) или по тенанту (list/hash). Запросы к одной партиции пропускают сканирование остальных. VACUUM и обслуживание индексов выполняются для каждой партиции отдельно.
-- ✅ Правильно CREATE TABLE events ( id BIGSERIAL, created_at TIMESTAMPTZ NOT NULL, payload JSONB ) PARTITION BY RANGE (created_at); CREATE TABLE events_2025_01 PARTITION OF events FOR VALUES FROM ('2025-01-01') TO ('2025-02-01'); -- ❌ Неправильно -- 500M строк в одной таблице -- VACUUM занимает 6 часов -- Каждый запрос = full table scan
24. Храните JSON в JSONB, а не в JSON или TEXT
Критичность: обычная
JSONB — бинарный формат, поддерживает индексы (GIN) и операторы включения (@>, ?). JSON — просто валидированный текст, парсится заново при каждом обращении. TEXT вообще не проверяет валидность.
-- ✅ Правильно metadata JSONB NOT NULL DEFAULT '{}'; CREATE INDEX idx_meta_gin ON products USING GIN(metadata); -- ❌ Неправильно metadata JSON -- перепарсивается при каждом чтении metadata TEXT -- никакой валидации
25. Используйте Row-Level Security (RLS) для мультитенантных приложений
Критичность: обычная
Баги в приложении могут привести к утечке данных тенанта. RLS обеспечивает изоляцию на уровне базы данных — даже SQL-инъекция не сможет пересечь границы тенанта.
-- ✅ Правильно ALTER TABLE documents ENABLE ROW LEVEL SECURITY; CREATE POLICY tenant_isolation ON documents USING (tenant_id = current_setting('app.tenant_id')); -- ❌ Неправильно -- WHERE tenant_id = ? в каждом запросе -- Один пропущенный WHERE = утечка данных
Шпаргалка по именованию
Быстрая справка по конвенциям, чтобы ваша схема была консистентной:
Таблицы - множественное число, snake_case: users, order_items. Никогда не в единственном числе и не camelCase.
Первичные ключи - всегда id с типом BIGSERIAL или UUID. Никогда не составные PK на бизнес-данных.
Внешние ключи - паттерн {singular_table}_id: user_id, order_id.
Индексы - паттерн idx_{table}_{columns}: idx_users_email.
Ограничения - паттерн chk_{table}_{desc} или uq_{table}_{cols}. Будьте явными.
Таймстемпы - каждая таблица получает created_at + updated_at. Всегда TIMESTAMPTZ, никогда TIMESTAMP.
Заключение
Эти 25 правил не догма, а концентрированный опыт команд, которые обслуживают базы с миллиардами строк. Каждое «неправильно» из списка выше это реальный инцидент, который кто-то пережил (часто в нерабочее время).
Начните с малого: пройдитесь по текущей схеме вашего проекта и проверьте хотя бы первые пять правил. Скорее всего, вы обнаружите хотя бы пару мин, которые ещё не взорвались. Лучше обезвредить их сейчас, чем разбираться на постмортеме.
Если статья была полезна сохраните шпаргалку и поделитесь с командой. Меньше граблей - больше сна.
Комментарии (153)

shurutov
14.02.2026 12:05Каждая таблица ОБЯЗАНА иметь created_at и updated_at
Крайне спорное утверждение. И да, когда подобные требования вытекают из необходимости аудита, то автоматически подтягивается требование указывать, кто именно и на каком основании вносил соответствующие изменения. С учётом того, что, как правило, в любом случае все подключения идут из-под специальной ТЕХНИЧЕСКОЙ учётной записи, то удовлетворение этого требования (указания, кто вносил изменения) становится не совсем тривиальной задачей.
Используйте TEXT вместо VARCHAR(n)
Как-то неубедительно. Вот здесь: https://ru-postgres.livejournal.com/65930.html развёрнуто;
Используйте BIGINT / BIGSERIAL для ID, а не INT
Если против типа (
BIGINT) никаких возражений нет и неизвестно, то вот использование*SERIALвызывает лютейшую боль и страдания, т.к., начиная с 10-й версии, для указания первичного ключа служит более другая конструкция:CREATE TABLE mylib ( id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, ...Используйте EXPLAIN ANALYZE перед деплоем запросов
Работает, если у вас в среде нагрузочного тестирования ресурсы под БД и размеры БД таковы, что планы запросов и там (в тесте), и там (в проде) будут идентичными.
Это вот навскидку, после просмотра по-диагонали. Подумаю, может ещё чего придумаю.

tojiboyevumidjon Автор
14.02.2026 12:05Согласен, спасибо за комментарий! Вы правы насчёт спорности утверждения. Да, в случаях когда требования аудита вытекают из необходимости, то автоматически подтягивается требование указывать, кто и на каком основании вносил изменения и здесь одних таймстемпов недостаточно. Прошу прощения, что не уточнил эти детали и не описал более сложные кейсы с учётной записью и техническими ограничениями. Ваше дополнение делает картину более полной!

iamkisly
14.02.2026 12:05Не согласен с мнением по поводу created_at и updated_at. У нас правда SQL server, а не PG, но мы пришли к тому же мнению. У нас суммарно 4к таблиц в разных базах, и часть из них это витрины данных. Без дат невозможно понять насколько данные актуальны. Их вставили 15 минут назад или 5 лет назад.. я не утрирую, это был реальный вопрос к самому себе когда я делал аудит.

TimReset
14.02.2026 12:05Крайне спорное утверждение.
Вообще ни разу не спорное. И я бы ещё добавил что лучше хранить историю изменений таблицы, для доступа к последней версии записи ввести что-то вроде is_last.
Потому что когда на проде что-то пройдёт не так и к тебе придут с вопросом, а почему тут такие значения, то гораздо удобнее ответ получить из селекта. Где будет по шагам расписано что именно кто и когда менял, чем ходить по логам и искать что там вообще было.
Логи в любом случае нужны, но из базы это всё удобнее получить и более наглядно.
И так как мы в будущее плохо смотрим, то я выбрал простую стратегию - всегда пишем, а если как то это будет негативно влиять (ни разу с этим не сталкивался, но всё же) то явно конкретно в этом месте не используем.
Но вообще всё зависит от бизнеса. В банках важно, кто когда что-то поменял (с них потом спросят), поэтому тут лучше всё писать. Где нибудь в pet project - наверное нет.

vadim_bv
14.02.2026 12:05Для отслеживания изменений создаются таблицы логов/истории как копия таблицы + технические поля. Они не нужны примерно никогда (проверено на опыте), так что зачем утяжелять структуру / кол-во строк в исходной таблице?
Запись идет либо сразу в две таблицы, либо на основную (если изменения достаточно редки) вешается триггер, пишущий в таблицу истории.
TimReset
14.02.2026 12:05Ну т.е. всё равно приходим к тому что нужно хранить created, updated? И они не нужны примерно в 99%, но вот когда нужны - вот тут прям без них ни как. Это как логи - зачем в них смотреть когда всё работает? Но если что-то не так - они как раз кстати.

vadim_bv
14.02.2026 12:05По своему опыту скажу, что у меня за 15+ лет был один или два случая, когда мы откатывали запись таким образом. Использовали именно историческую таблицу.
Т.е. имеются две таблицы, условно говоря, users + users_hist. users_hist идентична по структуре таблице users, но обогащена полями для технической истории - предыдущее состояние + updated_timestamp + updated_user.
При этом в users может быть (но не обязательно) ещё поле src_updated_timestamp, которое используется для наката изменений с источника, чтобы не перезаливать таблицу целиком.
TimReset
14.02.2026 12:05За 20 лет в индустрии скажу что это зависит от предметной области. Но практически везде где я работал - хранились исторические данные
На текущем месте работы почти каждый месяц приходят запросы вида - а почему тут такое значение? кто поменял? а раньше же по другому было?
Вот последний раз это было несколько недель назад - раньше работало, а потом перестало и начали смотреть почему - а там в таблице запись поменяли, в которой конфигурация хранилась. И так как у нас есть историческая таблица - увидели кто и когда это сделал.

vitalus
14.02.2026 12:05Частично объективно, частично субъективно (неприменимо в общем случае)

tojiboyevumidjon Автор
14.02.2026 12:05Согласен с вашей оценкой! Действительно, часть правил это объективные технические требования (например, TIMESTAMPTZ вместо TIMESTAMP, NUMERIC для денег), а часть это субъективные best practices, которые зависят от контекста проекта и команды (например, naming conventions, TEXT vs VARCHAR).
Статья предлагает один из подходов, который хорошо работает в высоконагруженных системах, но вы правы не все применимо универсально. Важно понимать почему стоит то или иное правило, чтобы принимать осознанное решение для своего конкретного случая.

ahdenchik
14.02.2026 12:05часть правил это объективные технические требования (например [...] NUMERIC для денег
Замечательная штука - позволяет незаметно прятать остатки от неделимых копеек, потому что сотая доля копейки, положенная в numeric(10, 2), просто исчезает. Слышал даже что в 80-е кто-то из американских IT-шников на этих центах состояние сколотил

bankir1980
14.02.2026 12:05Работая в банках 20+ лет ни разу не встречал сотые доли копеек в расчетах. Все поля в банковской системе для хранения денег в формате с 2 числами после запятой.

aleksandy
14.02.2026 12:05Деньги считают не только в банках.
Например, ситуация 15 летней давности. Сейчас, возможно, что-то изменилось. Сбытовая компания закупает на свободном рынке электроэнергию в МВт, а перепродаёт её потребителям уже в кВт. Так вот для потребителя тариф получался простым делением на 1000. Т.о. для потребителя тариф на электроэнергию по свободной цене был с точностью до 5 знаков.

rSedoy
14.02.2026 12:05Таблицы - множественное число, snake_case: users, order_items. Никогда не в единственном числе и не camelCase.
Вот с "никогда не в единственном числе", точно нет, это больше зависит от принятых в "твоей тусовке" соглашений. Как пример, django, там принято модель называть в единственном числе, следовательно и название таблицы будет так же, если явно это не менять. Скорее всего где-то такое же и с snake_case vs camelCase, особенно когда это делается через ORM, а не в ручную.
В итоге, часть правил (в других комментах тоже много верных претензий по ним указано) сильно субъективные и спорные, чтобы выдавать их за "железные".
stranger_shaman
14.02.2026 12:05постгрес не различает регистры в именах колонок и таблиц, UserId и userid для него одно и тоже. Так что никакого CamelCase.

rSedoy
14.02.2026 12:05Да ладно, двойные кавычки без проблем заставят его различать. Я не зря упомянул "особенно когда это делается через ORM"

tojiboyevumidjon Автор
14.02.2026 12:05Спасибо за комментарий! Вы правы, что вопрос о единственном/множественном числе для таблиц больше зависит от принятых в 'вашей тусовке' соглашений. Например, в Django действительно принято единственное число, и это работает. Извините, что подал это как абсолютное правило лучше было бы сказать 'выберите один стиль и следуйте ему консистентно'. Snake_case vs camelCase тоже справедливое замечание, особенно когда это идёт через ORM.

rSedoy
14.02.2026 12:05Ну вот, очередной отвечает через LLM :( ты сам почитай, насколько тошнотворный этот ответ для кожаных.
XelaVopelk
Многое спорно, но вот за это надо точно бить с ноги. Нарисуют индексов на каждый чих, а потом жалуются, что база тормозит на запись.
tojiboyevumidjon Автор
Спасибо за уточнение! Вы совершенно правы. Я был недостаточно точен в формулировке. Действительно, индексы нужно создавать с умом, а не на каждый чих, и потом следить за тем, что база не тормозит на записи. Извините, что не раскрыл эту тему подробнее и не упомянул про баланс между производительностью чтения и записи. Важное дополнение!
vskorkosh
Как-то сразу грустно, когда видишь такие комментарии, где сразу видно, что отвечала нейросеть... Видимо старею.
tojiboyevumidjon Автор
https://habr.com/ru/articles/996560/#comment_29529264