SQL и базы данных: общие вопросы
Кратко о теме
Заголовок раздела «Кратко о теме»Общий блок по базам данных на собеседовании проверяет не знание синтаксиса, а наличие цельной модели: чем реляционная модель отличается от остальных, что именно гарантирует СУБД (ACID и уровни изоляции), как физически лежат данные на диске, что происходит с запросом от текста до результата и где он упирается в ресурсы. Почти все вопросы этого раздела сводятся к четырём осям: модель данных (реляционная / документная / KV / колоночная / графовая), гарантии (ACID, MVCC, CAP), физика (страницы, heap, WAL, индексы) и SQL как язык (декларативность, порядок вычисления секций, JOIN, агрегаты, множественные операции).
Важно держать в голове, что SQL — декларативный язык: вы описываете результат, а планировщик сам выбирает способ его получить. Отсюда растёт половина практических вопросов: почему один и тот же запрос вдруг стал медленным (сменился план), зачем EXPLAIN (ANALYZE, BUFFERS), почему важна актуальная статистика, зачем индекс на колонку фильтра и почему функция над колонкой этот индекс убивает. Вторая половина вопросов растёт из физики PostgreSQL: heap-таблицы из страниц по 8 КБ, MVCC с версиями строк (xmin/xmax), отсутствие «обновления на месте», WAL перед изменением страницы, TOAST для больших значений, VACUUM как сборщик мусора.
Отдельный смысловой узел — конкурентность. Уровни изоляции (в PostgreSQL по умолчанию Read Committed), аномалии (dirty read, non-repeatable read, phantom, lost update, write skew), блокировки строк и таблиц, дедлоки. Не путайте эти «аномалии конкурентности» с «аномалиями нормализации» (insert/update/delete anomaly) — на собеседовании вопрос «какие есть аномалии в БД?» может означать и то, и другое, поэтому уточняйте.
Наконец, распределённая часть: реплики, шардирование, CAP, consistent hashing, dual write между сервисами. Здесь ключевая мысль — одиночный PostgreSQL не «выбирает» между C и A, CAP говорит о поведении при сетевом разделении в распределённой системе; как только появляются реплики и синхронный/асинхронный commit, выбор становится настоящим.
Вопросы и ответы
Заголовок раздела «Вопросы и ответы»Какие бывают виды бд? Какие плюсы и минусы?
Заголовок раздела «Какие бывают виды бд? Какие плюсы и минусы?»Коротко. По модели данных: реляционные (PostgreSQL, MySQL, Oracle), документные (MongoDB), key-value (Redis, etcd), колоночные/wide-column (ClickHouse, Cassandra), графовые (Neo4j), временных рядов (TimescaleDB, Prometheus), полнотекстовые/поисковые (Elasticsearch). Плюс/минус каждой — это компромисс между гибкостью схемы, силой гарантий и скоростью конкретного паттерна доступа.
Глубже. Реляционные: строгая схема, внешние ключи, транзакции и произвольные JOIN — платите за это сложностью горизонтального масштабирования записи. Документные: удобны, когда агрегат читается и пишется целиком, схема плавает; расплата — денормализация, ручное поддержание целостности, слабые кросс-документные транзакции. Key-value: минимальная латентность и простое шардирование, но доступ только по ключу. Колоночные: сжатие и сканирование по столбцам, отлично для аналитики на миллиардах строк, плохо для точечных обновлений и OLTP. Графовые: обход связей произвольной глубины за разумное время (в реляционке это рекурсивные CTE и боль). Time-series: специализированное сжатие, ретеншн, downsampling. Правильный ответ на собеседовании заканчивается фразой «выбор — от паттернов доступа и требований к консистентности, а не от моды»; и упоминанием, что в реальном проекте почти всегда полиглотное хранение: PostgreSQL как источник истины + Redis как кеш + ClickHouse под аналитику.
С какой СУБД вам приятнее работать: MySQL или Postgres? Почему?
Заголовок раздела «С какой СУБД вам приятнее работать: MySQL или Postgres? Почему?»Коротко. Это вопрос про аргументацию, а не про «правильный ответ». Скажите, с чем реально работали, и назовите 3–4 конкретных технических отличия, которые для вас важны, а не «постгрес лучше».
Глубже. Содержательные отличия, которые уместно назвать: типы данных и расширяемость PostgreSQL (jsonb с индексами GIN, массивы, uuid, tsvector, PostGIS, кастомные типы и расширения); богатый SQL (оконные функции, FILTER, LATERAL, рекурсивные CTE, RETURNING, частичные и выражательные индексы, EXCLUDE-ограничения); транзакционный DDL. Со стороны MySQL/InnoDB: кластерный индекс по первичному ключу (диапазонные чтения по PK дешевле), обновление строки на месте с undo-логом вместо MVCC-мусора и VACUUM, исторически более простая и зрелая встроенная репликация и большая экосистема хостинга, обычно меньший расход памяти на соединение (в PostgreSQL соединение — процесс, поэтому нужен pgbouncer). Честная формулировка: «мне ближе PostgreSQL из-за типов и SQL-возможностей, но у него надо уметь готовить autovacuum, bloat и пул соединений».
Почему на проекте решили мигрировать с MySQL на Postgres?
Заголовок раздела «Почему на проекте решили мигрировать с MySQL на Postgres?»Коротко. Ответ должен быть про конкретную боль, а не про вкус: не хватило типов и индексов (jsonb + GIN, полнотекст, массивы), нужны были оконные функции/CTE, требовалась строгость (транзакционный DDL, честные CHECK-ограничения, отсутствие «тихого» приведения типов), либо экосистема расширений (PostGIS, TimescaleDB, pg_partman).
Глубже. Хорошая структура ответа: (1) какая проблема была измерима — например, JSON-поле фильтровали полным сканом, потому что в MySQL нужного индекса не было; (2) какие альтернативы рассматривали — доработка на MySQL, отдельное хранилище; (3) как мигрировали — двойная запись/логическая репликация или pgloader, теневое чтение и сверка, поэтапное переключение по фиче-флагу, план отката; (4) что получили и чем заплатили — пришлось настраивать autovacuum, ставить pgbouncer, переписывать запросы с ON DUPLICATE KEY UPDATE на INSERT ... ON CONFLICT, ловить отличия в сортировке (collation) и в поведении GROUP BY. Если такого опыта не было — так и скажите, но опишите, как бы вы это спланировали.
SQL - это декларативный или императивный язык?
Заголовок раздела «SQL - это декларативный или императивный язык?»Коротко. Декларативный: вы описываете, какой результат нужен, а не как его получить. Способ выполнения (порядок соединений, метод JOIN, использование индексов) выбирает планировщик на основе статистики.
Глубже. Оговорки, которые ценят: SQL декларативен в части DML/запросов, но процедурные расширения (PL/pgSQL, хранимки, триггеры) — уже императивны; а подсказки вроде enable_seqscan = off, SET LOCAL, материализованные CTE (WITH ... AS MATERIALIZED) и ORDER BY/LIMIT фактически влияют на план. Практический вывод из декларативности: одинаковый по смыслу запрос может выполняться по-разному в зависимости от объёма данных и статистики, поэтому «запрос внезапно стал медленным» почти всегда означает «сменился план», и лечится это ANALYZE, статистикой (CREATE STATISTICS), индексами и переписыванием, а не заклинаниями.
Что такое нормальные формы базы данных?
Заголовок раздела «Что такое нормальные формы базы данных?»Коротко. Нормальные формы — набор требований к схеме, устраняющих избыточность и аномалии изменения. 1NF: атомарные значения, нет повторяющихся групп. 2NF: 1NF + нет зависимости неключевого атрибута от части составного ключа. 3NF: 2NF + нет транзитивных зависимостей неключевых атрибутов друг от друга. BCNF — усиление 3NF: любая детерминанта является ключом.
Глубже. Практически всё промышленное проектирование останавливается на 3NF/BCNF; 4NF (многозначные зависимости) и 5NF вспоминают редко. Смысл нормализации — один факт хранится в одном месте, поэтому нет ситуации, когда обновление адреса клиента нужно сделать в 500 строках заказов. Обратная сторона: больше таблиц — больше JOIN. Поэтому в OLTP держат 3NF, а осознанную денормализацию (кешированные счётчики, дублирование имени в снимок заказа, витрины) делают точечно и с ответом на вопрос «кто и когда пересчитывает копию». Отдельно: снимок исторических данных (цена товара в момент заказа) — это не нарушение нормализации, а другой факт, его правильно хранить в заказе.
What types of databases do you know? a) Relational (SQL) b) Key-value (Redis) c) Columnar (Cassandra) d) Indexes e) In-memory (Memcached)
Заголовок раздела «What types of databases do you know? a) Relational (SQL) b) Key-value (Redis) c) Columnar (Cassandra) d) Indexes e) In-memory (Memcached)»Коротко. Вопрос с ловушкой: «Indexes» — не вид базы данных, а структура внутри неё. Остальные варианты валидны как категории: реляционные, key-value, колоночные/wide-column, in-memory.
Глубже. Аккуратные уточнения, которые стоит проговорить: Cassandra — это wide-column store, а не аналитическая колоночная СУБД в смысле ClickHouse; их часто путают, хотя назначение разное (масштабируемая запись по ключу партиции против сканирования колонок для аналитики). Redis — key-value, но с типизированными структурами (list, set, sorted set, stream, hash) и опциональной персистентностью (RDB/AOF), поэтому это не просто кеш. Memcached — чистый in-memory кеш без персистентности и репликации. К списку стоит добавить документные (MongoDB), графовые (Neo4j), time-series (Prometheus, TimescaleDB), поисковые (Elasticsearch) и объектные/blob-хранилища как отдельный класс.
Task: There is a marketplace like AliExpress. Determine the seller whose total purchase amount from you is in second place. Build the table
Заголовок раздела «Task: There is a marketplace like AliExpress. Determine the seller whose total purchase amount from you is in second place. Build the table»Коротко. Минимальная схема: sellers, orders (кто у кого купил) и order_items (позиции с ценой и количеством). Ответ — агрегируем сумму по продавцу и берём вторую строку: ORDER BY total DESC OFFSET 1 LIMIT 1 либо DENSE_RANK() = 2, если нужны «вторые места» с учётом равенства сумм.
Глубже. Схема и запрос:
CREATE TABLE sellers ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL);
CREATE TABLE orders ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, buyer_id bigint NOT NULL, seller_id bigint NOT NULL REFERENCES sellers(id), created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE order_items ( order_id bigint NOT NULL REFERENCES orders(id) ON DELETE CASCADE, product_id bigint NOT NULL, qty int NOT NULL CHECK (qty > 0), price numeric(12,2) NOT NULL CHECK (price >= 0), PRIMARY KEY (order_id, product_id));
-- второе место по сумме моих покупок у продавцаSELECT s.id, s.name, SUM(oi.qty * oi.price) AS totalFROM orders oJOIN order_items oi ON oi.order_id = o.idJOIN sellers s ON s.id = o.seller_idWHERE o.buyer_id = $1GROUP BY s.id, s.nameORDER BY total DESCOFFSET 1 LIMIT 1;Вариант с ранжированием, когда несколько продавцов могут делить первое место:
SELECT id, name, totalFROM ( SELECT s.id, s.name, SUM(oi.qty * oi.price) AS total, DENSE_RANK() OVER (ORDER BY SUM(oi.qty * oi.price) DESC) AS rnk FROM orders o JOIN order_items oi ON oi.order_id = o.id JOIN sellers s ON s.id = o.seller_id WHERE o.buyer_id = $1 GROUP BY s.id, s.name) tWHERE rnk = 2;Обязательно скажите про numeric вместо float для денег и про то, что цену позиции фиксируем в order_items (снимок), а не тянем из products.
Пример агрегатных функций. В какой секции запроса их можно применить, чтобы отфильтровать результаты?
Заголовок раздела «Пример агрегатных функций. В какой секции запроса их можно применить, чтобы отфильтровать результаты?»Коротко. Агрегаты: COUNT, SUM, AVG, MIN, MAX, ARRAY_AGG, STRING_AGG, BOOL_AND/OR, JSONB_AGG. Фильтровать по результату агрегата можно только в HAVING — она выполняется после GROUP BY; WHERE фильтрует строки до агрегации и агрегаты в ней запрещены.
Глубже. Логический порядок вычисления: FROM/JOIN → WHERE → GROUP BY → агрегация → HAVING → оконные функции → SELECT → DISTINCT → ORDER BY → LIMIT/OFFSET. Поэтому в WHERE не видны ни агрегаты, ни алиасы из SELECT, а в HAVING — видны агрегаты. Ещё два инструмента: FILTER (WHERE ...) для условной агрегации внутри одного прохода и оборачивание в подзапрос/CTE, когда нужно фильтровать по значению оконной функции (в HAVING она недоступна).
SELECT city_id, COUNT(*) AS users_total, COUNT(*) FILTER (WHERE is_active) AS users_activeFROM usersWHERE created_at >= now() - interval '1 year' -- до агрегацииGROUP BY city_idHAVING COUNT(*) FILTER (WHERE is_active) > 10; -- после агрегацииЧто такое триггер? Для чего используется?
Заголовок раздела «Что такое триггер? Для чего используется?»Коротко. Триггер — процедура, которую СУБД автоматически выполняет при событии над таблицей (INSERT/UPDATE/DELETE/TRUNCATE), до или после него, построчно (FOR EACH ROW) или один раз на оператор (FOR EACH STATEMENT). Используют для аудита, поддержания денормализованных полей, валидации, автозаполнения (updated_at), логирования истории.
Глубже. В PostgreSQL триггер — это связка «функция, возвращающая trigger» + CREATE TRIGGER. BEFORE ROW может изменить NEW или отменить операцию, вернув NULL; AFTER ROW видит уже применённое состояние и годится для записи в журнал; есть INSTEAD OF для представлений и WHEN-условие, чтобы не вызывать функцию зря.
CREATE FUNCTION set_updated_at() RETURNS trigger AS $$BEGIN NEW.updated_at := now(); RETURN NEW;END;$$ LANGUAGE plpgsql;
CREATE TRIGGER users_set_updated_atBEFORE UPDATE ON usersFOR EACH ROWWHEN (OLD.* IS DISTINCT FROM NEW.*)EXECUTE FUNCTION set_updated_at();Триггеры, для чего они нужны и какие есть проблемы?
Заголовок раздела «Триггеры, для чего они нужны и какие есть проблемы?»Коротко. Назначение — см. выше. Проблемы: скрытая логика (её не видно в коде приложения), стоимость на каждой строке при массовых операциях, риск каскадов и рекурсии, усложнение отладки и миграций, и то, что триггер работает внутри той же транзакции — ошибка в нём откатывает основную операцию.
Глубже. Конкретика для собеседования: построчный триггер на INSERT из 1 млн строк — это 1 млн вызовов функции, часто в 5–10 раз медленнее, чем без него (лечится FOR EACH STATEMENT + переходными таблицами REFERENCING NEW TABLE AS ...). Триггеры не выполняются при COPY ... FREEZE-подобных обходах и логической репликации по умолчанию (нужны ENABLE ALWAYS TRIGGER). Побочные эффекты вне БД (HTTP, отправка письма) в триггере делать нельзя: транзакция может откатиться, а письмо уже улетело — для этого transactional outbox. Триггеры также ломают ожидания ORM (после INSERT данные в объекте устарели — нужен RETURNING). Общее правило: бизнес-логику — в приложение, триггеры — под инфраструктурные задачи (аудит, updated_at, поддержка истории), либо под инварианты, которые критично нельзя обойти в обход приложения.
В чем разница между OLAP и OLTP решениями?
Заголовок раздела «В чем разница между OLAP и OLTP решениями?»Коротко. OLTP — короткие транзакции над небольшим числом строк, много конкурентных пользователей, нормализованная схема, строковое хранение, индексы под точечный доступ (PostgreSQL, MySQL). OLAP — аналитика: тяжёлые сканы и агрегаты по миллиардам строк, схема «звезда/снежинка», колоночное хранение и сжатие, пакетная загрузка (ClickHouse, BigQuery, Greenplum).
Глубже. Различия в требованиях: OLTP оптимизируют по latency одной операции и по конкурентности, OLAP — по throughput сканирования и по стоимости хранения. Отсюда физика: колоночное хранение читает только нужные столбцы и сжимает их в 5–20 раз, но точечный UPDATE в нём дорог. Гибриды (HTAP) существуют, но на практике аналитику уводят с боевой OLTP-базы: либо реплика для чтения, либо ETL/CDC в отдельное хранилище — иначе один аналитический запрос съедает shared buffers и ломает latency продакшена.
Что такое агрегатные функции? Примеры.
Заголовок раздела «Что такое агрегатные функции? Примеры.»Коротко. Функции, которые сворачивают множество строк в одно значение: COUNT, SUM, AVG, MIN, MAX, ARRAY_AGG, STRING_AGG, JSONB_AGG, BOOL_AND, BOOL_OR, статистические STDDEV, PERCENTILE_CONT (упорядоченная агрегация через WITHIN GROUP).
Глубже. Ключевые тонкости, которые проверяют: агрегаты (кроме COUNT(*)) игнорируют NULL, поэтому COUNT(col) ≤ COUNT(*), а AVG(col) делит на число не-NULL значений; без GROUP BY агрегат по пустому набору даёт NULL для SUM/AVG/MIN/MAX и 0 для COUNT; поддерживается COUNT(DISTINCT x) (дорого — обычно сортировка/хеш) и FILTER (WHERE ...). Отдельно стоит различать агрегатные и оконные функции: одна и та же SUM в форме SUM(x) OVER (PARTITION BY ...) не схлопывает строки, а добавляет колонку.
Что такое триггеры и для чего нужны?
Заголовок раздела «Что такое триггеры и для чего нужны?»Коротко. См. выше про триггеры: автоматический вызов процедуры на событие изменения данных, применяется для аудита, автозаполнения полей, поддержки денормализованных агрегатов и жёстких инвариантов.
Глубже. Дубликат предыдущих вопросов; отличие ответа — здесь уместно сразу назвать альтернативы, которыми в PostgreSQL часто закрывают те же задачи без триггеров: DEFAULT и GENERATED ALWAYS AS ... STORED для вычисляемых колонок, CHECK/EXCLUDE-ограничения для инвариантов, внешние ключи с ON DELETE CASCADE вместо триггера-уборщика, INSERT ... ON CONFLICT вместо триггера-«апсерта», logical decoding/CDC вместо триггерного журнала изменений.
Какие проблемы с производительностью PostgreSQL вы встречали?
Заголовок раздела «Какие проблемы с производительностью PostgreSQL вы встречали?»Коротко. Типовой список: отсутствующие или неподходящие индексы и seq scan на больших таблицах, устаревшая статистика и плохие планы, bloat таблиц/индексов из-за отстающего autovacuum, «дорогие» соединения (сотни процессов без pgbouncer), блокировки и дедлоки, долгие транзакции, удерживающие горизонт xmin, спиллинг сортировок и хешей на диск при малом work_mem, N+1 из ORM.
Глубже. Как это диагностируют: pg_stat_statements (топ по total_exec_time и по mean_exec_time), EXPLAIN (ANALYZE, BUFFERS) для конкретного запроса (расхождение rows estimated/actual = проблема со статистикой), pg_stat_activity + wait_event_type (Lock, IO, LWLock), pg_stat_user_tables.n_dead_tup и last_autovacuum для bloat, pg_locks для блокировок, pg_stat_bgwriter/checkpoint-логи для «пилы» на диске. Отдельные грабли, которые полезно назвать: DDL под ACCESS EXCLUSIVE без lock_timeout останавливает всю таблицу; ALTER TABLE ... ADD COLUMN ... DEFAULT до PG 11 переписывал таблицу (с 11 — нет, для непроменчивых значений); CREATE INDEX без CONCURRENTLY блокирует запись; неверный тип (text vs int) ломает использование индекса; OFFSET на глубоких страницах линейно деградирует (keyset pagination); TOAST-поля и SELECT * на широких таблицах.
Как PostgreSQL хранит данные?
Заголовок раздела «Как PostgreSQL хранит данные?»Коротко. Каждая таблица и индекс — набор файлов в каталоге кластера (base/<db_oid>/<relfilenode>), сегментами по 1 ГБ, состоящих из страниц по 8 КБ. Строки (tuples) кладутся в страницу heap-файла в произвольном порядке; порядок вставки не гарантирован, кластерного индекса нет. Большие значения выносятся в TOAST-таблицу. Все изменения сначала пишутся в WAL, потом попадают в файлы данных при checkpoint.
Глубже. Структура страницы: заголовок (PageHeaderData), массив указателей на строки (ItemId, отсюда ctid = (номер страницы, номер слота)), свободное место посередине, сами строки с конца страницы. У каждой версии строки есть системные поля xmin, xmax, ctid, t_infomask — это основа MVCC. Кроме основного forka у отношения есть FSM (free space map — где есть место под вставку), VM (visibility map — какие страницы полностью видимы, что даёт index-only scan и ускоряет vacuum) и init-fork для unlogged-таблиц. Значения длиннее ~2 КБ (порог TOAST_TUPLE_THRESHOLD) сжимаются и/или выносятся в отдельную TOAST-таблицу кусками, потому что строка не может пересекать границу страницы. Индексы (B-tree, GIN, GiST, BRIN, hash) — отдельные файлы, ссылающиеся на ctid heap-строки; поэтому в PostgreSQL любой индекс вторичный, и «index-only scan» возможен только если visibility map говорит, что страница целиком видима.
CAP-теория. Как достигается согласованность в PostgreSQL?
Заголовок раздела «CAP-теория. Как достигается согласованность в PostgreSQL?»Коротко. CAP: в распределённой системе при сетевом разделении (P) нельзя одновременно сохранить и линеаризуемую согласованность (C), и доступность (A) — надо выбрать. Одиночный PostgreSQL под CAP формально не подпадает (нет разделения): согласованность там обеспечивается ACID — WAL, MVCC со снимками, блокировками и уровнями изоляции вплоть до Serializable (SSI).
Глубже. Как только появляется репликация, выбор становится реальным. Асинхронная потоковая репликация: primary отвечает клиенту сразу, реплики отстают → доступность выше, чтение с реплик даёт eventual consistency и возможную потерю последних транзакций при аварийном failover. Синхронная (synchronous_commit = on|remote_write|remote_apply + synchronous_standby_names): commit ждёт подтверждения реплики, потеря данных исключается, но при недоступности синхронной реплики запись останавливается — это выбор CP. Для чтения своих же записей с реплики применяют pg_current_wal_lsn()/pg_last_wal_replay_lsn() (read-your-writes через ожидание LSN) или просто маршрутизируют критичные чтения на primary. Про CAP полезно добавить, что практичнее модель PACELC: даже без разделения есть выбор между latency и consistency.
Что такое SQL и NoSQL, их преимущества и недостатки, различия?
Заголовок раздела «Что такое SQL и NoSQL, их преимущества и недостатки, различия?»Коротко. SQL-базы — реляционная модель, фиксированная схема, декларативный язык запросов, ACID-транзакции, JOIN. NoSQL — обобщающее название для нереляционных хранилищ (документные, KV, wide-column, графовые) с гибкой схемой, простым API доступа и упором на горизонтальное масштабирование и доступность.
Глубже. Плюсы SQL: целостность данных на уровне БД (FK, CHECK, уникальность), произвольные запросы без переписывания схемы, зрелые транзакции, единый стандарт. Минусы: масштабирование записи требует шардирования, миграции схемы на больших таблицах болезненны, схема жёстче. Плюсы NoSQL: масштабирование «из коробки», удобство для агрегатных документов, высокая скорость точечных операций, отсутствие миграций. Минусы: целостность и «джойны» переезжают в приложение, часто eventual consistency, запросы ограничены заранее выбранным ключом/индексом, легко получить рассинхрон дублированных данных. Актуальная поправка: граница размылась — PostgreSQL умеет jsonb с GIN-индексами, а MongoDB с 4.0 умеет многодокументные транзакции. Поэтому выбирать надо по паттернам доступа и требованиям к консистентности.
В чем отличие реляционных от не реляционных баз данных?
Заголовок раздела «В чем отличие реляционных от не реляционных баз данных?»Коротко. Реляционная БД хранит данные в виде отношений (таблиц) со строгой схемой, обеспечивает ссылочную целостность и позволяет соединять любые таблицы произвольными запросами. Нереляционная хранит данные в виде документов/пар ключ-значение/колоночных семейств/графа, схему валидирует слабо или не валидирует, а связи и целостность оставляет приложению.
Глубже. См. предыдущий ответ; отличие акцента здесь — в модели данных, а не в языке. Полезно проговорить, что «реляционная» значит математическую модель Кодда (отношения, ключи, реляционная алгебра), из неё следуют нормализация и декларативность SQL. Из практики: в реляционной БД одна и та же схема обслуживает много разных запросов, в нереляционной схему проектируют «под запрос» (query-driven design в Cassandra/DynamoDB), и появление нового паттерна доступа означает новую таблицу и переливку данных.
Всегда ли можно записать NULL в поле базы данных?
Заголовок раздела «Всегда ли можно записать NULL в поле базы данных?»Коротко. Нет. Запись NULL запрещена, если на колонке стоит NOT NULL, если она входит в PRIMARY KEY (он неявно NOT NULL), либо если CHECK/GENERATED-ограничение это не допускает.
Глубже. Дополнительные тонкости: UNIQUE в стандартном поведении PostgreSQL допускает несколько NULL (они не равны друг другу), а с PG 15 это регулируется UNIQUE NULLS NOT DISTINCT. NULL в колонке внешнего ключа разрешён и означает «связи нет» (проверка FK не выполняется); для составного FK по умолчанию MATCH SIMPLE — если хоть одна колонка NULL, проверка пропускается. Если у колонки есть DEFAULT, явная запись NULL всё равно запишет NULL, а не значение по умолчанию — дефолт применяется только при DEFAULT-ключевом слове или отсутствии колонки в INSERT. И общее про семантику: NULL — это «неизвестно», сравнение x = NULL даёт NULL (не true), проверяют через IS NULL / IS DISTINCT FROM.
С какой бд работал?
Заголовок раздела «С какой бд работал?»Коротко. Вопрос про опыт: назовите основную БД, версию, масштаб (объём, RPS, число таблиц), и 2–3 задачи, которые вы на ней решали руками — а не просто список названий.
Глубже. Каркас сильного ответа: «Основная — PostgreSQL 14/16 в проде: таблицы до N сотен миллионов строк, партиционирование по дате, pgbouncer в transaction pooling, работа через pgx/sqlc; занимался разбором медленных запросов через pg_stat_statements и EXPLAIN ANALYZE, настройкой индексов, онлайн-миграциями через goose с CREATE INDEX CONCURRENTLY. Дополнительно Redis как кеш и распределённый лок, ClickHouse под аналитику, MongoDB — точечно». Типичные ошибки: перечислить десять систем без глубины; сказать «работал с базой» и не назвать ни одной операционной задачи; преувеличить — интервьюер тут же уточнит про autovacuum или про план запроса.
Как можно повысить производительность бд в плане масштабируемости?
Заголовок раздела «Как можно повысить производительность бд в плане масштабируемости?»Коротко. По возрастанию сложности: индексы и переписывание запросов → пулинг соединений (pgbouncer) → вертикальное масштабирование → кеш (Redis) → read-реплики для чтения → партиционирование больших таблиц → архивирование и ретеншн → шардирование записи → вынос аналитики в отдельное хранилище.
Глубже. Важно проговорить порядок и цену. Сначала дешёвое: убрать N+1, добавить нужные индексы, батчить записи (COPY, многострочный INSERT), убрать SELECT *, заменить OFFSET-пагинацию на keyset. Затем инфраструктура: pgbouncer, потому что каждое соединение в PostgreSQL — процесс, и 1000 соединений убивают сервер; ограничение пула в приложении. Реплики масштабируют только чтение и приносят лаг репликации — нужно решать, какие чтения терпят устаревание. Партиционирование (PARTITION BY RANGE (created_at)) даёт partition pruning и дешёвое удаление старых данных через DROP PARTITION вместо DELETE. Шардирование — последняя мера: выбирается ключ шардирования, ломаются кросс-шардовые JOIN и уникальность, транзакции становятся распределёнными; варианты — на уровне приложения или Citus. Отдельно: очереди и асинхронная обработка снимают пик записи, а CQRS с денормализованными витринами снимает тяжёлые чтения.
Через орм или чистый sql работаете с бд и какие критерии выбоа?
Заголовок раздела «Через орм или чистый sql работаете с бд и какие критерии выбоа?»Коротко. В Go я обычно за явный SQL — pgx + sqlc/sqlx, потому что запросы видны, план предсказуем и легко читается ревьюером; ORM (GORM, ent) оправдан на CRUD-heavy сервисах и там, где важна скорость разработки и генерация миграций.
Глубже. Критерии выбора: (1) сложность запросов — оконные функции, CTE, ON CONFLICT, LATERAL в ORM выражаются плохо и всё равно уезжают в raw SQL; (2) требования к производительности — ORM провоцирует N+1 и SELECT *, поэтому в горячих путях нужен контроль; (3) команда и объём CRUD — на большом количестве однотипных сущностей ORM экономит много кода; (4) тестируемость и наблюдаемость — sqlc даёт типизированные методы на этапе компиляции, что ловит ошибки раньше; (5) вендорная переносимость — реально нужна редко и не стоит отказа от возможностей PostgreSQL. Практичная позиция: репозиторный слой, внутри которого 90% запросов — явный SQL, а часть простых CRUD-операций может генерироваться. Главное — уметь назвать конкретные минусы обеих сторон, а не топить за одну.
С какими реляционными базами работал кроме Postgres?
Заголовок раздела «С какими реляционными базами работал кроме Postgres?»Коротко. Честно перечислите то, чем реально пользовались (MySQL/MariaDB, SQLite, ClickHouse как SQL-хранилище, Oracle/MS SQL), и для каждой скажите, чем именно занимались и чем она отличалась от PostgreSQL.
Глубже. Полезные точки отличия, которые показывают глубину: в MySQL/InnoDB кластерный индекс по PK и вторичные индексы, хранящие PK вместо ctid — отсюда важность короткого монотонного первичного ключа; уровень изоляции по умолчанию REPEATABLE READ (в PostgreSQL — READ COMMITTED); отсутствие транзакционного DDL; utf8mb4 vs utf8; поведение GROUP BY и ONLY_FULL_GROUP_BY. SQLite — встраиваемая, один писатель, WAL-режим, динамическая типизация; отлично подходит для тестов и локальных инструментов. Если опыта нет — скажите прямо и обозначьте, какие отличия знаете теоретически.
Какие есть аномалии в бд?
Заголовок раздела «Какие есть аномалии в бд?»Коротко. Термин используют в двух смыслах. Аномалии проектирования (следствие ненормализованной схемы): аномалия вставки, обновления и удаления. Аномалии конкурентного доступа (отсюда уровни изоляции): грязное чтение, неповторяемое чтение, фантомы, потерянное обновление, read skew и write skew.
Глубже. Первая группа: если адрес клиента хранится в каждой строке заказов — нельзя завести клиента без заказа (insert anomaly), изменение адреса нужно применить ко всем строкам (update anomaly), удаление последнего заказа стирает и данные клиента (delete anomaly). Лечится нормализацией.
Вторая группа и уровни изоляции по стандарту: Read Uncommitted допускает dirty read (в PostgreSQL этот уровень реализован как Read Committed, грязного чтения нет никогда); Read Committed допускает non-repeatable read и фантомы; Repeatable Read запрещает первые три, а в PostgreSQL реализован как snapshot isolation, поэтому фантомов тоже нет, но остаётся write skew; Serializable в PostgreSQL реализован через SSI и убирает write skew ценой ошибок сериализации (40001), которые приложение обязано ретраить. Потерянное обновление на Read Committed решают SELECT ... FOR UPDATE, атомарным UPDATE ... SET x = x + 1 или оптимистической блокировкой по версии.
Что делает UNION и UNION ALL?
Заголовок раздела «Что делает UNION и UNION ALL?»Коротко. Оба объединяют результаты двух и более запросов по вертикали. UNION дополнительно удаляет дубликаты строк, UNION ALL — нет и потому дешевле. Число и типы колонок должны быть совместимыми, имена берутся из первого запроса.
Глубже. UNION реализуется через сортировку или хеширование всего результата, поэтому на больших наборах он заметно дороже; если дубликатов заведомо нет (например, объединяются непересекающиеся периоды), всегда пишите UNION ALL. Тонкости: ORDER BY и LIMIT относятся ко всему объединению, а не к последнему запросу — чтобы ограничить каждую часть, оборачивайте её в скобки/подзапрос. При дедупликации NULL считаются равными друг другу (в отличие от =). Рядом стоят INTERSECT и EXCEPT с той же семантикой ALL/без.
Question about escaping in SQL queries
Заголовок раздела «Question about escaping in SQL queries»Коротко. Правильный ответ: не экранировать вручную, а использовать параметризованные запросы (в PostgreSQL — плейсхолдеры $1, $2, в Go — db.QueryContext(ctx, "... WHERE id = $1", id)). Тогда значение никогда не попадает в текст запроса и SQL-инъекция невозможна в принципе.
Глубже. Что делать, когда параметр подставить нельзя. Имена таблиц/колонок и ORDER BY параметрами не биндятся — их либо валидируют по белому списку, либо экранируют как идентификатор (pgx.Identifier{"my table"}.Sanitize(), quote_ident()). Для литералов в динамическом SQL внутри PL/pgSQL — quote_literal()/quote_nullable() или format('%L/%I'), а лучше EXECUTE ... USING. Внутри строковых литералов одинарная кавычка удваивается ('O''Reilly'); при standard_conforming_strings = on (по умолчанию с 9.1) обратный слеш — обычный символ, а C-стиль экранирования включается префиксом E'...'. Отдельно про LIKE: символы % и _ экранируются через ESCAPE, иначе пользовательский ввод % превратится в полный скан. Ещё частые дыры: конкатенация в IN (...) (используйте = ANY($1) с массивом), fmt.Sprintf для «просто числа», и LIMIT/OFFSET из строки.
Question about locking in SQL
Заголовок раздела «Question about locking in SQL»Коротко. В PostgreSQL блокировки бывают на уровне строк (FOR UPDATE, FOR NO KEY UPDATE, FOR SHARE, FOR KEY SHARE), на уровне таблиц (от ACCESS SHARE при SELECT до ACCESS EXCLUSIVE при DROP/ALTER/VACUUM FULL) и advisory-локи. Читатели не блокируют писателей и наоборот благодаря MVCC — блокируются только конкурирующие писатели.
Глубже. Практические вещи: SELECT ... FOR UPDATE берёт строку под запись и решает проблему потерянного обновления; SKIP LOCKED даёт очередь задач без внешнего брокера, NOWAIT — быстрый отказ вместо ожидания. Дедлок возникает при разном порядке захвата ресурсов; PostgreSQL детектирует его через deadlock_timeout (по умолчанию 1 с) и убивает одну транзакцию с 40P01 — лечение: единый порядок блокировок и ретраи. Для DDL обязательно ставьте lock_timeout, иначе ALTER TABLE встанет в очередь за долгим SELECT и заблокирует всех, кто пришёл после него. Диагностика: pg_locks в join с pg_stat_activity, поле wait_event_type = 'Lock'. Полезно упомянуть альтернативу пессимистичным локам — оптимистическую блокировку через колонку version и UPDATE ... WHERE version = $1.
number of employees in each project;
Заголовок раздела «number of employees in each project;»Коротко. Классическая задача на GROUP BY с LEFT JOIN, чтобы проекты без сотрудников тоже попали в вывод с нулём.
SELECT p.id, p.name, COUNT(ep.employee_id) AS employeesFROM projects pLEFT JOIN employee_projects ep ON ep.project_id = p.idGROUP BY p.id, p.nameORDER BY employees DESC;Глубже. Ключевая деталь — COUNT(ep.employee_id), а не COUNT(*): при LEFT JOIN без совпадений строка всё равно есть, и COUNT(*) вернёт 1 вместо 0. Если связь «сотрудник → проект» один-к-многим (колонка project_id в employees), таблица связей не нужна. Если сотрудник может числиться в проекте несколько раз (историчность назначений), считайте COUNT(DISTINCT ep.employee_id).
number of non-dismissible employees working on a project.
Заголовок раздела «number of non-dismissible employees working on a project.»Коротко. Вероятно имелось в виду «не уволенные» (fired_at IS NULL / is_active). Условие нужно ставить в ON (или через FILTER), а не в WHERE, иначе LEFT JOIN превратится в INNER и проекты без активных сотрудников исчезнут.
-- условие в ON: проекты без активных сотрудников останутся с 0SELECT p.id, p.name, COUNT(e.id) AS active_employeesFROM projects pLEFT JOIN employee_projects ep ON ep.project_id = p.idLEFT JOIN employees e ON e.id = ep.employee_id AND e.fired_at IS NULLGROUP BY p.id, p.name;
-- эквивалент через FILTERSELECT p.id, p.name, COUNT(e.id) FILTER (WHERE e.fired_at IS NULL) AS active_employeesFROM projects pLEFT JOIN employee_projects ep ON ep.project_id = p.idLEFT JOIN employees e ON e.id = ep.employee_idGROUP BY p.id, p.name;Глубже. Это самая частая ловушка блока про JOIN: разница между условием в ON (применяется до формирования внешнего соединения) и в WHERE (после, отбрасывая строки с NULL). Проговорите это вслух — интервьюер обычно именно это и проверяет.
Как выстроены данные в Postgres? В виде чего хранятся?
Заголовок раздела «Как выстроены данные в Postgres? В виде чего хранятся?»Коротко. Данные лежат в heap-файлах: файл на отношение, сегменты по 1 ГБ, внутри страницы по 8 КБ, внутри страницы — слоты с версиями строк (tuples). Порядок строк в heap произвольный, кластерного индекса нет; адрес версии строки — ctid (страница, слот), на него ссылаются индексы.
Глубже. См. подробный разбор в ответе «Как PostgreSQL хранит данные?». Здесь стоит добавить то, что часто спрашивают дополнительно: у отношения кроме основного форка есть FSM и VM; длинные значения уезжают в TOAST; CLUSTER физически переупорядочивает таблицу по индексу, но порядок не поддерживается автоматически и разрушается при обновлениях. Проверить раскладку можно расширениями pageinspect и pgstattuple, а имя файла узнать через SELECT pg_relation_filepath('users').
Что такое аггрегирующие функции? Примеры функций?
Заголовок раздела «Что такое аггрегирующие функции? Примеры функций?»Коротко. См. выше «Что такое агрегатные функции?»: COUNT, SUM, AVG, MIN, MAX, ARRAY_AGG, STRING_AGG, JSONB_AGG, BOOL_AND/OR, PERCENTILE_CONT(...) WITHIN GROUP (ORDER BY ...).
Глубже. Отличие от предыдущего вопроса — здесь удобно добавить, что в PostgreSQL агрегат можно написать самому: CREATE AGGREGATE со SFUNC (шаг), STYPE (тип состояния) и опциональной FINALFUNC; именно так устроены встроенные. А в планах запросов агрегация видна как Aggregate (без группировки), GroupAggregate (по отсортированному входу) или HashAggregate (хеш-таблица групп в work_mem, при нехватке — спилл на диск).
Что такое JOINы? Для чего нужны? Какие есть?
Заголовок раздела «Что такое JOINы? Для чего нужны? Какие есть?»Коротко. JOIN соединяет строки двух таблиц по условию, позволяя собрать связанные данные из нормализованной схемы. Виды: INNER (только совпадения), LEFT/RIGHT OUTER (все строки одной стороны + NULL вместо отсутствующих), FULL OUTER, CROSS (декартово произведение), а также NATURAL (по одноимённым колонкам — использовать не стоит), SELF JOIN (таблица сама с собой) и LATERAL (подзапрос, видящий колонки левой стороны).
Глубже. Отдельно различают синтаксические виды JOIN (что выше) и физические алгоритмы, которые выбирает планировщик: Nested Loop (хорош, когда внешняя сторона мала и есть индекс на внутренней), Hash Join (строит хеш по меньшей стороне, требует work_mem, только для эквисоединений), Merge Join (обе стороны отсортированы по ключу). В EXPLAIN вы видите именно эти узлы. Плюс полусоединения: EXISTS/IN дают Semi Join, NOT EXISTS — Anti Join, и они, в отличие от обычного JOIN, не размножают строки. Частые ошибки: дублирование строк при JOIN по неуникальному ключу, условие на правую таблицу в WHERE вместо ON при LEFT JOIN, NOT IN с NULL в подзапросе (вернёт пусто — используйте NOT EXISTS).
Знаком ли с операторами Limit и Offset?
Заголовок раздела «Знаком ли с операторами Limit и Offset?»Коротко. Да: LIMIT n ограничивает число возвращаемых строк, OFFSET k пропускает первые k. Без ORDER BY результат недетерминирован. Главная проблема — OFFSET на больших смещениях: СУБД всё равно вычисляет и отбрасывает k строк, поэтому глубокая пагинация деградирует линейно.
Глубже. Рабочая альтернатива — keyset (seek) пагинация по индексируемому курсору:
-- страница вперёд после последнего увиденного (created_at, id)SELECT id, created_at, titleFROM postsWHERE (created_at, id) < ($1, $2)ORDER BY created_at DESC, id DESCLIMIT 20;Такой запрос при индексе (created_at DESC, id DESC) работает за одно и то же время на любой странице. Ещё детали: стандартный синтаксис — FETCH FIRST n ROWS ONLY / OFFSET n ROWS; LIMIT применяется после ORDER BY, но планировщик умеет использовать это для «top-N sort» и раннего выхода по индексу; для точного общего числа страниц нужен отдельный COUNT(*), который на больших таблицах дорог — часто заменяют приблизительной оценкой из pg_class.reltuples или «есть ли следующая страница» (запрос LIMIT n+1).
Оператор UNION.
Заголовок раздела «Оператор UNION.»Коротко. См. выше «Что делает UNION и UNION ALL?»: вертикальное объединение результатов с удалением дубликатов; UNION ALL — без удаления и дешевле.
Глубже. Отличие акцента: UNION часто используют, чтобы обойти ограничение планировщика с OR по разным индексам — два запроса с разными условиями, объединённые UNION ALL, могут использовать оба индекса, тогда как один запрос с OR может свалиться в seq scan (хотя PostgreSQL умеет и BitmapOr). Также UNION ALL — основа ручного «партиционирования» и объединения витрин, а в PG на нём же строятся планы Append/MergeAppend для секционированных таблиц.
Какие знаешь базы данных?
Заголовок раздела «Какие знаешь базы данных?»Коротко. См. ответ «Какие бывают виды бд?»: PostgreSQL, MySQL/MariaDB, SQLite, Oracle, MS SQL — реляционные; Redis, etcd — key-value; MongoDB — документная; Cassandra, ScyllaDB — wide-column; ClickHouse — колоночная аналитическая; Elasticsearch — поисковая; Neo4j — графовая; Prometheus/VictoriaMetrics — временные ряды.
Глубже. Разница с предыдущим вопросом в том, что здесь ждут не классификацию, а ваш кругозор и умение сопоставить систему с задачей. Хорошо звучит связка «система → зачем»: Redis — кеш, rate limiting, распределённая блокировка, stream-очередь; ClickHouse — продуктовая аналитика и логи; Kafka — не БД, а лог событий, но часто в списке; S3/MinIO — объектное хранилище для файлов, которые нельзя класть в БД.
Database (postgres) - хранение и обработка sql запросов
Заголовок раздела «Database (postgres) - хранение и обработка sql запросов»Коротко. Это не вопрос, а тема из плана собеседования: от вас ждут связный рассказ про физическое хранение (heap, страницы 8 КБ, MVCC, WAL, TOAST) и про путь запроса (parser → rewriter → planner → executor).
Глубже. Каркас рассказа. Обработка запроса: клиент шлёт текст (или подготовленный оператор) по протоколу; парсер строит дерево, rewriter применяет правила и раскрывает представления; планировщик перебирает варианты соединений и методов доступа, оценивая стоимость по статистике из pg_statistic (обновляется ANALYZE) и по настройкам random_page_cost, work_mem, effective_cache_size; исполнитель тянет строки по дереву узлов (volcano-модель), читая страницы через shared buffers. Запись: изменение сначала попадает в WAL-буфер и на диск при commit (synchronous_commit), страница в shared buffers помечается dirty и сбрасывается фоновым writer’ом или checkpoint’ом. Подготовленные операторы после пяти выполнений могут перейти на generic plan — это отдельный источник «внезапно медленных» запросов.
Напишите SQL-запрос, который вернет имя и сумму бонуса каждого сотрудника с бонусом менее 1000.
Заголовок раздела «Напишите SQL-запрос, который вернет имя и сумму бонуса каждого сотрудника с бонусом менее 1000.»Коротко.
SELECT name, bonusFROM employeesWHERE bonus < 1000;Глубже. Здесь проверяют внимание к NULL: сотрудники с bonus IS NULL в выборку не попадут, потому что NULL < 1000 даёт NULL, а не true. Если по условию «нет бонуса» тоже считается «меньше 1000», пишите WHERE bonus < 1000 OR bonus IS NULL либо WHERE COALESCE(bonus, 0) < 1000 (последний вариант убивает обычный индекс — понадобится индекс по выражению). Если бонусы лежат отдельной таблицей и нужна сумма по сотруднику, это уже агрегат с фильтром в HAVING:
SELECT e.name, SUM(b.amount) AS bonus_totalFROM employees eJOIN bonuses b ON b.employee_id = e.idGROUP BY e.id, e.nameHAVING SUM(b.amount) < 1000;Какой дефолтный уровень изоляции в PostgreSQL?
Заголовок раздела «Какой дефолтный уровень изоляции в PostgreSQL?»Коротко. READ COMMITTED. Задаётся параметром default_transaction_isolation, меняется на сессию/транзакцию через SET TRANSACTION ISOLATION LEVEL ... или BEGIN ISOLATION LEVEL ....
Глубже. На Read Committed каждый оператор внутри транзакции видит свой свежий снимок, поэтому два одинаковых SELECT в одной транзакции могут вернуть разное (non-repeatable read), и возможны фантомы. Особенность PostgreSQL: при конфликте UPDATE дожидается конкурентной транзакции и затем перепроверяет условие WHERE на новой версии строки (EPQ, EvaluatePlanQual) — из-за этого возможен «потерянный апдейт» в паттерне read-modify-write на стороне приложения. Read Uncommitted в PostgreSQL реализован как Read Committed (грязного чтения нет). Repeatable Read — снимок на всю транзакцию (snapshot isolation), фантомов нет, но остаётся write skew и появляются ошибки could not serialize access (40001). Serializable — SSI с проверкой опасных структур зависимостей; тоже даёт 40001, поэтому любой код на RR/Serializable обязан уметь ретраить транзакцию. Для сравнения: в MySQL/InnoDB по умолчанию REPEATABLE READ.
САР теорема это? Какие условия в PostgreSQL удовлетворяются?
Заголовок раздела «САР теорема это? Какие условия в PostgreSQL удовлетворяются?»Коротко. См. выше «CAP-теория…»: из Consistency, Availability и Partition tolerance при реальном сетевом разделении выбирают два. Одиночный PostgreSQL — не распределённая система, там работают ACID-гарантии; кластер PostgreSQL с синхронной репликацией ведёт себя как CP, с асинхронной — ближе к AP по чтению с реплик.
Глубже. Дополнение к предыдущему ответу: важно не говорить «PostgreSQL — это CA», не пояснив, что «CA» означает лишь «нет разделения, потому что один узел». Также стоит различать уровни: клиентская консистентность (что видит приложение) настраивается отдельно — можно писать в primary и читать с реплики (eventual), можно ждать применения LSN, можно читать только с primary. Кворумная синхронная репликация задаётся synchronous_standby_names = 'ANY 1 (r1, r2)' — компромисс между надёжностью и доступностью записи.
Есть 3 сущности - пользователь, чат и сообщение. У пользователя есть имя и дата регистрации. У чата есть название и дата создания. У сообщения есть текст, автор и дата создания. Пользователь может состоять в нескольких чатах одновременно. Сообщение обязательно принадлежит чату, сообщение не может принадлежать более чем 1 чату одновременно Нужно описать предметную область в виде таблиц
Заголовок раздела «Есть 3 сущности - пользователь, чат и сообщение. У пользователя есть имя и дата регистрации. У чата есть название и дата создания. У сообщения есть текст, автор и дата создания. Пользователь может состоять в нескольких чатах одновременно. Сообщение обязательно принадлежит чату, сообщение не может принадлежать более чем 1 чату одновременно Нужно описать предметную область в виде таблиц»Коротко. Четыре таблицы: users, chats, связующая chat_members (many-to-many) и messages с обязательным chat_id (one-to-many, поэтому связующая таблица здесь не нужна).
CREATE TABLE users ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, registered_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE chats ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL, created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE chat_members ( chat_id bigint NOT NULL REFERENCES chats(id) ON DELETE CASCADE, user_id bigint NOT NULL REFERENCES users(id) ON DELETE CASCADE, joined_at timestamptz NOT NULL DEFAULT now(), PRIMARY KEY (chat_id, user_id));CREATE INDEX ON chat_members (user_id, chat_id); -- обратное направление
CREATE TABLE messages ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, chat_id bigint NOT NULL REFERENCES chats(id) ON DELETE CASCADE, author_id bigint NOT NULL REFERENCES users(id), body text NOT NULL, created_at timestamptz NOT NULL DEFAULT now());CREATE INDEX ON messages (chat_id, created_at DESC, id DESC); -- лента чатаГлубже. Что проговорить вслух: «сообщение принадлежит ровно одному чату» выражается колонкой chat_id NOT NULL, а не таблицей связей — именно это и проверяют. Составной первичный ключ (chat_id, user_id) в chat_members одновременно запрещает дубль членства и даёт индекс для «кто в чате»; для «в каких чатах Вася» нужен второй индекс с обратным порядком колонок. timestamptz, а не timestamp. Автор сообщения ссылается на users без каскада — историю сообщений при удалении пользователя обычно сохраняют (мягкое удаление или ON DELETE SET NULL с nullable-колонкой). Опционально стоит упомянуть, что при больших объёмах messages партиционируют по created_at или по chat_id.
Выбрать все чаты пользователя Вася в формате (chat_id,chat_name)
Заголовок раздела «Выбрать все чаты пользователя Вася в формате (chat_id,chat_name)»Коротко.
SELECT c.id AS chat_id, c.title AS chat_nameFROM chats cJOIN chat_members cm ON cm.chat_id = c.idJOIN users u ON u.id = cm.user_idWHERE u.name = 'Вася';Глубже. Если имя не уникально, вернутся чаты всех Вась — правильнее фильтровать по u.id = $1, а имя использовать только когда это явно требуется условием. Эквивалент через EXISTS без риска размножить строки (полезно, если бы связь была не уникальной):
SELECT c.id AS chat_id, c.title AS chat_nameFROM chats cWHERE EXISTS ( SELECT 1 FROM chat_members cm JOIN users u ON u.id = cm.user_id WHERE cm.chat_id = c.id AND u.name = 'Вася');По мере роста размеров таблиц этот запрос начинает все медленнее работать, можешь понять почему и исправить?
Заголовок раздела «По мере роста размеров таблиц этот запрос начинает все медленнее работать, можешь понять почему и исправить?»Коротко. Потому что нет индексов под условия и соединения: фильтр users.name = 'Вася' даёт seq scan по users, а поиск членств по user_id — seq scan по chat_members (индекс от PRIMARY KEY (chat_id, user_id) для поиска по user_id не годится, префикс не тот). Лечится индексами users(name) и chat_members(user_id, chat_id).
Глубже. Диагностика — EXPLAIN (ANALYZE, BUFFERS): смотрим на Seq Scan по большим таблицам, на расхождение rows= оценки и факта, на Hash Join там, где ожидался Nested Loop с индексом. После добавления CREATE INDEX CONCURRENTLY ON chat_members (user_id, chat_id) соединение превращается в index scan, а (user_id, chat_id) ещё и покрывающий — можно получить index-only scan, не заглядывая в heap. Дополнительно: индекс users(name) (или lower(name) при регистронезависимом поиске), запуск ANALYZE после массовой заливки данных, отказ от SELECT *, пагинация вместо выгрузки всех чатов. Если запрос строится ORM’ом в цикле по чатам — сначала чинится N+1, потом индексы.
Как заполнять поле id при вставках в таблицы users, chats, messages
Заголовок раздела «Как заполнять поле id при вставках в таблицы users, chats, messages»Коротко. Не вручную: id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY (стандартный способ, PG 10+) или устаревший bigserial — оба под капотом используют последовательность. В распределённом случае — UUID (лучше UUIDv7, монотонный по времени) или Snowflake-подобные ID.
Глубже. Важные детали: последовательность не транзакционна — при откате номера теряются, в id будут дыры; это нормально и не баг. GENERATED ALWAYS запрещает подсунуть свой id (можно только через OVERRIDING SYSTEM VALUE), GENERATED BY DEFAULT разрешает — вторая форма удобна для миграций и тестов, но ей легко «обогнать» последовательность и потом получить конфликт (лечится setval). Категорически нельзя INSERT ... VALUES ((SELECT max(id)+1 FROM t), ...) — это гонка и дедлоки под нагрузкой. Полученный id возвращают через RETURNING id, чтобы не делать второй запрос:
INSERT INTO chats (title) VALUES ($1) RETURNING id;Про UUID: uuid занимает 16 байт против 8 у bigint, а случайный UUIDv4 как ключ ухудшает локальность вставок в B-tree и раздувает индексы, поэтому если нужны глобальные идентификаторы — UUIDv7/ULID. Для messages при большом потоке часто используют bigint с шардируемой последовательностью или составной ключ (chat_id, id) при партиционировании.
Дана таблица “orders ”:
Заголовок раздела «Дана таблица “orders ”:»Коротко. Обрывок исходника, сама постановка не восстанавливается — дальше в оригинале шло определение колонок и конкретное задание.
Глубже. Практически всегда за такой формулировкой следует один из четырёх типовых запросов, к которым стоит быть готовым: сумма/количество заказов по клиенту (GROUP BY customer_id), топ-N клиентов или продавцов (ORDER BY ... LIMIT либо DENSE_RANK), заказы за период с фильтром по датам и индексом на created_at, и нарастающий итог/сравнение с предыдущим периодом через оконные функции (SUM(...) OVER (PARTITION BY ... ORDER BY ...), LAG). Если такое задание дают устно — сначала уточните схему и типы, потом проговорите план запроса, потом пишите.
Что вернет следующий запрос?
Заголовок раздела «Что вернет следующий запрос?»Коротко. Обрывок исходника, текста запроса нет — восстановить нельзя. Такие вопросы почти всегда проверяют одну из известных ловушек SQL.
Глубже. Ловушки, которые стоит держать наготове: NULL в сравнениях (x = NULL → NULL, NOT IN (1, NULL) → пусто, NULL <> NULL → NULL); разница COUNT(*), COUNT(col) и COUNT(DISTINCT col) при NULL; агрегат без GROUP BY по пустой выборке (COUNT → 0, SUM → NULL); LEFT JOIN + условие на правую таблицу в WHERE (превращается в INNER); целочисленное деление 5/2 = 2; ORDER BY без детерминированного тай-брейка; UNION молча убирает дубликаты; HAVING без GROUP BY работает по всей выборке как одна группа; WHERE date_col = '2024-01-01' не находит строки с временем внутри дня для timestamp; LIKE 'abc%' использует индекс, а LIKE '%abc' — нет.
Допустим мы создаем таблицу:
Заголовок раздела «Допустим мы создаем таблицу:»Коротко. Обрывок исходника: DDL не приведён. Обычно за этим следует разбор того, какие ограничения и типы вы поставите и что за этим стоит.
Глубже. Чек-лист, который стоит проговорить при любом CREATE TABLE: суррогатный первичный ключ bigint GENERATED ALWAYS AS IDENTITY (или естественный, если он реально стабилен); NOT NULL по умолчанию на всё, что обязательно; правильные типы — text вместо varchar(n) без нужды, numeric для денег (не float), timestamptz вместо timestamp, boolean вместо smallint; внешние ключи с осознанным ON DELETE (RESTRICT/CASCADE/SET NULL); UNIQUE на естественные ключи; CHECK на инварианты (qty > 0); индексы под фактические запросы, а не «на все колонки»; DEFAULT now() для created_at. Отдельно: колонки-флаги низкой селективности индексировать бессмысленно, а вот частичный индекс WHERE deleted_at IS NULL часто окупается.
Допустим мы имеем базу данных из следующих таблиц
Заголовок раздела «Допустим мы имеем базу данных из следующих таблиц»Коротко. Обрывок исходника, перечень таблиц отсутствует. По следующим двум вопросам видно, что речь шла о схеме products / orders / order_items — классической связке many-to-many между заказами и товарами.
Глубже. Разумная реконструкция схемы, к которой относятся следующие два вопроса:
CREATE TABLE products ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL, price numeric(12,2) NOT NULL CHECK (price >= 0));
CREATE TABLE orders ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, user_id bigint NOT NULL REFERENCES users(id), created_at timestamptz NOT NULL DEFAULT now());
CREATE TABLE order_items ( order_id bigint NOT NULL REFERENCES orders(id) ON DELETE CASCADE, product_id bigint NOT NULL REFERENCES products(id) ON DELETE RESTRICT, qty int NOT NULL CHECK (qty > 0), price numeric(12,2) NOT NULL, PRIMARY KEY (order_id, product_id));при удалении записи из таблицы products гарантированно не удалялись все записи из таблицы order_items с тем же продуктом?
Заголовок раздела «при удалении записи из таблицы products гарантированно не удалялись все записи из таблицы order_items с тем же продуктом?»Коротко. Нужен внешний ключ с ON DELETE RESTRICT (или NO ACTION, который стоит по умолчанию): СУБД просто не даст удалить товар, на который ссылаются строки заказов — удаление упадёт с ошибкой foreign key violation.
Глубже. Разница RESTRICT и NO ACTION в PostgreSQL: RESTRICT проверяет сразу и не может быть отложен, NO ACTION при DEFERRABLE INITIALLY DEFERRED проверяется в конце транзакции — то есть можно удалить товар и в той же транзакции переставить ссылки. Правильное продуктовое решение обычно не «удалять товар», а помечать его снятым с продажи (is_archived/deleted_at), потому что исторические заказы должны остаться корректными. И обязательно: цена и название товара в момент заказа фиксируются в order_items (снимок), иначе история заказов поедет при изменении карточки товара. Отдельная эксплуатационная деталь — на колонках-ссылках (order_items.product_id) нужен индекс, иначе каждое удаление товара приводит к seq scan order_items для проверки FK.
ALTER TABLE order_items ADD CONSTRAINT order_items_product_fk FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE RESTRICT;CREATE INDEX ON order_items (product_id);при удалении из таблицы заказов автоматически удалялись все записи этого заказа из order_items?
Заголовок раздела «при удалении из таблицы заказов автоматически удалялись все записи этого заказа из order_items?»Коротко. Внешний ключ с ON DELETE CASCADE:
ALTER TABLE order_items ADD CONSTRAINT order_items_order_fk FOREIGN KEY (order_id) REFERENCES orders(id) ON DELETE CASCADE;Глубже. Каскад выполняется самой СУБД в той же транзакции; это надёжнее, чем удалять из приложения двумя запросами. Подводные камни: массовое удаление заказов при каскаде порождает лавину удалений в дочерних таблицах (и в их дочерних) — под нагрузкой это долгие блокировки, поэтому удаляют батчами; каскад не вызывает ON DELETE-логику приложения (кеши, поисковый индекс не узнают); каскадные удаления не видны в EXPLAIN основного запроса. Альтернативы — ON DELETE SET NULL (когда связь необязательна) и мягкое удаление. И снова про индекс: без индекса на order_items(order_id) каждое удаление заказа сканирует всю дочернюю таблицу (в этой схеме индекс есть — это префикс первичного ключа (order_id, product_id)).
Нужно описать модель библиотеки. Есть 3 сущности: “Автор ”, “Книга ”, “Читатель ”. Физически книга только одна и может быть только у одного читателя. Нужно составить таблицы для библиотеки так чтобы это учесть.
Заголовок раздела «Нужно описать модель библиотеки. Есть 3 сущности: “Автор ”, “Книга ”, “Читатель ”. Физически книга только одна и может быть только у одного читателя. Нужно составить таблицы для библиотеки так чтобы это учесть.»Коротко. authors, books, связующая book_authors (у книги может быть много авторов и наоборот), readers и таблица выдач loans. Условие «экземпляр один и может быть только у одного читателя» выражается уникальным частичным индексом на невозвращённые выдачи книги.
CREATE TABLE authors ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL);
CREATE TABLE books ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, title text NOT NULL);
CREATE TABLE book_authors ( book_id bigint NOT NULL REFERENCES books(id) ON DELETE CASCADE, author_id bigint NOT NULL REFERENCES authors(id) ON DELETE RESTRICT, PRIMARY KEY (book_id, author_id));CREATE INDEX ON book_authors (author_id, book_id);
CREATE TABLE readers ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, name text NOT NULL);
CREATE TABLE loans ( id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY, book_id bigint NOT NULL REFERENCES books(id), reader_id bigint NOT NULL REFERENCES readers(id), taken_at timestamptz NOT NULL DEFAULT now(), returned_at timestamptz);
-- ключевой инвариант: у книги не может быть двух открытых выдачCREATE UNIQUE INDEX loans_one_active_per_book ON loans (book_id) WHERE returned_at IS NULL;Глубже. Именно частичный уникальный индекс — тот ответ, который отличает сильного кандидата: он даёт и историю выдач, и жёсткую гарантию «книга у одного читателя» на уровне СУБД, без триггеров и без гонок. Упрощённая альтернатива (books.current_reader_id nullable) тоже удовлетворяет условию, но теряет историю. Если бы у библиотеки было несколько экземпляров одной книги, появилась бы сущность book_copies (физический экземпляр), и loans ссылался бы на неё — этот нюанс стоит проговорить, показав, что вы отличаете «произведение» от «экземпляра».
Написать запрос - выбрать названия всех книг которые на руках
Заголовок раздела «Написать запрос - выбрать названия всех книг которые на руках»Коротко.
SELECT b.titleFROM books bJOIN loans l ON l.book_id = b.id AND l.returned_at IS NULL;Глубже. С уникальным частичным индексом на активные выдачи дубликатов быть не может; без него безопаснее SELECT DISTINCT или EXISTS. Если модель упрощена до books.current_reader_id, запрос превращается в SELECT title FROM books WHERE current_reader_id IS NOT NULL. Обратная задача — «книги, свободные сейчас» — это NOT EXISTS (не NOT IN, из-за NULL):
SELECT b.titleFROM books bWHERE NOT EXISTS ( SELECT 1 FROM loans l WHERE l.book_id = b.id AND l.returned_at IS NULL);Написать запрос - выбрать названия всех книг в библиотеке у которых больше 3 авторов
Заголовок раздела «Написать запрос - выбрать названия всех книг в библиотеке у которых больше 3 авторов»Коротко.
SELECT b.titleFROM books bJOIN book_authors ba ON ba.book_id = b.idGROUP BY b.id, b.titleHAVING COUNT(*) > 3;Глубже. Считать по связующей таблице достаточно — там пары уникальны благодаря первичному ключу; если бы дубликаты были возможны, нужен COUNT(DISTINCT ba.author_id). Группировка по b.id (первичный ключ) позволяет вывести b.title без добавления его в GROUP BY — PostgreSQL это разрешает благодаря функциональной зависимости, но писать оба поля привычнее и переносимее. Вариант без JOIN, иногда быстрее при большой books:
SELECT b.titleFROM books bWHERE (SELECT COUNT(*) FROM book_authors ba WHERE ba.book_id = b.id) > 3;Написать запрос - выбрать имена топ 3 читаемых авторов на данный момент
Заголовок раздела «Написать запрос - выбрать имена топ 3 читаемых авторов на данный момент»Коротко. «На данный момент» = считаем только активные выдачи (returned_at IS NULL):
SELECT a.name, COUNT(*) AS books_on_handsFROM loans lJOIN book_authors ba ON ba.book_id = l.book_idJOIN authors a ON a.id = ba.author_idWHERE l.returned_at IS NULLGROUP BY a.id, a.nameORDER BY books_on_hands DESC, a.nameLIMIT 3;Глубже. Тонкости: у книги может быть несколько авторов, поэтому одна выдача засчитывается каждому соавтору — это надо проговорить как продуктовое решение (альтернатива — делить вес выдачи). Тай-брейк в ORDER BY обязателен, иначе при равенстве счётчиков результат недетерминирован; если нужны «все авторы, попавшие в топ-3 по числу», используйте DENSE_RANK() OVER (ORDER BY COUNT(*) DESC) <= 3 в подзапросе вместо LIMIT 3. Если бы «читаемые» означало «за всё время», условие WHERE убирается и считается по всем выдачам, возможно с фильтром по периоду l.taken_at >= now() - interval '30 days'.
Найти пользователей, которые совершили покупок на сумму больше 5000р. Вывести их имена в формате id пользователя | имя | фамилия | сумма покупок
Заголовок раздела «Найти пользователей, которые совершили покупок на сумму больше 5000р. Вывести их имена в формате id пользователя | имя | фамилия | сумма покупок»Коротко.
SELECT u.id, u.first_name, u.last_name, SUM(oi.qty * oi.price) AS totalFROM users uJOIN orders o ON o.user_id = u.idJOIN order_items oi ON oi.order_id = o.idGROUP BY u.id, u.first_name, u.last_nameHAVING SUM(oi.qty * oi.price) > 5000ORDER BY total DESC;Глубже. Если сумма заказа уже хранится денормализованно в orders.total_amount, запрос упрощается до SUM(o.total_amount) без второго JOIN. Что стоит уточнить у интервьюера: учитывать ли отменённые/возвращённые заказы (обычно нужен WHERE o.status = 'paid'), нужен ли период, и что делать с пользователями без покупок (они не должны попасть — HAVING их и так отсечёт, но при LEFT JOIN сумма была бы NULL). Фильтр по агрегату — только в HAVING; повторять выражение можно, а можно обернуть в подзапрос/CTE и фильтровать по алиасу. Деньги — numeric, иначе на float появятся копеечные расхождения.
Какие проблемы возникают при работе хэш-таблицы?
Заголовок раздела «Какие проблемы возникают при работе хэш-таблицы?»Коротко. Коллизии и их обработка, деградация при плохой хеш-функции или атаке на коллизии (hash flooding), стоимость рехеширования при росте, лишняя память при низкой заполненности, отсутствие порядка (нельзя диапазонный поиск и сортированный обход) и плохая локальность обращений к памяти.
Глубже. В контексте баз данных это выливается в две конкретные вещи. Первая — hash-индекс в PostgreSQL: поддерживает только =, не умеет диапазоны, сортировку и многоколоночность, не даёт index-only scan; до PG 10 он вообще не журналировался в WAL и не переживал падение, из-за чего многие до сих пор считают его непригодным. B-tree почти всегда предпочтительнее, hash имеет смысл только на очень длинных значениях, где B-tree-ключ был бы огромным. Вторая — Hash Join и HashAggregate: хеш-таблица строится в work_mem, и при недооценке числа строк планировщиком она не помещается в память и уходит в батчи на диск (в EXPLAIN ANALYZE видно Batches: N Disk: ... kB) — запрос резко замедляется. Ещё сюда же: перекос данных (много строк с одинаковым ключом) делает один бакет огромным и убивает преимущество хеширования.
При каких условиях в хэш-таблице происходит эвакуация?
Заголовок раздела «При каких условиях в хэш-таблице происходит эвакуация?»Коротко. «Эвакуация» — термин из рантайма Go, а не из SQL: в старой реализации map (до Go 1.23 включительно) при росте создавался новый массив бакетов вдвое больше, и старые бакеты переносились в него постепенно — по 1–2 бакета на каждую операцию записи или удаления.
Глубже. Условий роста было два: превышение фактора загрузки — в среднем больше 6.5 элементов на бакет (тогда B увеличивался на 1, размер удваивался), и слишком много overflow-бакетов при нормальной загрузке (тогда делался «same-size grow»: перестроение того же размера, чтобы уплотнить данные после массовых удалений). Пока шла эвакуация, мапа держала два массива — buckets и oldbuckets, — и чтение проверяло, эвакуирован ли нужный бакет. В Go 1.24 встроенная мапа переписана на Swiss Tables (internal/runtime/maps), где вместо oldbuckets/эвакуации применяется деление на таблицы через directory: перестраивается только одна таблица, а не вся мапа. Если вопрос всё же задан на собеседовании по БД — уточните, о чём речь: о Go-мапе или о рехешировании hash-индекса/Hash Join в СУБД.
Изоляция узлов: Ноды одной и той же БД не могут находиться на одном физическом сервере;
Заголовок раздела «Изоляция узлов: Ноды одной и той же БД не могут находиться на одном физическом сервере;»Коротко. Это не вопрос, а требование к развёртыванию: primary и реплики (или узлы шардированного кластера) обязаны жить в разных доменах отказа, иначе репликация не защищает ни от чего — падение одного гипервизора уносит весь кворум.
Глубже. Как это обеспечивается на практике: в Kubernetes — podAntiAffinity с topologyKey: kubernetes.io/hostname (жёсткий requiredDuringScheduling...) и распределение по зонам через topologySpreadConstraints; на железе — разные стойки, питание и сетевые каналы. Уровни доменов отказа: процесс → виртуалка → хост → стойка → зона доступности → регион. Важные следствия: синхронная репликация между зонами добавляет к каждому commit сетевой RTT (обычно 1–3 мс внутри региона), поэтому конфигурацию выбирают исходя из требований RPO/RTO; для кворумных систем число узлов делают нечётным и раскладывают так, чтобы потеря одной зоны не лишала кворума; отдельно проверяют, что бэкапы и WAL-архив лежат не там же, где данные. Ещё стоит упомянуть split brain и роль внешнего арбитра/DCS (etcd, Consul) в failover-решениях вроде Patroni.
C какими БД работал?
Заголовок раздела «C какими БД работал?»Коротко. См. выше «С какой бд работал?» — дубль вопроса. Отвечайте одинаково: основная СУБД + версия + масштаб + конкретные задачи, затем вспомогательные хранилища.
Глубже. Отличие в том, что множественное число прямо приглашает показать широту. Хорошая структура — «источник истины / кеш / аналитика / очередь»: PostgreSQL как OLTP, Redis как кеш и распределённые блокировки, ClickHouse или BigQuery под аналитику, Kafka как лог событий, S3 под файлы. Для каждой позиции держите наготове по одной истории с цифрами и по одной граблям («autovacuum не успевал, таблица разбухла на 40 ГБ — включили агрессивные настройки для конкретной таблицы»). Ответ «работал с Postgres и Mongo» без деталей закрывает тему за 15 секунд не в вашу пользу.
Что можешь рассказать про анализирование SQL запросов?
Заголовок раздела «Что можешь рассказать про анализирование SQL запросов?»Коротко. Основной инструмент — EXPLAIN (ANALYZE, BUFFERS): показывает выбранный план, оценку и фактическое число строк, время по узлам и обращения к страницам. Плюс pg_stat_statements, чтобы найти, какие запросы вообще стоит анализировать, и auto_explain для отлова медленных запросов в проде.
Глубже. Методика: сначала находим топ по суммарному времени (pg_stat_statements по total_exec_time, а не только по среднему — тысяча быстрых запросов часто дороже одного медленного), затем берём конкретный запрос и смотрим EXPLAIN (ANALYZE, BUFFERS, VERBOSE, SETTINGS). На что смотреть: (1) расхождение rows= (оценка) и actual rows в разы — планировщику не хватает статистики, помогают ANALYZE, увеличение default_statistics_target, CREATE STATISTICS для коррелированных колонок; (2) Seq Scan по большой таблице с селективным фильтром — не хватает индекса или фильтр не саргабельный (функция над колонкой, несовпадение типов, LIKE '%x'); (3) Rows Removed by Filter — читаем много, отдаём мало; (4) Batches > 1 / Sort Method: external merge Disk — не хватает work_mem; (5) shared read против shared hit — сколько ушло мимо кеша; (6) Nested Loop с большим числом итераций там, где нужен Hash Join. Обязательно замечание: EXPLAIN ANALYZE реально выполняет запрос, поэтому UPDATE/DELETE анализируют внутри транзакции с ROLLBACK. Для чтения планов удобны визуализаторы (explain.dalibo.com, explain.depesz.com). И не забывайте про уровень выше SQL: N+1 из ORM и отсутствие батчинга видны только в трассировке приложения.
Базовые вопросы по БД;
Заголовок раздела «Базовые вопросы по БД;»Коротко. Это пометка из плана интервью, а не конкретный вопрос: за ней идёт стандартный набор по ACID, индексам, JOIN, нормализации, уровням изоляции и транзакциям.
Глубже. Минимальный набор, который нужно уметь отвечать без подготовки: что такое ACID и что означает каждая буква; чем INNER отличается от LEFT JOIN; что такое индекс, почему B-tree и когда индекс не используется; нормальные формы до 3NF и зачем денормализация; уровни изоляции и какие аномалии каждый допускает; чем WHERE отличается от HAVING; UNION против UNION ALL; что делает EXPLAIN; как устроен первичный и внешний ключ; что такое транзакция и что происходит при ROLLBACK; чем DELETE отличается от TRUNCATE (второй не построчный, не вызывает ON DELETE-триггеры для строк, требует ACCESS EXCLUSIVE, но транзакционен в PostgreSQL).
Какие бывают джоины и какой по дефолту в pg?
Заголовок раздела «Какие бывают джоины и какой по дефолту в pg?»Коротко. Виды: INNER, LEFT/RIGHT/FULL OUTER, CROSS, NATURAL, SELF, LATERAL. По умолчанию, если написать просто JOIN без ключевого слова, это INNER JOIN; слово OUTER в LEFT OUTER JOIN тоже необязательно.
Глубже. Второй смысл вопроса — какой физический алгоритм по умолчанию: его нет, планировщик каждый раз выбирает между Nested Loop, Hash Join и Merge Join по стоимости. Ориентиры: Nested Loop выигрывает, когда внешняя сторона даёт мало строк и на внутренней есть индекс по ключу соединения; Hash Join — когда одна сторона помещается в work_mem и соединение эквивалентное; Merge Join — когда обе стороны уже отсортированы (например, приходят из index scan). Проверить выбор можно EXPLAIN, а временно запретить метод — SET enable_hashjoin = off (только для диагностики, не в проде). Полезно также назвать Semi Join/Anti Join как результат EXISTS/NOT EXISTS и напомнить, что NATURAL JOIN в проде не используют: добавление одноимённой колонки молча меняет смысл запроса.
В приложении лог показывает, что время выполнения стало 1 сек при той же нагрузке. Какие ресурсы загруженности базы данных мы смотрим?
Заголовок раздела «В приложении лог показывает, что время выполнения стало 1 сек при той же нагрузке. Какие ресурсы загруженности базы данных мы смотрим?»Коротко. Смотрим четыре группы: CPU, дисковый ввод-вывод (и попадание в кеш), память (work_mem, shared buffers, спиллы), и ожидания — блокировки, соединения, лаг репликации. Начинать удобнее не с ресурсов, а с pg_stat_activity.wait_event_type: он сразу говорит, во что упёрлись — в Lock, IO, LWLock или в CPU.
Глубже. Конкретный чек-лист. pg_stat_activity: сколько активных сессий, есть ли idle in transaction (долгие транзакции держат горизонт и блокировки), какие wait_event. pg_stat_statements: изменилось ли mean_exec_time у конкретного запроса или выросло число вызовов — то есть это деградация плана или рост нагрузки. pg_stat_database: blks_hit/blks_read (cache hit ratio), deadlocks, temp_files/temp_bytes (спиллы из-за work_mem), xact_rollback. pg_stat_user_tables: n_dead_tup, last_autovacuum — bloat и отставший autovacuum. pg_locks — кто кого ждёт. Системные метрики: %util и await на дисках, iowait, saturation CPU, свободная память и swap, checkpoint-активность (log_checkpoints, «пила» при checkpoint_completion_target). Отдельно: лаг репликации (pg_stat_replication), исчерпание пула pgbouncer (запросы ждут соединения, а база при этом простаивает), и внезапный SELECT от аналитика, вымывший shared buffers. Типичные причины «то же самое, но в 10 раз медленнее»: таблица выросла и план сменился с Index Scan на Seq Scan; autovacuum не отработал и таблица раздулась; кто-то забыл индекс на новой колонке фильтра; блокировка от миграции; закончилось место в page cache и данные перестали помещаться в память.
Как в PostgreSQL физически происходит изменение и удаление строк?
Заголовок раздела «Как в PostgreSQL физически происходит изменение и удаление строк?»Коротко. Обновления «на месте» нет. UPDATE пишет новую версию строки и проставляет старой xmax текущей транзакции; DELETE только проставляет xmax, ничего физически не удаляя. Старые версии остаются в heap как мёртвые кортежи и освобождаются позже процессом VACUUM.
Глубже. Механика MVCC: у каждой версии строки есть xmin (транзакция, создавшая её) и xmax (транзакция, удалившая/обновившая). Видимость определяется снимком транзакции, поэтому читатели никогда не блокируют писателей. Следствия: (1) UPDATE одного поля переписывает всю строку целиком и требует места в странице; (2) обновление обычно означает вставку новых записей во все индексы таблицы — кроме случая HOT-обновления (Heap-Only Tuple), когда не менялись индексируемые колонки и в той же странице есть место: тогда новая версия связывается со старой через указатель в странице и индексы не трогаются; (3) частые обновления порождают bloat — таблица и индексы растут, сканы замедляются; лечится настройкой autovacuum (autovacuum_vacuum_scale_factor на горячих таблицах), fillfactor меньше 100 для увеличения шансов на HOT, и при запущенном случае — VACUUM FULL (эксклюзивная блокировка) или pg_repack (онлайн). VACUUM также обновляет FSM и visibility map и продвигает relfrozenxid, защищая от transaction ID wraparound. Ещё важная деталь: долгая открытая транзакция удерживает горизонт xmin, из-за чего VACUUM не может убрать мёртвые версии во всей базе — классическая причина внезапного bloat. TRUNCATE, в отличие от DELETE, просто создаёт новый пустой файл отношения и мгновенно освобождает место.
вывести одной таблицей названия городов и количество пользователей из этих городов; так, чтобы города, пользователей из которых нет, отсутствовали в выборке; так, чтобы города, пользователей из которых нет, присутствовали в выборке со значением 0; так, чтобы города, пользователей из которых нет, присутствовали в выборке со значением null;
Заголовок раздела «вывести одной таблицей названия городов и количество пользователей из этих городов; так, чтобы города, пользователей из которых нет, отсутствовали в выборке; так, чтобы города, пользователей из которых нет, присутствовали в выборке со значением 0; так, чтобы города, пользователей из которых нет, присутствовали в выборке со значением null;»Коротко. Три варианта: INNER JOIN (города без пользователей выпадают), LEFT JOIN + COUNT(u.id) (даёт 0), LEFT JOIN + SUM(CASE WHEN u.id IS NOT NULL THEN 1 END) или NULLIF(COUNT(u.id), 0) (даёт NULL).
-- 1) только города, где есть пользователиSELECT c.name, COUNT(*) AS usersFROM cities cJOIN users u ON u.city_id = c.idGROUP BY c.id, c.name;
-- 2) все города, пустые — с нулёмSELECT c.name, COUNT(u.id) AS usersFROM cities cLEFT JOIN users u ON u.city_id = c.idGROUP BY c.id, c.name;
-- 3) все города, пустые — с NULLSELECT c.name, SUM(CASE WHEN u.id IS NOT NULL THEN 1 END) AS usersFROM cities cLEFT JOIN users u ON u.city_id = c.idGROUP BY c.id, c.name;-- эквивалент: NULLIF(COUNT(u.id), 0)Глубже. Суть вопроса — понимать, что COUNT никогда не возвращает NULL: по пустой группе он даёт 0, поэтому третий вариант требует либо SUM (агрегат по одним лишь NULL даёт NULL), либо явного NULLIF. И различать COUNT(*) (считает строки, включая «пустую» правую сторону LEFT JOIN — вернёт 1 для города без пользователей) и COUNT(u.id) (считает не-NULL значения — вернёт 0). Ещё одна корректная форма для варианта 2 и 3 — коррелированный подзапрос в SELECT, который для варианта 3 естественно даёт NULL, если написать его как (SELECT NULLIF(count(*),0) ...); на больших объёмах он обычно медленнее группировки.
оставить в выводе только те города, количество пользователей из которых больше 1;
Заголовок раздела «оставить в выводе только те города, количество пользователей из которых больше 1;»Коротко. Фильтр по агрегату — в HAVING:
SELECT c.name, COUNT(u.id) AS usersFROM cities cLEFT JOIN users u ON u.city_id = c.idGROUP BY c.id, c.nameHAVING COUNT(u.id) > 1;Глубже. Здесь LEFT JOIN уже избыточен: условие > 1 всё равно отсекает пустые города, поэтому можно писать INNER JOIN — и это будет дешевле. Ключевое, что проверяют: попытка написать WHERE COUNT(u.id) > 1 — синтаксическая ошибка, потому что WHERE выполняется до агрегации. Алиас users в HAVING в PostgreSQL тоже использовать нельзя (в отличие от MySQL) — либо повторяем выражение, либо оборачиваем в подзапрос/CTE и фильтруем снаружи в WHERE.
Есть сервис, который обновляет инфузорным о юзере у себя в БД и еще в каком-то сервисе. Что тут может пойти не так и как с этим бороться?
Заголовок раздела «Есть сервис, который обновляет инфузорным о юзере у себя в БД и еще в каком-то сервисе. Что тут может пойти не так и как с этим бороться?»Коротко. Это классическая проблема dual write: две записи в разные системы не атомарны — вторая может упасть, зависнуть или выполниться дважды, и данные разъедутся. Стандартное решение — transactional outbox: в одной локальной транзакции пишем и данные, и запись в таблицу-исходящих; отдельный воркер (или CDC) доставляет её во внешний сервис с ретраями и идемпотентностью.
Глубже. Что именно ломается: (1) БД записала, внешний вызов упал — рассинхрон; (2) внешний вызов прошёл, а локальная транзакция откатилась — «фантомное» обновление снаружи; (3) таймаут — ответ не получен, но операция могла выполниться, наивный ретрай даст дубль; (4) конкурентные обновления доходят в разном порядке и «побеждает» старое значение. Инструменты: outbox + at-least-once доставка + идемпотентные обработчики (ключ идемпотентности, INSERT ... ON CONFLICT DO NOTHING на стороне получателя); версионирование/монотонные метки (updated_at, version), чтобы отбрасывать устаревшие обновления (last-write-wins по версии, а не по времени прихода); saga с компенсирующими действиями, если операцию нельзя просто повторить; периодическая сверка (reconciliation job), которая находит расхождения; RabbitMQ/Kafka как транспорт с гарантией повторной доставки. Двухфазный коммит (XA) упоминают, но в микросервисах избегают: он блокирующий, плохо масштабируется и требует поддержки от всех участников. Отдельно проговорите, что вызывать внешний сервис внутри открытой транзакции БД нельзя — это удерживает блокировки и горизонт xmin на всё время сетевого вызова.
Как устроены БД, состоящие из нескольких нод? Как работает consistent hashing при добавлении новых нод?
Заголовок раздела «Как устроены БД, состоящие из нескольких нод? Как работает consistent hashing при добавлении новых нод?»Коротко. Многоузловые БД делятся на два ортогональных механизма: репликация (одни и те же данные на нескольких узлах — для отказоустойчивости и чтения) и шардирование/партиционирование (разные данные на разных узлах — для масштабирования объёма и записи). Consistent hashing раскладывает ключи по кольцу хешей: узел занимает точки на кольце, ключ достаётся первому узлу по часовой стрелке; при добавлении узла переезжает только ~K/N ключей, а не вся раскладка, как при hash(key) % N.
Глубже. Топологии: single-leader (PostgreSQL primary + реплики), multi-leader (гео-репликация с конфликтами), leaderless с кворумом (Dynamo, Cassandra: W + R > N). Согласованность обеспечивают консенсусом (Raft/Paxos в etcd, CockroachDB, TiDB) или кворумом с последующим read-repair и anti-entropy. Про consistent hashing важны детали: (1) виртуальные узлы (vnodes) — каждый физический узел занимает сотни точек на кольце, иначе распределение получается перекошенным и при добавлении узла нагрузка снимается только с одного соседа; (2) репликация — ключ пишется на N следующих по кольцу узлов; (3) при добавлении узла он забирает диапазоны у соседей, данные переезжают в фоне (streaming), пока трафик обслуживают старые владельцы; (4) при удалении узла его диапазоны наследует следующий по кольцу. Альтернативы: диапазонное шардирование с ребалансировкой (CockroachDB, HBase) — лучше для range-запросов, но требует координатора и склонно к hot spot на монотонных ключах; фиксированное число слотов (16384 в Redis Cluster), которые переносятся между узлами. Для PostgreSQL шардирование строят вручную по ключу тенанта или через Citus.
Что такое union и как он работает?
Заголовок раздела «Что такое union и как он работает?»Коротко. См. выше: UNION объединяет результаты нескольких SELECT по вертикали и удаляет дубликаты, UNION ALL — не удаляет. Требуется одинаковое число колонок и совместимые типы.
Глубже. Механика в PostgreSQL: UNION ALL — это узел Append, который просто последовательно отдаёт строки подзапросов (или MergeAppend, если нужен сохранённый порядок сортировки). UNION добавляет сверху дедупликацию: HashAggregate по всем колонкам или Sort + Unique — то есть материализацию всего результата и расход work_mem. Отсюда практическое правило: UNION ALL по умолчанию, UNION — только когда дубликаты реально возможны и вредны. Типы колонок приводятся к общему (int + numeric → numeric), имена берутся из первого запроса, NULL при дедупликации считаются равными.
Что можно сделать на стороне PostgreSQL, чтобы выборка за период перестала быть узким местом?
Заголовок раздела «Что можно сделать на стороне PostgreSQL, чтобы выборка за период перестала быть узким местом?»Коротко. Индекс по колонке периода (B-tree на created_at, лучше составной (tenant_id, created_at) под реальный фильтр), секционирование таблицы PARTITION BY RANGE (created_at) для partition pruning и дешёвого удаления старых данных, а для больших append-only таблиц — BRIN-индекс. Плюс убрать функции над колонкой в WHERE, чтобы индекс вообще применялся.
Глубже. Полный набор мер по возрастанию цены. (1) Саргабельность: WHERE created_at >= $1 AND created_at < $2 вместо WHERE date_trunc('day', created_at) = $1 (или индекс по выражению, если переписать нельзя); следить, чтобы типы совпадали (timestamptz против date). (2) Составной индекс в правильном порядке: сначала колонки равенства, потом диапазон — (user_id, created_at), а не наоборот. (3) Покрывающий индекс с INCLUDE (...) для index-only scan — тогда heap не читается вовсе (при актуальной visibility map, то есть при работающем VACUUM). (4) Частичный индекс, если горячий только свежий период или только определённый статус. (5) BRIN: крошечный индекс на естественно упорядоченной по времени таблице, даёт огромную экономию памяти на терабайтных объёмах. (6) Декларативное секционирование по диапазону дат: планировщик отсекает лишние секции, DROP/DETACH PARTITION заменяет долгий DELETE, VACUUM идёт посекционно. (7) Предагрегация: материализованное представление или таблица-витрина с посуточными итогами, обновляемая инкрементально — если запрос агрегирует, а не выбирает строки. (8) Пагинация keyset вместо OFFSET. (9) Вынос тяжёлой аналитики на реплику или в ClickHouse. (10) Настройки: work_mem под сортировки, актуальный ANALYZE, effective_cache_size. И всегда сначала EXPLAIN (ANALYZE, BUFFERS) — чтобы понять, упирается запрос в чтение страниц, в сортировку или в неправильный план.
Есть 3 отдельных БД с заказами по каждому виду транспорта.
Заголовок раздела «Есть 3 отдельных БД с заказами по каждому виду транспорта.»Коротко. Обрывок исходника: сама задача не восстанавливается. Дальше в оригинале почти наверняка требовалось получить общую выборку/отчёт по заказам всех видов транспорта, лежащих в разных базах.
Глубже. Как отвечать, если такое условие прозвучало. Варианты решения по возрастанию сложности: (1) агрегация на стороне приложения — параллельно опросить три базы и слить результаты в памяти, работает, пока нужны небольшие срезы и не нужны кросс-базовые JOIN и точная сортировка с пагинацией; (2) postgres_fdw + UNION ALL (или наследование/секционирование поверх внешних таблиц) — SQL получается единый, но планировщик ограничен в push-down и всё упирается в сеть; (3) отдельное аналитическое хранилище, куда данные всех трёх баз стекаются через CDC/ETL (Debezium → Kafka → ClickHouse) — правильный путь, если нужны отчёты и агрегаты; (4) объединить базы, если разделение не оправдано доменными границами. Ключевые вопросы к интервьюеру перед ответом: нужен ли реальный time-консистентный срез, какие объёмы и латентность, нужны ли JOIN между базами, допустимо ли отставание данных. Отдельно стоит проговорить сквозные проблемы: пересекающиеся идентификаторы заказов (нужен составной ключ «источник + id» или UUID), разные схемы и словари статусов, отсутствие распределённых транзакций.
Допустим у нас проблема - есть info ручка, она отдает ответ 500 или очень долго обрабатывает запрос. Внутри нее происходит чтение из базы данных. Как диагностировать эту проблему и как ее решить? Потом мы узнали что bottle-neck это чтение из базы, как теперь решать проблему?
Заголовок раздела «Допустим у нас проблема - есть info ручка, она отдает ответ 500 или очень долго обрабатывает запрос. Внутри нее происходит чтение из базы данных. Как диагностировать эту проблему и как ее решить? Потом мы узнали что bottle-neck это чтение из базы, как теперь решать проблему?»Коротко. Диагностика идёт сверху вниз: сначала метрики ручки (RPS, доля 5xx, p50/p95/p99 латентности) и логи с текстом ошибки, потом распределённая трассировка или тайминги внутри хендлера, чтобы понять, где именно уходит время — в БД, в походе к соседнему сервису или в CPU самого сервиса. Если время уходит в БД, дальше смотрим pg_stat_statements (какой запрос даёт основной total_exec_time), EXPLAIN (ANALYZE, BUFFERS) на этом запросе, pg_stat_activity (ожидания, блокировки) и утилизацию пула соединений. Лечение зависит от диагноза: индекс под реальный предикат, переписывание запроса, keyset-пагинация вместо OFFSET, устранение N+1, кэш, реплика для чтения, материализованное представление.
Глубже. Важно проговорить, что 500 и «долго» — это часто одна и та же причина с разными симптомами: запрос упирается в таймаут (statement_timeout, дедлайн context, таймаут пула) и превращается в ошибку. Поэтому первым делом стоит развести три гипотезы: (1) сам SQL медленный, (2) SQL быстрый, но соединений в пуле не хватает и запросы стоят в очереди на Acquire, (3) запросы блокируются на замках, которые держит чужая транзакция. Различаются они дёшево: pg_stat_statements покажет mean_exec_time (если он мал, а ручка тормозит — проблема не в самом запросе), метрики пула ((*sql.DB).Stats().WaitCount/WaitDuration, у pgxpool — Stat().EmptyAcquireCount) закроют вторую гипотезу, а pg_stat_activity с wait_event_type = 'Lock' и pg_locks — третью. Отдельно проверяем, не выросли ли данные: план, который был Index Scan на 10 тысячах строк, при 10 миллионах может честно превратиться в Seq Scan, а раздутая (bloat) таблица без работающего autovacuum читается кратно дольше.
Когда бутылочное горлышко локализовано как чтение из БД, порядок действий — от дешёвого к дорогому. Сначала запрос и схема: посмотреть EXPLAIN (ANALYZE, BUFFERS), убедиться, что нет Seq Scan по большой таблице, что оценка строк планировщиком (rows=) близка к фактической (actual rows=) — расхождение в разы означает устаревшую статистику (ANALYZE) или коррелированные предикаты (CREATE STATISTICS). Добавить составной индекс в правильном порядке колонок (равенство → диапазон → сортировка), при возможности покрывающий (INCLUDE), чтобы получить Index Only Scan; убрать функции над колонкой в WHERE (WHERE lower(email) = $1 не использует индекс по email, нужен индекс по выражению). Выбросить SELECT *, если ручке нужно пять полей из сорока: это и сеть, и TOAST-разжатие. Проверить N+1 — типичная причина, когда «ручка info» делает один запрос за списком и по одному за каждым элементом.
Дальше — архитектурные меры: кэш (Redis или in-memory с коротким TTL) для действительно «инфо»-данных, которые меняются редко; вынос тяжёлых read-only запросов на физическую реплику (с явным пониманием, что реплика отстаёт и данные могут быть слегка устаревшими); материализованное представление или предагрегированная таблица, обновляемая по расписанию, если ручка считает агрегаты по большому объёму; денормализация горячих полей. Параллельно обязательно ставим защиту: statement_timeout на пользовательские запросы, дедлайн в context, ограничение и корректный размер пула, circuit breaker и деградация (отдать частичный ответ или закэшированный, а не 500). Наконец, если объём данных растёт линейно во времени — партиционирование по дате, чтобы запросы за период читали одну-две партиции вместо всей таблицы.
-- топ запросов по суммарному времениSELECT queryid, calls, total_exec_time, mean_exec_time, rows, queryFROM pg_stat_statementsORDER BY total_exec_time DESCLIMIT 20;
-- кто чего ждёт прямо сейчасSELECT pid, state, wait_event_type, wait_event, now() - query_start AS dur, queryFROM pg_stat_activityWHERE state <> 'idle'ORDER BY dur DESC;Что такое хэш-таблица?
Заголовок раздела «Что такое хэш-таблица?»Коротко. Хэш-таблица — структура данных для ассоциативного массива: ключ прогоняется через хэш-функцию, полученное число отображается в номер бакета (обычно взятием остатка или младших бит), и в этом бакете лежит значение. Средняя сложность вставки, поиска и удаления — O(1), худшая — O(n) при массовых коллизиях; порядок обхода не определён.
Глубже. Два принципиальных механизма — разрешение коллизий и рост. Коллизии решают либо цепочками (в бакете список/слайс элементов), либо открытой адресацией (линейное/квадратичное пробирование, double hashing) — второй вариант дружелюбнее к кэшу процессора, но чувствителен к коэффициенту заполнения. Когда load factor (элементов на бакет) превышает порог, таблица расширяется вдвое и элементы перехэшируются; в Go это делается инкрементально, чтобы не получить длинную паузу на одной вставке. В Go 1.24 реализация map переехала на Swiss Tables (открытая адресация с группами по 8 слотов и SIMD-сравнением метаданных) — это изменило внутренности и дало ускорение, но контракт остался прежним: порядок итерации случаен, брать адрес элемента карты нельзя, конкурентные запись и чтение без синхронизации ловятся детектором и приводят к fatal error: concurrent map writes.
В контексте баз данных хэш-таблица — не абстракция, а рабочий инструмент исполнителя запросов. Hash Join строит хэш-таблицу по меньшей стороне соединения в work_mem и проходит по большей, зондируя её; если не влезает — план деградирует в batched hash join с записью на диск. HashAggregate так же группирует строки для GROUP BY. Есть и отдельный тип индекса hash (только для =), который до PostgreSQL 10 не писался в WAL и потому не переживал репликацию и восстановление; с 10-й версии он полноценный, но в 99% случаев B-tree всё равно предпочтительнее, потому что поддерживает диапазоны и сортировку. Наконец, хэширование — основа hash-партиционирования и шардирования (в распределённых системах — consistent hashing, чтобы добавление узла перекладывало ~1/N ключей, а не все).
Что такое varchar?
Заголовок раздела «Что такое varchar?»Коротко. varchar(n) — строковый тип переменной длины с ограничением сверху: хранится ровно столько символов, сколько записали, но не больше n. Попытка записать больше — ошибка (в PostgreSQL; в MySQL в нестрогом режиме будет усечение с предупреждением).
Глубже. В PostgreSQL n считается в символах, а не в байтах, максимум — 10 485 760. Физически значение хранится как varlena: заголовок в 1 байт для коротких строк (до 126 байт) или 4 байта для длинных, плюс сами данные. Если строка не влезает в страницу, включается TOAST: значение сжимается, а при необходимости выносится в отдельную TOAST-таблицу кусками — поэтому чтение большого текстового поля стоит дополнительных обращений. Важный практический момент: в PostgreSQL varchar без указания длины, varchar(n) и text — это один и тот же движок хранения, разница только в проверке длины; никакого выигрыша в производительности от маленького n нет, и text + CHECK (length(x) <= n) даёт ту же семантику с более дешёвой миграцией (изменить CHECK легче, чем тип). Отсюда распространённая рекомендация в PG-мире: использовать text, а ограничения длины ставить осознанно, если они действительно бизнес-требование.
В MySQL/InnoDB картина другая: VARCHAR(n) хранит 1–2 байта длины плюс данные, n тоже в символах, но лимит строки — 65 535 байт на всю строку целиком, а длинные значения могут выноситься в overflow-страницы. Там же длина влияет на размер временных таблиц и на длину индекса, поэтому выбор n не так безобиден, как в PostgreSQL. И в обеих СУБД varchar — не место для хранения того, что имеет собственный тип: даты, числа, UUID, JSON и enum лучше хранить соответствующими типами — это и валидация, и компактность, и индексируемость.
Почему иногда лучше char, чем varchar? (он быстрее)
Заголовок раздела «Почему иногда лучше char, чем varchar? (он быстрее)»Коротко. Формулировка «char быстрее» верна не везде. В PostgreSQL это миф: char(n) дополняется пробелами до фиксированной длины и по документации обычно даже медленнее varchar/text из-за лишнего места и обрезки пробелов. В MySQL/InnoDB и особенно в старых движках с фиксированной длиной строки CHAR действительно может выигрывать: строки одинакового размера позволяют вычислять смещение записи арифметикой и не вызывают фрагментации при обновлениях.
Глубже. Разумный ответ звучит так: char(n) имеет смысл, когда данные объективно фиксированной длины и короткие — код валюты char(3), код страны ISO char(2), хэш фиксированного размера, статус из двух букв. Тогда экономится байт-другой заголовка длины и снимается вопрос о вариативности. Во всех остальных случаях char — источник багов: он дополняет значение пробелами справа, и при сравнении по стандарту эти пробелы игнорируются, а при конкатенации, выгрузке в файл или сравнении на стороне приложения — нет. Классический продовый инцидент: значение записали в char(20), прочитали в Go и сравнили с константой — не совпало из-за хвостовых пробелов. Отдельно стоит упомянуть, что для маленьких доменов с фиксированным набором значений лучше не char, а enum-тип, справочная таблица с FK или smallint-код — это и меньше места, и настоящая валидация.
Какую БД и как использовали?
Заголовок раздела «Какую БД и как использовали?»Коротко. Это вопрос про опыт, а не про факт; интервьюер хочет услышать конкретику: название СУБД и версию, объём данных и профиль нагрузки, какие задачи она закрывала, и хотя бы один нетривиальный случай, где вы принимали решение и понимали его последствия.
Глубже. Хороший каркас ответа — четыре части. Первая: контекст — «PostgreSQL 14 как основное хранилище сервиса биллинга, около 300 ГБ, самая большая таблица — транзакции, ~400 млн строк, профиль OLTP, пик 2 тыс. запросов в секунду, из них 90% чтения». Вторая: как именно работали — драйвер и слой доступа (pgx v5 + sqlc, миграции через goose, пул на 30 соединений, pgbouncer в transaction pooling), схема (партиционирование по месяцу, составные индексы, внешние ключи включены), транзакции (Read Committed по умолчанию, Repeatable Read для отчётов). Третья: конкретная задача с результатом — «страница истории тормозила на OFFSET при глубокой пагинации, перешли на keyset по (created_at, id), p99 упал с 1,8 с до 60 мс». Четвёртая: что бы сделали иначе сейчас — это показывает рефлексию.
Типичные ошибки: отвечать одним словом «постгрес»; называть технологии, которых не трогали руками (проверят уточняющим вопросом); говорить «данных было много» без цифр; описывать только CRUD. Если реальный опыт скромный — так и скажите, но добавьте, что именно делали сами: «схему проектировал я, писал миграции, разбирал два инцидента с блокировками при ALTER TABLE». Честная конкретика ценится выше раздутого списка.
Использовали ли у себя постгрес и если дa, то какие интересные задачи решали с ее помощью?
Заголовок раздела «Использовали ли у себя постгрес и если дa, то какие интересные задачи решали с ее помощью?»Коротко. Вопрос-приглашение показать, что вы знаете PostgreSQL глубже, чем SELECT/INSERT. Отвечать надо одной-двумя историями, где использовалась специфичная возможность PG и была измеримая польза.
Глубже. Заготовьте истории из тех областей, которые реально трогали, например: транзакционный outbox с FOR UPDATE SKIP LOCKED вместо брокера на старте проекта (и объяснение, почему SKIP LOCKED даёт конкурентных воркеров без взаимных блокировок); полнотекстовый поиск на tsvector + GIN-индекс, чтобы не тащить Elasticsearch ради тысячи документов; JSONB для полей с изменчивой схемой плюс GIN-индекс по jsonb_path_ops для поиска по вложенным ключам; партиционирование по времени с DETACH PARTITION для дешёвого удаления старых данных вместо DELETE на миллионы строк; LISTEN/NOTIFY для инвалидации кэша; расширения — PostGIS для геозапросов, pg_stat_statements для профилирования, pgcrypto, TimescaleDB для метрик; логическая репликация для миграции без даунтайма; EXCLUDE-ограничение с tstzrange для запрета пересекающихся бронирований — это очень выигрышный пример, потому что показывает, что целостность можно переложить на БД.
Обязательно назовите и подводные камни, с которыми столкнулись: ALTER TABLE ... ADD COLUMN с DEFAULT до PG 11 переписывал таблицу, а сейчас нет; создание индекса блокирует запись, если не CREATE INDEX CONCURRENTLY; долгие транзакции держат горизонт xmin и мешают autovacuum; MVCC означает, что UPDATE — это новая версия строки, а не правка на месте, отсюда bloat и важность настройки autovacuum. Такие детали убеждают, что опыт настоящий.
Что такое view?
Заголовок раздела «Что такое view?»Коротко. View (представление) — именованный сохранённый SQL-запрос, который выглядит как таблица. Своих данных он не хранит: на этапе перезаписи запроса планировщик подставляет тело представления в основной запрос и строит общий план. Нужен для инкапсуляции сложной логики, для стабильного контракта над меняющейся схемой и для разграничения доступа.
Глубже. Ключевые нюансы для PostgreSQL. Представление можно обновлять (INSERT/UPDATE/DELETE), если оно «просто устроено» — один источник в FROM, без агрегатов, DISTINCT, GROUP BY, оконных функций и UNION; такие auto-updatable views работают с версии 9.3, а более сложные случаи закрываются триггером INSTEAD OF. WITH CHECK OPTION не даёт вставить через представление строку, которая в него не попадёт. Для безопасности: security_barrier мешает планировщику протащить внутрь пользовательские функции-предикаты, которые могли бы утечь данные, а с PostgreSQL 15 есть security_invoker = true — представление читает таблицы с правами вызывающего, что удобно вместе с RLS. Важно помнить и про цену: обычное представление ничего не ускоряет, и вложенные представления поверх представлений часто рождают планы, где планировщик уже не может протолкнуть предикат вниз.
Отдельная сущность — материализованное представление (CREATE MATERIALIZED VIEW, с 9.3): оно хранит результат физически, читается быстро, но устаревает и требует REFRESH MATERIALIZED VIEW. Обычный REFRESH берёт ACCESS EXCLUSIVE и блокирует чтение; REFRESH ... CONCURRENTLY (с 9.4) читателей не блокирует, но требует уникального индекса на представлении и работает дольше. Автоматического инкрементального обновления в ванильном PostgreSQL нет — это частая ошибка на собеседовании.
CREATE VIEW active_users ASSELECT id, email, created_atFROM usersWHERE deleted_at IS NULLWITH CHECK OPTION;
CREATE MATERIALIZED VIEW daily_orders ASSELECT date_trunc('day', created_at) AS day, count(*) AS cnt, sum(amount) AS totalFROM ordersGROUP BY 1;
CREATE UNIQUE INDEX ON daily_orders (day); -- нужен для CONCURRENTLYREFRESH MATERIALIZED VIEW CONCURRENTLY daily_orders;Как обеспечить целостность данных в бд?
Заголовок раздела «Как обеспечить целостность данных в бд?»Коротко. Через декларативные ограничения схемы — NOT NULL, PRIMARY KEY, UNIQUE, FOREIGN KEY с осмысленными ON DELETE/ON UPDATE, CHECK, доменные типы — плюс транзакции с подходящим уровнем изоляции. Всё, что можно проверить в БД, надо проверять в БД: приложений и инстансов много, база одна, и она последняя линия обороны.
Глубже. Полезно разложить по классическим уровням. Сущностная целостность — первичный ключ, который уникален и не NULL; в PostgreSQL часто это bigint GENERATED ALWAYS AS IDENTITY или UUIDv7-подобный ключ (случайный UUIDv4 плох как ключ кластеризации из-за разброса вставок по индексу). Ссылочная целостность — внешние ключи; здесь стоит уметь объяснить разницу между ON DELETE RESTRICT/NO ACTION (запретить удаление родителя — как раз то, что нужно для «нельзя удалить товар, если на него есть заказы»), CASCADE (удалить детей вместе с родителем — уместно для позиций заказа) и SET NULL. Отдельно: FK не индексируется автоматически со стороны потомка, и без такого индекса каскадные операции и удаления родителей становятся медленными. Доменная целостность — правильные типы (timestamptz, а не text; numeric для денег, а не float), CHECK, ENUM, DOMAIN. Пользовательская — триггеры и EXCLUDE-ограничения (например, запрет пересечения интервалов бронирования через EXCLUDE USING gist (room_id WITH =, period WITH &&)).
Второй слой — транзакционный. Атомарность даёт BEGIN/COMMIT, но одной атомарности мало: гонки вида «прочитал остаток, проверил, списал» ломаются на Read Committed (уровень по умолчанию в PostgreSQL). Лечится либо блокировкой (SELECT ... FOR UPDATE), либо повышением изоляции до REPEATABLE READ/SERIALIZABLE с ретраями на ошибку сериализации (код 40001), либо, что чаще всего лучше, переносом проверки в ограничение: CHECK (balance >= 0) не обманешь никакой гонкой. Конструкции INSERT ... ON CONFLICT DO NOTHING/UPDATE дают идемпотентную вставку без гонки чтения-записи. Ограничения можно объявить DEFERRABLE INITIALLY DEFERRED, если внутри транзакции состояние временно нарушает инвариант (циклические ссылки).
Третий слой — за пределами одной БД: согласованность между сервисами обеспечивается не 2PC, а транзакционным outbox (запись в бизнес-таблицу и в таблицу событий в одной транзакции, отдельный воркер публикует), идемпотентными ключами у обработчиков и сагами с компенсациями. И операционная часть: бэкапы с проверкой восстановления, pg_dump/PITR через WAL-архив, контрольные запросы-инварианты в мониторинге, миграции, которые не ломают старую версию приложения (expand/contract).
С какими базами данных вы работали?
Заголовок раздела «С какими базами данных вы работали?»Коротко. См. выше про «Какую БД и как использовали» — тот же вопрос в более широкой формулировке. Отличие в том, что здесь ждут карту: несколько систем разных классов и понимание, зачем в проекте была каждая.
Глубже. Оптимальная структура — перечислить 3–5 систем и к каждой дать одну фразу «зачем она там была»: PostgreSQL как основное транзакционное хранилище; Redis как кэш и распределённая блокировка (и честное замечание, что как primary storage он подходит не всегда); ClickHouse для аналитики и хранения событий; Kafka как лог событий (уточнив, что это не БД, но часто в этом ряду упоминается); MongoDB там, где схема документов реально изменчива; SQLite в CLI-утилите или в тестах. Дальше интервьюер выберет одну и будет копать — поэтому не называйте систему, о которой не сможете рассказать хотя бы модель данных, способ индексирования и типичные грабли. Явно разделите «работал в проде и дежурил» / «использовал в пет-проекте» / «читал документацию» — это снимает риск провала на уточняющих вопросах и воспринимается как зрелость.
Разница между реляционными и нереляционными БД с точки зрения модели данных?
Заголовок раздела «Разница между реляционными и нереляционными БД с точки зрения модели данных?»Коротко. В реляционной модели данные — это набор отношений (таблиц) из кортежей с фиксированной, объявленной заранее схемой; связи выражаются значениями ключей и восстанавливаются во время запроса через JOIN, а сама модель нормализуется, чтобы каждый факт хранился ровно один раз. В нереляционных моделях единица хранения — самодостаточный агрегат: пара ключ-значение, документ, строка с семейством колонок или вершина/ребро графа; схема гибкая или отсутствует, связи чаще денормализованы внутрь агрегата, а join’ов либо нет, либо они ограничены.
Глубже. Правильно говорить не про «NoSQL» вообще, а про четыре разные модели. Key-value (Redis, etcd, DynamoDB в простом режиме): значение непрозрачно для БД, доступ только по ключу, отсюда предсказуемая O(1)-латентность и невозможность запроса «найди всех, у кого age > 30». Документная (MongoDB, Couchbase): значение — JSON/BSON-документ, БД понимает его структуру, умеет индексировать вложенные поля и фильтровать по ним; типичный приём — вложить связанные сущности внутрь документа, чтобы читать одним обращением. Wide-column (Cassandra, HBase): строка с динамическим набором колонок, и — ключевое — модель проектируется от запросов: сначала известны запросы, потом под них подбирается партиционный ключ и ключ кластеризации; одни и те же данные дублируются в нескольких таблицах. Графовая (Neo4j): вершины и рёбра первоклассны, обход связей стоит константу на шаг, а не join.
Из различия моделей вытекают практические следствия, которые и хотят услышать. В реляционной БД схема — контракт, который проверяет СУБД; в документной ответственность за форму данных лежит на приложении, и в одной коллекции легко получить пять поколений формата. В реляционной вы пишете декларативный запрос и планировщик сам решает как; в wide-column неудобный запрос просто невозможно выполнить эффективно, и надо менять модель. Реляционная даёт транзакции над произвольным набором строк; большинство нереляционных — атомарность в пределах одного документа или партиции. Реляционную сложнее шардировать именно из-за join’ов и внешних ключей между шардами; агрегатные модели шардируются естественно, потому что агрегат целиком лежит на одном узле.
Использовали ли хранимые функции в PostgreSQL?
Заголовок раздела «Использовали ли хранимые функции в PostgreSQL?»Коротко. Ответ по существу: да, в ограниченном объёме — там, где логика неотделима от данных (триггерные функции, сложные upsert-процедуры, обслуживающие задачи), и нет — для бизнес-логики приложения, потому что её тяжелее версионировать, тестировать и отлаживать, чем код сервиса.
Глубже. Что стоит знать по механике. Функции создаются через CREATE FUNCTION на разных языках: sql (простые, инлайнятся планировщиком, если функция на одном SELECT и объявлена не VOLATILE), plpgsql (процедурный, с переменными, циклами и обработкой исключений), C и расширения. Классификация волатильности критична для планировщика: IMMUTABLE (результат зависит только от аргументов — можно использовать в индексе по выражению и вычислить один раз), STABLE (постоянна в пределах одного запроса — подходит для функций, читающих таблицы), VOLATILE (по умолчанию, вычисляется на каждую строку). Неверно проставленная волатильность — источник и неверных результатов, и медленных планов. С PostgreSQL 11 появились процедуры (CREATE PROCEDURE + CALL), которые, в отличие от функций, умеют управлять транзакциями (COMMIT/ROLLBACK внутри) — это важно для батчевой обработки. С PostgreSQL 14 доступен стандартный синтаксис тела BEGIN ATOMIC ... END, при котором зависимости функции отслеживаются и тело парсится на этапе создания, а не при первом вызове.
Отдельно — безопасность и эксплуатация: SECURITY DEFINER выполняет функцию с правами владельца, и такую функцию обязательно надо создавать с зафиксированным search_path (SET search_path = pg_catalog, public), иначе это дыра. Триггерные функции возвращают trigger и пишутся почти всегда на plpgsql. Минусы, которые честно назвать: логика в БД не попадает в обычный код-ревью и CI, если не выгружается в миграции; отладка ограничена; при высокой нагрузке ошибка в функции ложится на CPU сервера БД, который масштабируется хуже, чем stateless-сервисы. Разумный компромисс — держать в БД только то, что требует атомарности и близости к данным, и обязательно версионировать все функции миграциями.
CREATE OR REPLACE FUNCTION set_updated_at() RETURNS triggerLANGUAGE plpgsql AS $$BEGIN NEW.updated_at := now(); RETURN NEW;END;$$;
CREATE TRIGGER users_set_updated_atBEFORE UPDATE ON usersFOR EACH ROW EXECUTE FUNCTION set_updated_at();Какие библиотеки для БД вы использовали?
Заголовок раздела «Какие библиотеки для БД вы использовали?»Коротко. В Go базовый слой — стандартный database/sql с драйвером; для PostgreSQL это github.com/jackc/pgx/v5 (можно как драйвер database/sql, а можно напрямую с pgxpool ради нативного протокола и типов), для MySQL — github.com/go-sql-driver/mysql. Поверх обычно один из: sqlx для удобного скана в структуры, sqlc для генерации типобезопасного кода из SQL, squirrel для сборки динамических запросов, из ORM — GORM или ent. Миграции — goose или golang-migrate.
Глубже. Стоит уметь объяснить, чем эти уровни отличаются и почему выбран конкретный. database/sql — это абстракция с пулом соединений и интерфейсом драйвера; сам он не умеет ни маппинга в структуры, ни специфичных типов PostgreSQL. pgx в нативном режиме даёт то, чего через database/sql не получить: бинарный протокол, полноценную поддержку jsonb, массивов, диапазонов, COPY FROM для быстрой массовой загрузки, batch-запросы, LISTEN/NOTIFY, настраиваемый кэш подготовленных выражений. Важная эксплуатационная деталь: при работе через pgbouncer в режиме transaction pooling подготовленные выражения на уровне сессии ломаются, поэтому в pgx нужно выбирать соответствующий режим выполнения запросов (QueryExecModeExec/SimpleProtocol) либо использовать pgbouncer версии с поддержкой prepared statements.
Про ORM полезно иметь взвешенную позицию: они экономят время на CRUD и связях, но прячут генерируемый SQL, легко порождают N+1 и мешают тонко управлять индексами и планами. Практичный подход, который хорошо звучит на собеседовании, — «SQL пишем сами, а рутину генерируем»: sqlc компилирует ваши .sql-файлы в Go-функции с типизированными параметрами и результатами, проверяя запросы по реальной схеме на этапе сборки. Для тестов стоит упомянуть testcontainers-go или dockertest (поднять настоящий PostgreSQL — надёжнее моков) и go-sqlmock для юнит-тестов слоя доступа. Не забудьте назвать pgx-совместимые метрики пула и то, как настраиваете SetMaxOpenConns/SetMaxIdleConns/SetConnMaxLifetime — вопрос «сколько соединений ставите и почему» задают часто; разумный ответ — исходя из числа ядер БД и характера нагрузки, а не «побольше», потому что избыточный пул только усиливает конкуренцию.
ctx := context.Background()
pool, err := pgxpool.New(ctx, os.Getenv("DATABASE_URL"))if err != nil { return err}defer pool.Close()
var email stringerr = pool.QueryRow(ctx, `SELECT email FROM users WHERE id = $1`, userID).Scan(&email)if errors.Is(err, pgx.ErrNoRows) { return ErrUserNotFound}В чем разница между реляционными и нереляционными БД?
Заголовок раздела «В чем разница между реляционными и нереляционными БД?»Коротко. См. выше разбор по модели данных. Если формулировка общая, ответ надо давать по четырём осям: модель данных и схема, язык и способ доступа, гарантии транзакций и согласованности, способ масштабирования.
Глубже. Свод по осям. Схема: в реляционной она объявлена и проверяется СУБД, изменение — миграция; в нереляционных schema-on-read, форма проверяется приложением. Язык: SQL декларативен и оптимизируется планировщиком, у NoSQL — свои API и языки (CQL похож на SQL синтаксисом, но не даёт произвольных join’ов). Транзакции: в реляционных ACID над произвольным набором строк — при этом важно не повторять устаревший тезис «в NoSQL нет транзакций»; MongoDB поддерживает многодокументные транзакции с версии 4.0 (и на шардированных кластерах с 4.2), в DynamoDB есть транзакционные операции. Масштабирование: реляционная классически масштабируется вертикально плюс реплики на чтение, а горизонтально — шардированием, которое приходится строить руками или брать распределённую SQL-СУБД (CockroachDB, YugabyteDB, Spanner); агрегатные модели шардируются из коробки. Согласованность: в распределённых системах вступает в силу CAP-компромисс, и Cassandra/DynamoDB дают настраиваемую согласованность (кворумы), тогда как одноузловой PostgreSQL просто строго согласован, а асинхронная реплика уже даёт eventual consistency при чтении с неё.
Финальная мысль, которую стоит озвучить: граница между лагерями размывается. PostgreSQL с JSONB и GIN-индексами закрывает большую часть документных сценариев; распределённые SQL-СУБД дают горизонтальное масштабирование с транзакциями; NoSQL-системы обзавелись транзакциями и вторичными индексами. Поэтому выбор делается не по ярлыку, а по конкретным требованиям: какая модель доступа доминирует, нужны ли транзакции через несколько сущностей, каков объём и профиль роста, какая допустима задержка согласованности и какая у команды экспертиза в эксплуатации.
Частые ошибки на собесе
Заголовок раздела «Частые ошибки на собесе»- Путают
WHEREиHAVINGи пытаются фильтровать по агрегату вWHERE; не знают логический порядок вычисления секций запроса. - Игнорируют семантику
NULL: пишут= NULL, используютNOT INс подзапросом, где возможенNULL, не помнят, чтоCOUNT(col)не считаетNULL, аCOUNTникогда не возвращаетNULL. - В
LEFT JOINставят условие на правую таблицу вWHEREвместоONи молча получаютINNER JOIN. - Говорят «Postgres — это CA по CAP», не оговариваясь, что CAP относится к распределённой системе, и что выбор появляется только с репликацией и её режимом синхронности.
- Называют дефолтным уровнем изоляции в PostgreSQL
Repeatable Read(это MySQL/InnoDB); не знают, что на Repeatable Read и Serializable приложение обязано ретраить ошибку40001. - Считают, что
UPDATEв PostgreSQL меняет строку на месте, и не могут объяснить, откуда берутся bloat и зачем нужен VACUUM. - Предлагают триггеры как основной способ реализации бизнес-логики и не называют ни одной их проблемы (скрытая логика, стоимость на строку, каскады, невозможность внешних побочных эффектов).
- Отвечают на вопрос про масштабирование одним словом «шардирование», пропуская индексы, пул соединений, кеш, реплики и партиционирование, которые дешевле и почти всегда идут раньше.
- Экранируют пользовательский ввод вручную вместо параметризованных запросов; не знают, что имена таблиц и колонок параметрами не биндятся.
- Пишут
SELECT ... LIMITбезORDER BYс детерминированным тай-брейком и удивляются «плавающим» результатам и дублям в пагинации. - Начинают чинить медленную ручку сразу с «добавим индекс», не измерив, где именно теряется время; половина реальных случаев — N+1, исчерпанный пул соединений или блокировки, и индекс там не поможет.
- Считают, что
EXPLAINбезANALYZEпоказывает реальное время выполнения. Он показывает только план и оценки планировщика; факт даётEXPLAIN (ANALYZE, BUFFERS), и запускать его наUPDATE/DELETEнадо внутри транзакции с откатом. - Утверждают, что
charбыстрееvarchar, не оговаривая СУБД. В PostgreSQL это неверно: документация прямо говорит, чтоcharacter(n)обычно самый медленный из трёх строковых типов. - Думают, что
varchar(50)в PostgreSQL экономит место или ускоряет работу по сравнению сtext. Хранение идентично, разница только в проверке длины. - Называют view «кэшем» или «ускорением запроса». Обычное представление не хранит данные и ничего не ускоряет; данные хранит только материализованное, и его нужно явно обновлять — инкрементального автообновления в ванильном PostgreSQL нет.
- Говорят, что представление нельзя изменять. Простые представления в PostgreSQL обновляемы автоматически с 9.3, сложные — через триггер
INSTEAD OF. - Обеспечивают уникальность «проверкой в коде» (
SELECT, потомINSERT) и не видят гонки. Уникальность обеспечивает только уникальный индекс; идемпотентную вставку даётINSERT ... ON CONFLICT. - Отказываются от внешних ключей «ради производительности», не понимая цену: без FK целостность рано или поздно ломается, а стоимость обычно решается индексом на стороне потомка.
- Повторяют, что «в NoSQL нет транзакций и схемы». Транзакции есть у MongoDB с 4.0 и у DynamoDB; схема есть всегда — вопрос лишь в том, кто её проверяет, СУБД или приложение.
- На вопросы про опыт отвечают перечислением технологий без единой детали. Интервьюер ждёт кейс с цифрами, принятым решением и его последствиями.
Что почитать
Заголовок раздела «Что почитать»- PostgreSQL Documentation: Internals / Database Physical Storage и Concurrency Control — страницы, MVCC, уровни изоляции.
- PostgreSQL Documentation: Using EXPLAIN и pg_stat_statements.
- Егор Рогов. «PostgreSQL изнутри» (postgrespro.ru/education/books/internals) — лучший русскоязычный разбор MVCC, VACUUM, буферного кеша и планировщика.
- Martin Kleppmann. «Designing Data-Intensive Applications» — репликация, шардирование, CAP/PACELC, консистентность, dual write и outbox.
- PostgreSQL Wiki: Don’t Do This — компактный список антипаттернов схемы и запросов.
- PostgreSQL Documentation, «Character Types» — почему
char(n)в PG не быстрее: https://www.postgresql.org/docs/current/datatype-character.html - PostgreSQL Documentation, «CREATE VIEW» и «Rules on INSERT/UPDATE/DELETE»: https://www.postgresql.org/docs/current/sql-createview.html
- PostgreSQL Documentation, «Data Definition: Constraints» — полный набор ограничений целостности: https://www.postgresql.org/docs/current/ddl-constraints.html
- PostgreSQL Documentation, «Using EXPLAIN» и модуль
pg_stat_statements: https://www.postgresql.org/docs/current/using-explain.html - Martin Kleppmann, «Designing Data-Intensive Applications», гл. 2 — сравнение реляционной, документной и графовой моделей данных
- Документация pgx v5 и sqlc — практика слоя доступа к БД в Go: https://pkg.go.dev/github.com/jackc/pgx/v5 и https://docs.sqlc.dev/
Список исходных вопросов с привязкой к компаниям: ../questions/general.md