Каждый, кто хоть раз разбирался в три часа ночи с упавшим продом, знает: большинство катастроф в базах данных это не сбой железа и не космические лучи. Это решения, принятые на этапе проектирования схемы. «Потом поправим», «в приложении проверим», «а зачем тут индекс?» каждая из этих фраз обходилась командам в часы даунтайма и миллионы потерянных строк.

Ниже 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)


  1. XelaVopelk
    14.02.2026 12:05

    16. Создавайте индексы для каждого WHERE, JOIN и ORDER BY

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


    1. tojiboyevumidjon Автор
      14.02.2026 12:05

      Спасибо за уточнение! Вы совершенно правы. Я был недостаточно точен в формулировке. Действительно, индексы нужно создавать с умом, а не на каждый чих, и потом следить за тем, что база не тормозит на записи. Извините, что не раскрыл эту тему подробнее и не упомянул про баланс между производительностью чтения и записи. Важное дополнение!


      1. vskorkosh
        14.02.2026 12:05

        Как-то сразу грустно, когда видишь такие комментарии, где сразу видно, что отвечала нейросеть... Видимо старею.



  1. shurutov
    14.02.2026 12:05

    1. Каждая таблица ОБЯЗАНА иметь created_at и updated_at

    Крайне спорное утверждение. И да, когда подобные требования вытекают из необходимости аудита, то автоматически подтягивается требование указывать, кто именно и на каком основании вносил соответствующие изменения. С учётом того, что, как правило, в любом случае все подключения идут из-под специальной ТЕХНИЧЕСКОЙ учётной записи, то удовлетворение этого требования (указания, кто вносил изменения) становится не совсем тривиальной задачей.

    1. Используйте TEXT вместо VARCHAR(n)

    Как-то неубедительно. Вот здесь: https://ru-postgres.livejournal.com/65930.html развёрнуто;

    1. Используйте BIGINT / BIGSERIAL для ID, а не INT

    Если против типа (BIGINT) никаких возражений нет и неизвестно, то вот использование *SERIAL вызывает лютейшую боль и страдания, т.к., начиная с 10-й версии, для указания первичного ключа служит более другая конструкция:

    CREATE TABLE mylib (
        id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
        ...
    
    1. Используйте EXPLAIN ANALYZE перед деплоем запросов

    Работает, если у вас в среде нагрузочного тестирования ресурсы под БД и размеры БД таковы, что планы запросов и там (в тесте), и там (в проде) будут идентичными.

    Это вот навскидку, после просмотра по-диагонали. Подумаю, может ещё чего придумаю.


    1. tojiboyevumidjon Автор
      14.02.2026 12:05

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


    1. iamkisly
      14.02.2026 12:05

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


    1. TimReset
      14.02.2026 12:05

      Крайне спорное утверждение.

      Вообще ни разу не спорное. И я бы ещё добавил что лучше хранить историю изменений таблицы, для доступа к последней версии записи ввести что-то вроде is_last.

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

      Логи в любом случае нужны, но из базы это всё удобнее получить и более наглядно.

      И так как мы в будущее плохо смотрим, то я выбрал простую стратегию - всегда пишем, а если как то это будет негативно влиять (ни разу с этим не сталкивался, но всё же) то явно конкретно в этом месте не используем.

      Но вообще всё зависит от бизнеса. В банках важно, кто когда что-то поменял (с них потом спросят), поэтому тут лучше всё писать. Где нибудь в pet project - наверное нет.


      1. vadim_bv
        14.02.2026 12:05

        Для отслеживания изменений создаются таблицы логов/истории как копия таблицы + технические поля. Они не нужны примерно никогда (проверено на опыте), так что зачем утяжелять структуру / кол-во строк в исходной таблице?
        Запись идет либо сразу в две таблицы, либо на основную (если изменения достаточно редки) вешается триггер, пишущий в таблицу истории.


        1. TimReset
          14.02.2026 12:05

          Ну т.е. всё равно приходим к тому что нужно хранить created, updated? И они не нужны примерно в 99%, но вот когда нужны - вот тут прям без них ни как. Это как логи - зачем в них смотреть когда всё работает? Но если что-то не так - они как раз кстати.


          1. vadim_bv
            14.02.2026 12:05

            По своему опыту скажу, что у меня за 15+ лет был один или два случая, когда мы откатывали запись таким образом. Использовали именно историческую таблицу.
            Т.е. имеются две таблицы, условно говоря, users + users_hist. users_hist идентична по структуре таблице users, но обогащена полями для технической истории - предыдущее состояние + updated_timestamp + updated_user.
            При этом в users может быть (но не обязательно) ещё поле src_updated_timestamp, которое используется для наката изменений с источника, чтобы не перезаливать таблицу целиком.


            1. TimReset
              14.02.2026 12:05

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

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

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


  1. vitalus
    14.02.2026 12:05

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


    1. tojiboyevumidjon Автор
      14.02.2026 12:05

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

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


      1. ahdenchik
        14.02.2026 12:05

        часть правил это объективные технические требования (например [...] NUMERIC для денег

        Замечательная штука - позволяет незаметно прятать остатки от неделимых копеек, потому что сотая доля копейки, положенная в numeric(10, 2), просто исчезает. Слышал даже что в 80-е кто-то из американских IT-шников на этих центах состояние сколотил


        1. vadim_bv
          14.02.2026 12:05

          NUMERIC бывает с какой угодно точностью после запятой. 4 знаков обычно хватает (как 64 кБ памяти Биллу Гейтсу :))


          1. ahdenchik
            14.02.2026 12:05

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


        1. bankir1980
          14.02.2026 12:05

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


          1. aleksandy
            14.02.2026 12:05

            Деньги считают не только в банках.

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


            1. BugM
              14.02.2026 12:05

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


  1. rSedoy
    14.02.2026 12:05

    Таблицы - множественное число, snake_case: users, order_items. Никогда не в единственном числе и не camelCase.

    Вот с "никогда не в единственном числе", точно нет, это больше зависит от принятых в "твоей тусовке" соглашений. Как пример, django, там принято модель называть в единственном числе, следовательно и название таблицы будет так же, если явно это не менять. Скорее всего где-то такое же и с snake_case vs camelCase, особенно когда это делается через ORM, а не в ручную.

    В итоге, часть правил (в других комментах тоже много верных претензий по ним указано) сильно субъективные и спорные, чтобы выдавать их за "железные".


    1. stranger_shaman
      14.02.2026 12:05

      постгрес не различает регистры в именах колонок и таблиц, UserId и userid для него одно и тоже. Так что никакого CamelCase.


      1. rSedoy
        14.02.2026 12:05

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


    1. tojiboyevumidjon Автор
      14.02.2026 12:05

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


      1. rSedoy
        14.02.2026 12:05

        Ну вот, очередной отвечает через LLM :( ты сам почитай, насколько тошнотворный этот ответ для кожаных.