Открытый канал ID 77400161222906

Java. Менторинг за чашкой кода

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

Каналов в рубрике
1 729
Место этого канала
№ 225
Открыть рубрику →

Публикации за период

По реакциям

Поиск и вся лента — после бесплатной регистрации. Свои даты — по подписке; владелец канала видит их бесплатно. Сортировка по реакциям — по подписке. Зарегистрироваться

Загрузка…

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

Hibernate: Продвинутый уровень

1. Состояния Entity (Жизненный цикл)

Объект в Hibernate — это не просто данные, это объект с состоянием. Их три:
* Transient (Временный): Объект создан через `new User()`, но Session о нем не знает. В БД его нет.
* Persistent (Постоянный): Объект прикреплен к Session (`session.save(user)` или `session.get(User.class, id)`). Любое изменение этого объекта отслеживается (Dirty Checking) и при коммите улетит в БД.
* Detached (Отсоединенный): Объект был Persistent, но Session закрылась. Он жив в памяти, но Hibernate за ним больше не следит. Чтобы вернуть его в игру, используют `merge()`.

2. Методы get() vs load() (Классика жанра)
Разница в том, как они ходят в базу:
* `get()`: Идет в БД **сразу**. Если объекта нет — возвращает `null`.
* `load()`: Возвращает **Прокси-объект** (заглушку) сразу. В БД идет только тогда, когда вы попытаетесь обратиться к полям объекта (Lazy Initialization). Если объекта нет — при обращении к полям кинет `ObjectNotFoundException`.
* Совет: Используйте `get()`, если не уверены, что объект есть. Используйте `load()`, если уверены (например, для установки связи `order.setUser(user)`).

3. save() vs persist() vs saveOrUpdate()
Это главная боль Junior-разработчиков.
* `persist()`: Делает объект Persistent. Работает только внутри транзакции. Не делает `INSERT` сразу (может сделать позже при flush). Гарантирует соблюдение спецификации JPA.
* `save()`: Старое Hibernate-API. Возвращает `Serializable` (ID объекта). Может сделать `INSERT` сразу (вне транзакции), что ломает логику JPA.
* `saveOrUpdate()`: Устаревший метод. Если ID есть — делает `UPDATE`, если нет — `INSERT`. Сейчас лучше использовать `merge()`.

4. Кэширование (Уровни кэша)
* L1 Cache (Session Cache): Есть у каждой Session по умолчанию. Если в рамках одной сессии вы дважды попросили юзера с `id=1`, Hibernate пойдет в БД только один раз. Очищается при закрытии Session.
* L2 Cache (SessionFactory Cache): Общий для всех сессий. Нужен, если данные читаются часто, а меняются редко (например, справочники: города, страны). Настраивается через провайдеры (EhCache, Redis).

5. Lazy Loading и Проблема N+1
* Lazy: Связанные коллекции (`@OneToMany`) не грузятся из БД, пока вы к ним не обратитесь. Это экономит память.
* Проблема N+1: У вас есть 100 заказов. В цикле вы делаете `order.getItems()()`. Hibernate делает 1 запрос, чтобы найти 100 заказов, и еще 100 запросов, чтобы для каждого заказа найти его товары. Итого 101 запрос.
* Решение: `JOIN FETCH` в HQL (`FROM Order o JOIN FETCH o.items`) или `@EntityGraph`.

6. Flush и Commit
* `flush()`: Выгружает изменения из Session в БД (генерирует SQL), но **не завершает транзакцию**. Если после `flush()` случится ошибка, можно откатить.
* `commit()`: Завершает транзакцию. Обычно внутри себя делает `flush()`.
* Зачем вручную? Если вам нужно получить ID объекта *до* коммита (например, чтобы записать его в лог или передать в другой сервис).

Итог:
Hibernate кажется магией, пока вы не поймете, что это просто умный SQL-генератор, который держит граф объектов в памяти. Как только вы поймете, как работает Session и Proxy, 90% багов исчезнут.
Просмотры: 67
Перейти в пост Открыть в MAX Сообщить о неточности
🛠️ Словарь Java-разработчика: 10 терминов Hibernate, которые нужно знать

Hibernate — это не просто библиотека, это целый мир со своим языком. Если вы начинаете новый проект со Spring Boot, без этих определений вы «поплывете» на первом же вопросе техлида.

1. ORM (Object-Relational Mapping)
Это сама концепция. Мост между миром Java (объекты, классы, наследование) и миром БД (таблицы, строки, связи). Hibernate берет ваш класс `User` и сам превращает его в строку в таблице `users`.

2. Entity (Сущность)
Обычный Java-класс (POJO), помеченный аннотацией `@Entity`. Это «паспорт» объекта. Hibernate говорит: «Ага, этот класс — не просто код, это данные, которые я должен сохранить в БД».

3. Session (org.hibernate.Session)
Главный рабочий инструмент. Именно через `Session` вы просите: «Сохрани это», «Найди то», «Удали вон то». Важно: `Session` — это не поток (Thread), это обычно короткоживущий объект внутри транзакции.

4. SessionFactory (org.hibernate.SessionFactory)
Тяжелая, неповоротливая, но очень важная штука. Она производит `Session` и создается один раз при старте приложения. Она читает конфиги, маппинги и строит в памяти карту вашей базы данных.

5. JPA (Java Persistence API)
Это спецификация (набор правил и аннотаций), написанная Oracle. Hibernate — это реализация этой спецификации.
Аналогия: JPA — это USB-порт (стандарт), а Hibernate — это флешка, которую вы в него втыкаете. Вы можете заменить Hibernate на EclipseLink, и ваш код почти не изменится, потому что оба работают по правилам JPA.

6. HQL (Hibernate Query Language)
Язык запросов, похожий на SQL, но пишется для объектов, а не для таблиц.
*SQL:* `SELECT * FROM users WHERE name = 'Ivan'`
*HQL:* `FROM User u WHERE u.name = 'Ivan'`
Вы просите «Дай мне объекты User», а не «Дай мне строки из таблицы».

7. Lazy Loading (Ленивая загрузка)
Спасение от `OutOfMemoryError`. По умолчанию Hibernate не грузит связанные данные (например, список `Order` для `User`), пока вы явно их не попросите.
Проблема (N+1): Если в цикле попросить заказы для 100 пользователей, Hibernate сделает 101 запрос к БД. Лечится `JOIN FETCH`.

8. Dirty Checking (Проверка на грязь)
Магия Hibernate. Перед закрытием транзакции он сравнивает состояние объектов в памяти с тем, что было считано из БД. Если поле `name` изменилось, он сам сгенерирует `UPDATE`. Вам не нужно писать `save()`, если вы просто поменяли геттер/сеттер внутри транзакции.

9. Cascade (Каскадирование)
Правило «семейственности». Если вы удаляете `User`, что делать с его `Order`?
`CascadeType.ALL` скажет: «Удаляй всё вместе с хозяином». `CascadeType.PERSIST` скажет: «Если сохраняешь юзера, сохрани и его новый адрес автоматически».

10. Flush (Сброс)
Это момент, когда Hibernate берет накопленные в памяти изменения (`save`, `update`) и отправляет их в БД в виде SQL-запросов. Обычно происходит при коммите транзакции. Но `session.flush()` можно вызвать принудительно, если нужно, чтобы ID сгенерировался прямо сейчас (например, чтобы записать его в лог).

Итог: Hibernate берет на себя рутину (JDBC, ResultSet, Connection pooling), но взамен требует понимания того, как работает сессия и ленивая загрузка.
Просмотры: 77
Перейти в пост Открыть в MAX Сообщить о неточности
Всем привет, сегодня заглянем под капот JVM, где на самом деле живёт int в JVM?
Все пишут int a = 10;
но что под капотом делает JVM?
int — это не объект, а примитив.

Он не создаётся через new, не лежит в куче по умолчанию и не проходит через конструктор. Для него у JVM есть отдельные байт-код инструкции: iload — положить на стек, istore — сохранить обратно, iadd — сложить напрямую. Никаких вызовов методов, никакого заголовка объекта, никакого участия GC.

Где хранится int — зависит от того, чем является переменная:
- Локальная переменная
→ лежит в массиве locals текущего стекового фрейма. Компилятор превращает имя переменной в «номер слота», а рантайм просто читает и пишет по индексу. Метод завершился — фрейм вытолкнулся, и этот int исчез. Сборщику мусора тут делать нечего.

- Поле экземпляра → лежит внутри объекта в куче. У объекта фиксированный layout, а у поля есть смещение (offset). Есть ссылка на объект — прибавили смещение — нашли этот int.
- Статическое поле → живёт вместе с объектом Class, который сам по сути лежит в куче. Время жизни привязано к загрузчику классов.

Почему int такой «низкоуровневый»? Потому что Java с самого начала разделила типы на примитивные и ссылочные. int — это 32-битное знаковое число в дополнительном коде, диапазон от -2³¹ до 2³¹−1. JVM работает с ним как с числом в регистре, без всякой «распаковки».
И только когда int нужно использовать как объект (например, положить в List<Integer>), компилятор автоматически упаковывает его в Integer: вот тогда появляется объект, заголовок, куча и GC.

Коротко: int в JVM — это по сути «отрезок из 32 бит». Где он лежит — зависит от того, где ты его объявил. На стеке — живёт мгновение, в куче — вместе с объектом, статический — вместе с классом.
Просмотры: 58
Перейти в пост Открыть в MAX Сообщить о неточности
Всем привет,
Сегодня поговорим о таком недооцененно инструменте, как Row-Level Security (RLS) — политика безопасности на уровне строк. Это фича PostgreSQL (и других СУБД), которая работает как невидимый `WHERE`, автоматически добавляемый к каждому вашему запросу.

Как это работает?
Представьте таблицу `documents`. Там лежат документы и для бухгалтерии, и для юристов, и для гендиректора.
Обычно вы пишете: `SELECT * FROM documents WHERE department_id = 5`.

С RLS вы просто пишете: `SELECT * FROM documents`.
А база сама дописывает: `...AND current_user = documents.author`.

Пользователь `ivan` даже не увидит строки, созданные пользователем `petr`. Для него таблица `documents` будет содержать только его собственные файлы.

Где это спасает?

1. SaaS-сервисы (Борьба с «проблемой соседа»).
Самый частый баг в стартапах — когда один клиент видит данные другого из-за ошибки в `JOIN` или забытого `WHERE` в сложном отчете.
С RLS это невозможно физически. Вы ставите политику: `USING (tenant_id = current_setting('app.current_tenant')::int)`. Теперь, даже если разработчик напишет `SELECT * FROM orders` без фильтра по клиенту, база отдаст только заказы текущей компании.

2. Мягкое удаление (Soft Delete).
Вместо того чтобы писать `WHERE deleted_at IS NULL` в каждом запросе (и обязательно забыть его в одном из 100 мест), вы делаете это политикой RLS. Для всех «обычных» запросов удаленные строки просто перестают существовать. Они есть в таблице, но база их скрывает.

3. Аудит и защита от «слива».
Вы можете запретить `SELECT *` даже администратору БД (если он залогинен под обычным пользователем), ограничив видимость только определенными колонками или строками.

Подводный камень:
RLS — это не бесплатно. Это дополнительная нагрузка на планировщик. Если политик много и они сложные, время планирования запроса (`Planning Time`) может вырасти. И главное — если вы забудете включить `RLS ENABLE` на таблице, политика работать не будет.

RLS — это, если бэкенд ошибся, база данных не даст украсть данные. Он делает вашу систему по-настоящему защищенной
Просмотры: 79
Перейти в пост Открыть в MAX Сообщить о неточности
Всем привет, сегодня поговорим про индексы в PostgreSQL.

Мы привыкли думать, что индекс — это просто «скоростная шпаргалка» для поиска. Но у него есть темная сторона, о которой молчат.

Факт: Индекс живет своей отдельной жизнью
Когда вы обновляете всего одну строку в таблице, база данных не меняет её «по месту».
1. Она создает новую версию строки (это называется MVCC).
2. А в каждом индексе, который есть на этой таблице, она тоже создает новую запись.

Если у вас на таблице 10 индексов, то при изменении одной строки база делает 11 физических записей на диск.

Почему это важно?
Это главная причина, почему индексы нужно удалять.
Каждый «лишний» индекс не просто занимает место на SSD. Он превращает легкие операции `UPDATE` и `INSERT` в тяжелую работу. База вынуждена обновлять дерево индекса, пересортировывать его и записывать на диск снова и снова.

Например:
У вас есть колонка `last_login_date`. Вы решили сделать по ней индекс, «на всякий случай».
Теперь каждый раз, когда пользователь просто заходит на сайт, Postgres:
* Записывает новую строку в таблицу.
* Записывает новую запись в индекс `last_login_date`.
* Записывает новые записи во все остальные 5 индексов таблицы.

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

Как проверить?
Зайдите в `pg_stat_user_indexes`. Если вы видите индекс, у которого `idx_scan` (счетчик использования) равен 0, а `idx_tup_upd` (счетчик обновлений) растет — этот индекс вреден. Он ничего не ускоряет, но заставляет базу работать на износ при каждой записи.

Индекс — это не бесплатно. Это компромисс.
Правило: Если вы ищете по полю редко, а пишете в него часто — индекс, скорее всего, не нужен.
Просмотры: 74
Перейти в пост Открыть в MAX Сообщить о неточности
CTE (WITH) в PostgreSQL: как превратить лапшу из SQL в читаемый конвейер

Вы открываете запрос на 500 строк, написанный вашим предшественником. Там `JOIN` внутри `JOIN`, а в `WHERE` зарыт подзапрос, который считает среднее. Грязный ди это код? И нужно ли переписать это через CTE?

Что это такое?
Простыми словами: это временная именованная таблица, которую вы создаете в начале запроса и используете дальше.
Вместо того чтобы прятать логику глубоко в скобках, вы выносите её наверх, даете имя и используете как обычную таблицу.

Задача: найти сотрудников, чья зарплата выше средней по их отделу.

Без CTE (Спагетти-код):

SELECT e.name, b.name AS boss
FROM employees e
JOIN employees b ON e.boss_id = b.id
WHERE e.salary > (
SELECT AVG(salary)
FROM employees e2
WHERE e2.department_id = e.department_id
);

Какая тут проблема: Подзапрос в `WHERE` может выполняться для каждой строки. Читать это тяжело.

С CTE (Чистый код):

WITH avg_by_dept AS (
SELECT department_id, AVG(salary) as avg_sal
FROM employees
GROUP BY department_id
)
SELECT e.name, b.name as boss
FROM employees e
JOIN employees b ON e.boss_id = b.id
JOIN avg_by_dept a ON e.department_id = a.department_id
WHERE e.salary > a.avg_sal;

Почему это лучше? Мы читаем код сверху вниз, как обычную программу. Сначала подготовили «ингредиенты» (`avg_by_dept`), потом из них «приготовили» результат.

Магия конвейера
Настоящая сила CTE — в разбиении сложной задачи на шаги.
Представьте аналитику для интернет-магазина: нужно найти активных пользователей, посчитать их выручку и сегментировать. Без CTE это монстр. С CTE это конвейер:

WITH
-- Шаг 1: Кто покупал в этом месяце?
active_users AS (
SELECT user_id FROM orders WHERE order_date >= NOW() - INTERVAL '30 days'
),
-- Шаг 2: Сколько они потратили?
recent_revenue AS (
SELECT user_id, SUM(amount) as total
FROM orders
WHERE user_id IN (SELECT user_id FROM active_users)
GROUP BY user_id HAVING SUM(amount) > 1000
)
-- Финал: Собираем всё вместе
SELECT u.name, r.total
FROM users u
JOIN recent_revenue r ON u.id = r.user_id;

Каждая часть запроса — отдельный бизнес-этап. Тестировать и отлаживать такой код в разы проще.

Важный нюанс: Оптимизация (Ограда оптимизатора)
Это то, что нужно знать, чтобы не выстрелить себе в ногу.
* До Postgres 12: CTE всегда вычислялись целиком и сохранялись во временную память. Это был «барьер»: планировщик не мог заглянуть внутрь CTE и применить к нему фильтры. Если вы делали `WITH cte AS (SELECT * FROM huge_table) SELECT * FROM cte WHERE id = 1`, база сначала читала *всю* огромную таблицу, и только потом фильтровала.
* Postgres 12+: Теперь CTE по умолчанию «встраиваются» (inlined) в основной запрос, как обычный подзапрос. Оптимизатор умнеет.
* Когда использовать `MATERIALIZED`? Если вы используете тяжелый CTE несколько раз в запросе. Чтобы не считать его заново, напишите: `WITH heavy_calc AS MATERIALIZED (...)`.
* Когда использовать `NOT MATERIALIZED`? Если CTE большой, а вам из него нужна всего пара строк через `LIMIT`. Это заставит базу «протолкнуть» лимит внутрь CTE.

Рекурсивные CTE: Лифт по дереву
Нужно обойти дерево категорий или структуру компании (где есть `parent_id`)?
Раньше для этого писали циклы на бэкенде или хранимые процедуры. Теперь это 10 строк SQL:

WITH RECURSIVE org_tree AS (
-- База: Топ-менеджмент (у кого нет начальника)
SELECT id, name, parent_id, 1 AS level
FROM departments WHERE parent_id IS NULL
UNION ALL
-- Рекурсия: Ищем подчиненных
SELECT d.id, d.name, d.parent_id, t.level + 1
FROM departments d
JOIN org_tree t ON d.parent_id = t.id
)
SELECT * FROM org_tree ORDER BY level;

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

CTE — это не про скорость (хотя иногда помогает). Это про читаемость и поддерживаемость. Код, написанный через `WITH`, говорит: «Смотрите, я сначала считаю это, потом это, а в конце соединяю всё здесь».
Просмотры: 98
Перейти в пост Открыть в MAX Сообщить о неточности
Транзакции в PostgreSQL: почему счетчик может «сойти с ума» и как работает ложная вложенность

Мы привыкли воспринимать транзакции как должное: `BEGIN`, `COMMIT`, готово. Но под капотом Postgres скрывается механизм, который может остановить весь кластер, и архитектурная иллюзия, на которую ловятся даже сеньоры.

1. Счетчик транзакций — это бомба с часовым механизмом.
Каждая транзакция получает ID (XID). В старых версиях Postgres под него было выделено всего 32 бита — это чуть больше 4 миллиардов.
В высоконагруженной системе этот счетчик может прокрутиться за пару месяцев. Если это произойдет, база не сможет понять, какая транзакция была раньше, а какая позже. Это грозит потерей данных.
Чтобы этого не случилось, существует заморозка (Freezing). Процесс автоочистки (`autovacuum`) заранее помечает очень старые, но актуальные строки как «замороженные». Для них ID больше не нужен.
Фишка: если `autovacuum` не справляется (например, из-за долгой транзакции-аналитика), база начнет жестко тормозить, а при достижении лимита `autovacuum_freeze_max_age` просто откажется писать данные, чтобы избежать катастрофы.

2. Вложенных транзакций не существует
Вы пишете `BEGIN` внутри другого `BEGIN`? В большинстве СУБД это вызовет ошибку. В PostgreSQL верхний `BEGIN` просто молча проигнорируется.
Настоящей вложенности нет. Есть только Savepoints (точки сохранения).
Если внутри большой транзакции упал тяжелый шаг (например, отправка письма через API), вы делаете `ROLLBACK TO SAVEPOINT`. Это откатит только работу с письмом, но сохранит всё, что было сделано до точки сохранения.
Ошибка думать, что `BEGIN` внутри транзакции создал изолированный контекст. Если вы сделаете `COMMIT` во «вложенном» блоке, зафиксируется вся внешняя транзакция целиком.

3. Транзакция не завершена, пока не записан WAL
Многие думают, что `COMMIT` — это фиксация в таблицах. На самом деле Postgres сначала записывает изменения в журнал упреждающей записи (WAL) на диск, и только потом сообщает приложению: «Я всё».
Если делать миллионы крошечных транзакций (например, вставлять записи по одной в цикле), диск умрет от записи WAL раньше, чем кончится память.
Для массовой загрузки данных используйте `COPY` с параметром `FREEZE` (если таблица пустая) или группируйте операции в пакеты по 5-10 тысяч строк. Это сократит количество записей в WAL в сотни раз.
4. Долгая транзакция — киллер диска
Самая частая причина, по которой база раздувается с 50 ГБ до 500 ГБ за неделю — забытый `BEGIN` в Python-скрипте аналитика.
Пока транзакция открыта, `VACUUM` не имеет права удалять старые версии строк (Dead Tuples), потому что «древняя» транзакция всё еще может их захотеть увидеть.
Таблица превращается в швейцарский сыр, место на SSD заканчивается, а производительность падает до нуля. Золотое правило: открыли — сделали — закрыли. Никаких HTTP-запросов внутри транзакции.

Чек-лист:
1. Мониторьте возраст транзакций (`SELECT datname, age(datfrozenxid) FROM pg_database;`).
2. Никогда не делайте сетевые вызовы (HTTP, запросы к другим БД) внутри блока `BEGIN...COMMIT`.
3. Используйте `SAVEPOINT` для частичного отката сложной логики.
Просмотры: 84
Перейти в пост Открыть в MAX Сообщить о неточности
Когда Postgres сам себя обманывает (и как это починить)

Иногда индекс есть, запрос написан правильно, а база всё равно тормозит. Часто виноват «мозг» Postgres — планировщик. Это он решает, как именно читать ваши данные.

Как он думает?
Представьте, что планировщик — это навигатор. Но карт у него нет. Он ходит с биноклем (`ANALYZE`) на крышу и примерно запоминает: «Ага, в этой колонке в основном числа от 1 до 100, а всего в таблице миллион строк».

На основе этих заметок он строит маршрут.

Где он ошибается?

1. У него старые данные.
Вы закинули в таблицу миллион новых заказов, а «навигатор» об этом не знает. Он по старой памяти думает, что строк мало. И решает: «Да ну его, этот индекс, я просто пробегусь по всей таблице, это быстро».
Итог: грузит весь диск в память, всё висит.
*Чинится:* командой `ANALYZE orders;` (или просто подождать автовакуум).

2. Он боится прыгать по диску
В настройках базы (параметр `random_page_cost`) до сих пор стоит цифра `4`. Это значит: «Один прыжок в случайное место диска в 4 раза дороже, чем читать подряд».
Это правда для дискет из 90-х. У вас SSD! Для него прыжок почти ничего не стоит. Поставьте этот параметр в `1.1`. База перестанет бояться индексов и начнет ими пользоваться.

3. Вы спрятали колонку в функцию
Написали `WHERE created_at::date = '2026-01-01'`
Планировщик видит эту маску (`::date`) и говорит: «Я не знаю, что получится на выходе, поэтому индекс по `created_at` я использовать не буду».
*Как надо:* `WHERE created_at >= '2026-01-01' AND created_at < '2026-01-02'`.

Как проверить, что он там надумал?
Перед запросом напишите слово `EXPLAIN` (лучше `EXPLAIN ANALYZE`).
База покажет план. Смотрите на строчку `rows`. Если там написано `rows=10`, а на самом деле вернулось 100 000 — значит, у планировщика безнадежно устарели данные о таблице.
Просмотры: 77
Перейти в пост Открыть в MAX Сообщить о неточности
INCLUDE в PostgreSQL: почему ваш индекс всё ещё не работает (и как это починить)

`INCLUDE` — пожалуй самый недооцененный инструмент в арсенале DBA.

Что это такое?
Обычный B-tree индекс хранит только те колонки, по которым вы ищете и сортируете. Если запросу нужны другие поля, Postgres находит указатель в индексе, а потом идет в саму таблицу (heap) за данными. Это две операции ввода-вывода.

`INCLUDE` добавляет дополнительные колонки прямо в «листья» индекса. База находит строку по ключу и сразу забирает остальные данные оттуда же. Поход в таблицу отменяется.

Миф: INCLUDE ускоряет запись
Часто говорят, что `INCLUDE` нужен, чтобы не обновлять дерево индекса при изменении доп. колонок. Это не совсем так. При `INSERT` или `UPDATE` индекс всё равно переписывается.

Реальная магия `INCLUDE` кроется в другом:

1. Обход ограничений типов данных
Вы не можете положить геометрический тип `box` или некоторые пользовательские типы в ключ B-tree индекса — для них не определены операторы сравнения «больше-меньше».
Но их можно добавить через `INCLUDE`. Индекс будет искать по `id`, а `box` просто лежать рядом в листе.

2. Уникальность без потери покрытия
Вам нужно гарантировать уникальность по `email`, но запросы всегда тянут `user_id`.
Если добавить `user_id` в ключ индекса, база начнет сортировать данные еще и по нему. Это лишняя работа.

CREATE UNIQUE INDEX uq_users_email
ON users (email) INCLUDE (user_id);

Уникальность проверяется только по `email`, но запрос `SELECT user_id FROM users WHERE email = '...'` ответит прямо из индекса.

3. Спасение от «неправильной» сортировки
Если добавить колонку в ключ (например, `CREATE INDEX ON logs (user_id, created_at)`), база будет поддерживать порядок и по `user_id`, и по `created_at`.
Если вам не нужно искать диапазон дат, а нужно просто подтянуть `created_at` для конкретного `user_id` — вы заставляете дерево индекса делать лишнюю работу по поддержке порядка.
`INCLUDE (created_at)` говорит базе: «Храни это здесь, но не трать ресурсы на пересортировку индекса по этой дате».

Когда это выстрелит?
Только когда планировщик сможет использовать *Index Only Scan*. Для этого страница таблицы должна быть «чистой» для вашей транзакции.
Если в таблице много `UPDATE` и `DELETE`, регулярный `VACUUM` критически важен — именно он обновляет Visibility Map, который говорит индексу: «Этим данным в куче можно верить, в таблицу не ходи». Если карта видимости грязная, `INCLUDE` не спасет — база всё равно пойдет в heap проверять видимость строк.
Просмотры: 45
Перейти в пост Открыть в MAX Сообщить о неточности
VACUUM в PostgreSQL: почему DELETE не удаляет, а база пухнет

В PostgreSQL `DELETE` не стирает данные. Он просто делает строку «невидимой». Физически она остается на диске. Спасибо MVCC — так читатели не блокируют писателей.

Как это работает?
Представьте кусок швейцарского сыра. `VACUUM` выедает дырки (мертвые строки). Размер куска не меняется, но новые данные теперь можно лить в эти пустоты. Без блокировок таблицы.

Зачем это нужно, кроме экономии места?
1. Статистика. Если ее не обновлять, планировщик запросов начнет тупить и жечь CPU.
2. XID Wraparound. Если не замораживать старые транзакции, счетчик ID переполнится, и база уйдет в `read-only`. Чинить такое больно.

Виды уборки:
• `VACUUM` — просто помечает место как свободное. Файл на диске меньше не станет.
• `VACUUM FULL` — пересоздает таблицу с нуля, возвращает место ОС. Блокирует таблицу наглухо. На проде лучше заменить на `pg_repack`.
• `AUTOVACUUM` — фоновый уборщик.

Как не положить прод?
Дефолтный авто-vacuum ленив. Для тяжелых таблиц режьте настройки:

ALTER TABLE heavy_table
SET (autovacuum_vacuum_scale_factor = 0.01, -- вместо дефолтных 0.2
autovacuum_vacuum_threshold = 500,
autovacuum_work_mem = '1GB');

Чек-лист:
Зайдите в `pg_stat_user_tables`. Если в `n_dead_tup` миллионы строк, а `last_autovacuum` был давно — база задыхается.

После большой заливки данных не забывайте делать:
`VACUUM (VERBOSE, ANALYZE) your_table;`

А у вас `AUTOVACUUM` вывозит или ручками чистите?
Просмотры: 62
Перейти в пост Открыть в MAX Сообщить о неточности
Вся лента — после бесплатной регистрации
ВКонтакте Телеграм Одноклассники
Избранное в datool.ru

Следите за каналом в личном кабинете

Зарегистрируйтесь в datool.ru, чтобы добавить канал «Java. Менторинг за чашкой кода» в избранное и отслеживать его показатели.

Зарегистрироваться
Для владельцев каналов

Подтвердите свой канал

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

Войти или зарегистрироваться

Сообщить о неточности

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

Вы покидаете datool.ru

Ссылка ведёт на внешний сайт. datool.ru не отвечает за его содержимое — проверьте адрес, прежде чем вводить пароли или платёжные данные.

Переход через 10 с

Перейти