Экономичный индекс
Только Макс набрал себе кофе и уселся в кресло, как в кабинет влетел Вася из сопровождения системы отчётности. Глаза у него были круглые от страха:
– Макс, беда! Сработал алерт – места на диске БД меньше 5%! Помоги! Что мне делать?
Макс спокойно отхлебнул кофе:
– Отправь заявку, чтобы добавили диски...
– Заявку уже отправил, но ты же знаешь наш банк – месяц будут добавлять! И ты знаешь моего начальника – он мне голову оторвёт.
Всё это Макс знал. Он подавил раздражение от того, что его снова отвлекают, и отхлебнул кофе:
– Ладно, давай посмотрим, от чего у вас там можно избавиться.
– Избавиться? Ты с ума сошел? Там нельзя ничего удалять.
Но Макс уже подключился к БД, запустил давно выверенный запрос и изучал список объектов БД отсортированный по размеру. Спустя минуту он нашел первого подозреваемого:
– Вася, твой B-tree индекс по дате транзакции весит уже 60 Гб. Предлагаю заменить его на BRIN.
– Какой-такой BRIN? Я не понимаю! – запаниковал Вася.
Макс улыбнулся:
– Спокойно, сейчас объясню. BRIN (Block Range Index) хранит не каждую запись отдельно, а диапазоны значений и физическое расположение на диске. Подходит идеально, если данные хранятся последовательно, а таблицы огромные. Как раз твой случай.
Макс быстро создал новый индекс:
Перезапустил запрос – результат был впечатляющим:
- Старый B-tree индекс: 60 Гб 🔴
- Новый BRIN индекс: 240 Мб 🟢
– Невероятно! – Вася не мог поверить глазам – А производительность сильно просядет?
– А ты проверь. – Улыбнулся Макс.
Вася сел за его компьютер, дрожащими руками удалил старый индекс, запустил пару основных отчетов и убедился, что производительность не пострадала.
Макс кивнул:
– Вот основные критерии для BRIN-индекса:
+ Данные в таблице должны поступать последовательно (даты, серийные номера),
+ Часто используется в отчётных и логирующих таблицах,
+ Не подходит для уникальных колонок
+ Немного хуже подходит для точечных выборок.
Воодушевлённый Вася убежал, а спустя час вернулся сияющий:
– Макс, я проверил остальные таблицы и нашёл ещё с десяток подходящих больших индексов! Удалось высвободить почти 20% места!
– Молодец! Кофе за твой счёт, – усмехнулся Макс, заглядывая в пустую кружку.
А вы используете BRIN-индексы в своей работе? Поделитесь своим опытом в комментариях.
#кейс #хранение #индексы
Только Макс набрал себе кофе и уселся в кресло, как в кабинет влетел Вася из сопровождения системы отчётности. Глаза у него были круглые от страха:
– Макс, беда! Сработал алерт – места на диске БД меньше 5%! Помоги! Что мне делать?
Макс спокойно отхлебнул кофе:
– Отправь заявку, чтобы добавили диски...
– Заявку уже отправил, но ты же знаешь наш банк – месяц будут добавлять! И ты знаешь моего начальника – он мне голову оторвёт.
Всё это Макс знал. Он подавил раздражение от того, что его снова отвлекают, и отхлебнул кофе:
– Ладно, давай посмотрим, от чего у вас там можно избавиться.
– Избавиться? Ты с ума сошел? Там нельзя ничего удалять.
Но Макс уже подключился к БД, запустил давно выверенный запрос и изучал список объектов БД отсортированный по размеру. Спустя минуту он нашел первого подозреваемого:
– Вася, твой B-tree индекс по дате транзакции весит уже 60 Гб. Предлагаю заменить его на BRIN.
– Какой-такой BRIN? Я не понимаю! – запаниковал Вася.
Макс улыбнулся:
– Спокойно, сейчас объясню. BRIN (Block Range Index) хранит не каждую запись отдельно, а диапазоны значений и физическое расположение на диске. Подходит идеально, если данные хранятся последовательно, а таблицы огромные. Как раз твой случай.
Макс быстро создал новый индекс:
CREATE INDEX idx_trxn_dt_brin ON transactions USING BRIN(created_at);
Перезапустил запрос – результат был впечатляющим:
- Старый B-tree индекс: 60 Гб 🔴
- Новый BRIN индекс: 240 Мб 🟢
– Невероятно! – Вася не мог поверить глазам – А производительность сильно просядет?
– А ты проверь. – Улыбнулся Макс.
Вася сел за его компьютер, дрожащими руками удалил старый индекс, запустил пару основных отчетов и убедился, что производительность не пострадала.
Макс кивнул:
– Вот основные критерии для BRIN-индекса:
+ Данные в таблице должны поступать последовательно (даты, серийные номера),
+ Часто используется в отчётных и логирующих таблицах,
+ Не подходит для уникальных колонок
+ Немного хуже подходит для точечных выборок.
Воодушевлённый Вася убежал, а спустя час вернулся сияющий:
– Макс, я проверил остальные таблицы и нашёл ещё с десяток подходящих больших индексов! Удалось высвободить почти 20% места!
– Молодец! Кофе за твой счёт, – усмехнулся Макс, заглядывая в пустую кружку.
А вы используете BRIN-индексы в своей работе? Поделитесь своим опытом в комментариях.
#кейс #хранение #индексы
👍12❤1
– Макс, мы тут совсем запутались, – вздохнул Ваня из соседнего отдела, аккуратно отодвигая от себя поднос с невкусным супом. – Наши джобы для рассылки подозрительных транзакций почему-то иногда пропускают часть результатов.
– Может ваши джобы подкуплены мошенниками? – ухмыльнулся Макс, пережёвывая салат с сомнительного вида курицей.
Ваня нахмурился:
– Ну серьёзно же. Каждые 10 минут запускаем выборку: выбираем всё, где время обнаружения (
Макс медленно кивнул:
– А вы считаете, что джобы у вас запускаются идеально ровно в каждые 10 минут?
– Ну думаю не совсем идеально...
– Вот именно, не совсем, – Макс чуть прищурился. – Поверь, ваш джоб далеко не единственный в базе, а планировщику выделяются ограниченные ресурсы. Он вполне может запуститься на несколько секунд позже. А в сбойных ситуациях, процесс вашего джоба вообще могут убить.
– Неужели все так плохо? у нас же база на очень мощном сервере.
– А вы верите, что базы и серверы живут в идеальном мире? Так вот, они живут в аду асинхронности, блокировок и подвисаний. Думаю, что ваша выборка теряет данные на границе интервала. - подытожил Макс.
– Что же делать? – Ваня выглядел обескураженно.
– Классика: заведи служебную таблицу, например,
– Так вы точно не потеряете транзакции из-за сбоев запуска, – заключил Макс.
– А зачем второй пункт? Почему мы делаем SELECT MAX до выборки?
– Потому что между стартом выборки (п.3) и апдейтом (п.4.) в таблицу могут попасть новые записи, которые в следующем запуске будут потеряны. А вы кажется не хотите пропускать записи? - улыбнулся Макс.
– Выглядит просто и надежно. И почему мы раньше до этого не додумались? – Ваня выглядел одновременно радостным и раздражённым.
Макс философски посмотрел на свою подозрительную курицу и суп Вани:
– По той же причине, почему я каждый раз надеюсь, что в столовой будет вкусный обед: излишний оптимизм часто побеждает здравый смысл.
#кейс
– Может ваши джобы подкуплены мошенниками? – ухмыльнулся Макс, пережёвывая салат с сомнительного вида курицей.
Ваня нахмурился:
– Ну серьёзно же. Каждые 10 минут запускаем выборку: выбираем всё, где время обнаружения (
founded) больше, чем sysdate - 10/24/60 и рассылаем на почту. Должно работать как часы, а иногда некоторые строки в выборку не попадают!Макс медленно кивнул:
– А вы считаете, что джобы у вас запускаются идеально ровно в каждые 10 минут?
– Ну думаю не совсем идеально...
– Вот именно, не совсем, – Макс чуть прищурился. – Поверь, ваш джоб далеко не единственный в базе, а планировщику выделяются ограниченные ресурсы. Он вполне может запуститься на несколько секунд позже. А в сбойных ситуациях, процесс вашего джоба вообще могут убить.
– Неужели все так плохо? у нас же база на очень мощном сервере.
– А вы верите, что базы и серверы живут в идеальном мире? Так вот, они живут в аду асинхронности, блокировок и подвисаний. Думаю, что ваша выборка теряет данные на границе интервала. - подытожил Макс.
– Что же делать? – Ваня выглядел обескураженно.
– Классика: заведи служебную таблицу, например,
job_last_run, и пиши туда последнее значение founded, которое было отправлено. Следующий поиск выполняй от последнего сохранённого значения:-- 1. читаем прошлое значение
SELECT last_founded FROM job_last_run WHERE job_name = 'fraud_monitor';
-- 2. заранее находим текущее максимальное
SELECT MAX(founded) INTO max_founded FROM fraud_alerts;
-- 3. делаем выборку
SELECT * FROM fraud_alerts WHERE founded > last_founded and founded <= max_founded;
-- 4. после рассылок обновляем крайнее значение
UPDATE job_last_run SET last_founded = max_founded WHERE job_name = 'fraud_monitor';
– Так вы точно не потеряете транзакции из-за сбоев запуска, – заключил Макс.
– А зачем второй пункт? Почему мы делаем SELECT MAX до выборки?
– Потому что между стартом выборки (п.3) и апдейтом (п.4.) в таблицу могут попасть новые записи, которые в следующем запуске будут потеряны. А вы кажется не хотите пропускать записи? - улыбнулся Макс.
– Выглядит просто и надежно. И почему мы раньше до этого не додумались? – Ваня выглядел одновременно радостным и раздражённым.
Макс философски посмотрел на свою подозрительную курицу и суп Вани:
– По той же причине, почему я каждый раз надеюсь, что в столовой будет вкусный обед: излишний оптимизм часто побеждает здравый смысл.
#кейс
👍9
🕵️♂️ Тайна пропавшей транзакции
Макс сидел у себя в кабинете и листал результаты аудита по таблице
— Макс, всё плохо. Твоё гениальное решение не помогло. У меня есть конкретные примеры: записи не попадают в рассылку, хотя точно должны!
Макс спокойно выслушал, промычал «разберёмся» и, не теряя времени, включил детальный аудит на таблицу. Его права позволяли делать больше, чем обычному сопровождающему. А Ване он поручил: «Найди свежий пример, как только появится — скидывай».
Через два часа пришло сообщение от Вани с новой «непойманной» транзакцией:
И вот сейчас Макс как раз смотрел аудит этой записи - на первый взгляд все было нормально. Макс добрался до временных меток и поднял бровь:
🟢 action_tstamp_tx (старт транзакции):
🟢 action_tstamp_stm (старт запроса):
🔴 action_tstamp_clk (завершение транзакции):
А вот и ключик нашелся
Он позвонил Ване.
— Вань, ты же уже знаешь, что время в БД — это не часы на вокзале. Когда в системе что-то всерьёз зависит от точного времени — обкладывайся логами и верь только им. И если видишь аномалию - докапывайся до причин.
— Но ведь founded = '14:59:45', а джоб запускался уже в 15:00, когда запись уже была найдена! — застонал Ваня. — Что может быть не так с этим временем?
Макс откинулся на спинку кресла:
— Вы когда вставляете записи в
— Ну... просто
— Вот тут и кроется причина.
— И что же теперь делать? Может сдвинуть время запуска механизмов поиска, чтобы они точно завершали работу до запуска джоба отправки результатов?
— Это не очень надежный вариант, так как со временем все снова разъедется. Дело в том, что sysdate внутри запроса insert-select для твоей задачи бесполезен. Он вызовется один раз в начале запроса — и всё.
Макс сделал глоток холодного кофе и продиктовал решение:
— Чтобы сократить переделки предлагаю такой вариант:
1. В insert-select пишешь
2. В конце функции делаешь:
— Тогда в
Ваня задумался:
— Просто. Даже слишком.
Макс усмехнулся и сказал:
— Иногда разбираться в проблеме — это как искать потерянный носок: если ты не можешь его найти после стирки, проверь - может он где-то в барабане застрял. Поэтому когда что-то в базе не складывается — не гадай по цифрам, посмотри, кто их туда принёс и когда.
Шпаргалка:
🟠 Oracle
SYSDATE → живое системное время сервера (точность: секунды, тип DATE)
SYSTIMESTAMP → живое время c микросекундной точностью + часовой пояс (TIMESTAMP WITH TIME ZONE)
CURRENT_TIMESTAMP → то же, но в часовой зоне текущего сеанса (TIMESTAMP WITH TIME ZONE)
LOCALTIMESTAMP → как CURRENT_TIMESTAMP, только без часового пояса (TIMESTAMP)
Каждое значение вычисляется один раз на SQL‑оператор; внутри длинной транзакции между операторами будет новое время.
🟣 PostgreSQL
now() / current_timestamp → alias transaction_timestamp(); фиксируется на старте транзакции
statement_timestamp() → время начала текущего SQL‑оператора
clock_timestamp() → «живое» системное время на момент вызова
#кейс #транзакции
Макс сидел у себя в кабинете и листал результаты аудита по таблице
fraud_alerts. С утра ему позвонил Ваня:— Макс, всё плохо. Твоё гениальное решение не помогло. У меня есть конкретные примеры: записи не попадают в рассылку, хотя точно должны!
Макс спокойно выслушал, промычал «разберёмся» и, не теряя времени, включил детальный аудит на таблицу. Его права позволяли делать больше, чем обычному сопровождающему. А Ване он поручил: «Найди свежий пример, как только появится — скидывай».
Через два часа пришло сообщение от Вани с новой «непойманной» транзакцией:
founded = '14:59:45', джоб запускался в 15:00:00, и *не должен был* пропустить запись. А он взял — и пропустил.
И вот сейчас Макс как раз смотрел аудит этой записи - на первый взгляд все было нормально. Макс добрался до временных меток и поднял бровь:
🟢 action_tstamp_tx (старт транзакции):
14:59:45🟢 action_tstamp_stm (старт запроса):
14:59:45🔴 action_tstamp_clk (завершение транзакции):
15:00:07А вот и ключик нашелся
Он позвонил Ване.
— Вань, ты же уже знаешь, что время в БД — это не часы на вокзале. Когда в системе что-то всерьёз зависит от точного времени — обкладывайся логами и верь только им. И если видишь аномалию - докапывайся до причин.
— Но ведь founded = '14:59:45', а джоб запускался уже в 15:00, когда запись уже была найдена! — застонал Ваня. — Что может быть не так с этим временем?
Макс откинулся на спинку кресла:
— Вы когда вставляете записи в
fraud_alerts, откуда берёте founded?— Ну... просто
sysdate в insert-select.— Вот тут и кроется причина.
sysdate в Oracle, вычисляется один раз на каждый SQL-оператор. У тебя insert-select работает примерно 20 секунд, и все строки получают одинаковый founded, равный времени запуска запроса. В твоем примере запрос завершил выполнение в 15:00:07, но у всех записей founded — это всё равно 14:59:45. Когда запустился твой джоб для рассылок в 15:00, эти записи в базу еще не попали, они попали в 15:00:07. При этом следующий джоб их тоже пропустит, так как будет считать уже отправленными ранее.— И что же теперь делать? Может сдвинуть время запуска механизмов поиска, чтобы они точно завершали работу до запуска джоба отправки результатов?
— Это не очень надежный вариант, так как со временем все снова разъедется. Дело в том, что sysdate внутри запроса insert-select для твоей задачи бесполезен. Он вызовется один раз в начале запроса — и всё.
Макс сделал глоток холодного кофе и продиктовал решение:
— Чтобы сократить переделки предлагаю такой вариант:
1. В insert-select пишешь
founded = NULL2. В конце функции делаешь:
UPDATE fraud_alerts SET founded = sysdate WHERE founded IS NULL;
COMMIT;
— Тогда в
founded у тебя будет *реальное* время завершения вставки. Ну и все записи попадут в рассылку, как положено.Ваня задумался:
— Просто. Даже слишком.
Макс усмехнулся и сказал:
— Иногда разбираться в проблеме — это как искать потерянный носок: если ты не можешь его найти после стирки, проверь - может он где-то в барабане застрял. Поэтому когда что-то в базе не складывается — не гадай по цифрам, посмотри, кто их туда принёс и когда.
Шпаргалка:
🟠 Oracle
SYSDATE → живое системное время сервера (точность: секунды, тип DATE)
SYSTIMESTAMP → живое время c микросекундной точностью + часовой пояс (TIMESTAMP WITH TIME ZONE)
CURRENT_TIMESTAMP → то же, но в часовой зоне текущего сеанса (TIMESTAMP WITH TIME ZONE)
LOCALTIMESTAMP → как CURRENT_TIMESTAMP, только без часового пояса (TIMESTAMP)
Каждое значение вычисляется один раз на SQL‑оператор; внутри длинной транзакции между операторами будет новое время.
🟣 PostgreSQL
now() / current_timestamp → alias transaction_timestamp(); фиксируется на старте транзакции
statement_timestamp() → время начала текущего SQL‑оператора
clock_timestamp() → «живое» системное время на момент вызова
#кейс #транзакции
👍5
📞 Звонок
Макс только собрался налить себе кофе, как раздался звонок.
— Это техподдержка? — раздалось в трубке. Голос был низкий и настойчивый, как будто звонил генерал.
— Нет, телефон поддержки 7777. — спокойно ответил Макс, прикрыв глаза. Прощай, кофе.
— Но мне сказали, что вы лучший специалист, поэтому я решил позвонить сразу вам. У меня тут проблема. Это срочно!
Лучший специалист. Конечно. А ещё экзорцист, шаман, гуру сетевых дУхов и психолог по совместительству. Макс вежливо уточнил:
— Вам нужно направить запрос через HelpDesk. Направьте заявку или позвоните 7777. Если заявка поступит в работу мне, я обязательно посмотрю.
— Да причем тут заявка? Я же уже звоню вам! — голос в трубке повысился на полтона. — Просто придите и сделайте! Я же говорю — это срочно!
Срочно? О да. Наверняка сервера горят, данные бегут в Китай, или хомячок застрял в сетевом кабеле, спасая мир от кибератаки. Макс подавил смешок.
— Вам нужно позвонить на четыре семерки. Понимаете, — максимально спокойно сказал он. — Если я сейчас начну решать вашу проблему без заявки, то другие пользователи, которые оформили свои запросы, останутся без поддержки. Это несправедливо.
— А мне плевать на других! — заорал абонент. — Вы лучший, значит должны немедленно мне помочь!
Вы лучший. Вы волшебник. Вы можете починить сервер взглядом. А если не можете — значит, вы не лучший. Макс закрыл глаза и глубоко вдохнул.
— Я всё понимаю, — сказал он ровным голосом. — Но у нас есть регламент, по которому мы работаем. Если я сейчас всё брошу и займусь вашей проблемой, это нарушит работу всей системы поддержки. Пожалуйста, оформите заявку. Обещаю — я посмотрю её в первую очередь, как только она попадёт ко мне.
— Так и знал! Бесполезная поддержка! — абонент бросил трубку с таким звуком, будто швырнул её в стену.
Макс положил трубку, сделал глубокий вдох и, наконец, налил себе кофе.
— Кто это был? — спросил коллега за соседним столом.
— Он не представился. — Макс сделал глоток и ухмыльнулся. — У него какая-то проблема. Но я не успел понять, то ли в ДНК, то ли в эволюции.
Макс только собрался налить себе кофе, как раздался звонок.
— Это техподдержка? — раздалось в трубке. Голос был низкий и настойчивый, как будто звонил генерал.
— Нет, телефон поддержки 7777. — спокойно ответил Макс, прикрыв глаза. Прощай, кофе.
— Но мне сказали, что вы лучший специалист, поэтому я решил позвонить сразу вам. У меня тут проблема. Это срочно!
Лучший специалист. Конечно. А ещё экзорцист, шаман, гуру сетевых дУхов и психолог по совместительству. Макс вежливо уточнил:
— Вам нужно направить запрос через HelpDesk. Направьте заявку или позвоните 7777. Если заявка поступит в работу мне, я обязательно посмотрю.
— Да причем тут заявка? Я же уже звоню вам! — голос в трубке повысился на полтона. — Просто придите и сделайте! Я же говорю — это срочно!
Срочно? О да. Наверняка сервера горят, данные бегут в Китай, или хомячок застрял в сетевом кабеле, спасая мир от кибератаки. Макс подавил смешок.
— Вам нужно позвонить на четыре семерки. Понимаете, — максимально спокойно сказал он. — Если я сейчас начну решать вашу проблему без заявки, то другие пользователи, которые оформили свои запросы, останутся без поддержки. Это несправедливо.
— А мне плевать на других! — заорал абонент. — Вы лучший, значит должны немедленно мне помочь!
Вы лучший. Вы волшебник. Вы можете починить сервер взглядом. А если не можете — значит, вы не лучший. Макс закрыл глаза и глубоко вдохнул.
— Я всё понимаю, — сказал он ровным голосом. — Но у нас есть регламент, по которому мы работаем. Если я сейчас всё брошу и займусь вашей проблемой, это нарушит работу всей системы поддержки. Пожалуйста, оформите заявку. Обещаю — я посмотрю её в первую очередь, как только она попадёт ко мне.
— Так и знал! Бесполезная поддержка! — абонент бросил трубку с таким звуком, будто швырнул её в стену.
Макс положил трубку, сделал глубокий вдох и, наконец, налил себе кофе.
— Кто это был? — спросил коллега за соседним столом.
— Он не представился. — Макс сделал глоток и ухмыльнулся. — У него какая-то проблема. Но я не успел понять, то ли в ДНК, то ли в эволюции.
😁9
🗑 Макс и стажёр с тяжёлой рукой
— Макс, помоги! — В офис ворвался Коля, новый стажёр. Лицо белое как стена.
— Что случилось? — Макс не отвёл глаз от монитора. Время не кофе пить, а тикеты разбирать.
— Я случайно удалил данные из
Макс медленно повернулся:
— Удалил? Как? Почему?
— Да я... Я подумал, они уже не нужны! — Коля чуть не плакал. — Выполнил
— Подумал? — Макс усмехнулся. — Ну да, это ведь всегда работает. Особенно в базах данных. Только вот для стажёров после "подумал" должно быть действие "спросил у старших", а потом уже sql-команда.
Макс сдержал дыхание и закрыл глаза. Сильно, конечно. Но не в первый раз. Он сделал глубокий вдох:
— Первое правило: прежде чем что-то удалять или редактировать, делай бэкап с помощью CTAS. У нас для бэкапов есть отдельная схема — Макс открыл новый файл скрипта и написал:
— Сначала всегда копируй данные. Удалить мы всегда успеем. А вот восстановить... — Макс бросил косой взгляд на Колю. — Ну, ты понял.
Коля кивнул, но выглядел по-прежнему растерянно.
— Второе правило. Смотри сюда, — Макс открыл схему таблицы
— Так данные остаются в таблице. В любой момент можно откатить изменения или восстановить запись. А ты что сделал?
— Удалил... навсегда, — пробормотал Коля, уставившись в пол.
— Часто вместо is_deleted используют обратную логику с is_active, которое для действующих записей устанавливают в true.
— Спасибо, я запомню.
— В данном случае не нужно было удалять эти данные даже с помощью soft-delete. Вся существующая логика уже учитывает статусы.
Макс сделал ещё один глубокий вдох, достал заготовку запроса для работы с аудитом данных, нашел все удаленные записи.
— Теперь можешь выдохнуть. Тебе повезло, что аудит был включён для этой таблицы.
Доработав запрос он вернул удаленные записи в основную таблицу.
— Ну, теперь ты понял, почему я не удаляю данные сразу? — Макс хлопнул его по плечу. — В жизни, как и в базе, важно не спешить избавляться от того, что кажется ненужным. Особенно если у тебя тяжёлая рука и быстрые пальцы.
#процессы
— Макс, помоги! — В офис ворвался Коля, новый стажёр. Лицо белое как стена.
— Что случилось? — Макс не отвёл глаз от монитора. Время не кофе пить, а тикеты разбирать.
— Я случайно удалил данные из
notification_queue. Коллеги сказали, что это важные данные! Они же исчезли навсегда!Макс медленно повернулся:
— Удалил? Как? Почему?
— Да я... Я подумал, они уже не нужны! — Коля чуть не плакал. — Выполнил
DELETE FROM notification_queue WHERE status = 'error'... и всё.— Подумал? — Макс усмехнулся. — Ну да, это ведь всегда работает. Особенно в базах данных. Только вот для стажёров после "подумал" должно быть действие "спросил у старших", а потом уже sql-команда.
Макс сдержал дыхание и закрыл глаза. Сильно, конечно. Но не в первый раз. Он сделал глубокий вдох:
— Первое правило: прежде чем что-то удалять или редактировать, делай бэкап с помощью CTAS. У нас для бэкапов есть отдельная схема — Макс открыл новый файл скрипта и написал:
CREATE TABLE support.notification_queue_backup AS SELECT * FROM notification_queue WHERE status = 'error';
— Сначала всегда копируй данные. Удалить мы всегда успеем. А вот восстановить... — Макс бросил косой взгляд на Колю. — Ну, ты понял.
Коля кивнул, но выглядел по-прежнему растерянно.
— Второе правило. Смотри сюда, — Макс открыл схему таблицы
notification_queue и указал на столбец is_deleted. — Видишь? У нас есть поле is_deleted. Это soft-delete. Вместо того, чтобы физически удалять данные, мы просто помечаем их как удалённые:UPDATE notification_queue SET is_deleted = true WHERE status = 'error';
— Так данные остаются в таблице. В любой момент можно откатить изменения или восстановить запись. А ты что сделал?
— Удалил... навсегда, — пробормотал Коля, уставившись в пол.
— Часто вместо is_deleted используют обратную логику с is_active, которое для действующих записей устанавливают в true.
— Спасибо, я запомню.
— В данном случае не нужно было удалять эти данные даже с помощью soft-delete. Вся существующая логика уже учитывает статусы.
Макс сделал ещё один глубокий вдох, достал заготовку запроса для работы с аудитом данных, нашел все удаленные записи.
— Теперь можешь выдохнуть. Тебе повезло, что аудит был включён для этой таблицы.
Доработав запрос он вернул удаленные записи в основную таблицу.
— Ну, теперь ты понял, почему я не удаляю данные сразу? — Макс хлопнул его по плечу. — В жизни, как и в базе, важно не спешить избавляться от того, что кажется ненужным. Особенно если у тебя тяжёлая рука и быстрые пальцы.
#процессы
👍10
🪦 Кладбище забытых индексов
— Ну что, Вася, три недели прошло. Диски вашей базе уже добавили? — Макс усмехнулся, откидываясь на спинку кресла.
Вася горько вздохнул:
— Ха-ха. Прикинь до какого маразма довели — наша заявка уже вторую неделю находится на согласовании у инфобеза. Видимо оценивают, как добавление дисков к базе отчётности привлечет внимание хакерских группировок.
Макс покачал головой:
— А свободное место на диске как было, так и осталось на грани?
— Да, и постоянно уменьшается, — Вася выдохнул. — Но я уже не удивляюсь. Помню как в прошлом году месяц пытался получить права на просмотр логов. До сих пор помню эти бессмысленные переписки и анкеты. А потом вдруг у какого-то начальника не сформировался отчет, подняли шум, и доступ предоставили за 15 минут.
Макс усмехнулся:
— А ты до сих пор это помнишь. Ну, тогда не удивляйся, что у твоей базы тоже столько мёртвых индексов. Она ведь тоже не забывает, что когда-то их создали.
— Мёртвых? — Вася нахмурился. — Что ты имеешь в виду?
— Когда я изучал, что у вас там занимает место, заметил, что у многих таблиц по 10 и больше индексов. Я понимаю, что отчётность, аналитика и всё такое, но это выглядит сильно избыточным. Напомни, у вас статистика автоматически собирается?
— Да, каждое последнее воскресенье месяца. Прошлый раз был три с половиной недели назад, — ответил Вася, почесав затылок.
— Отлично. Три недели — это уже достаточно. Смотри сюда. — Макс развернул ноутбук и открыл скрипт:
— Этот запрос покажет все индексы, которые не были использованы ни разу с момента последнего сброса статистики. Только не спеши их удалять.
— Это почему ещё? — Вася выглядел так, будто только что осознал, что у него в квартире поселился ненужный сосед.
— Ну всё же надо посмотреть на каждый индекс внимательно. Подготовить список для удаления и разослать коллегам. Чтобы не бегали потом с криками «Кто удалил? Прибью когда найду!»
Вася кивнул:
— Спасибо! Теперь у меня есть новая цель.
Они вместе задумчиво посмотрели на результаты запроса. Макс усмехнулся:
— Ну что, Вася, видишь, сколько мёртвого груза в базе накопилось?
— Угу, — кивнул Вася. — Кто бы мог подумать, что забытые и никому не нужные индексы занимают столько места.
Макс отставил кружку на стол и потянулся:
— Да, всё как в жизни. Мы с возрастом тоже так накапливаем. Вроде бы тащим с собой какие-то обиды, надежды, представления о том, как должно быть. Кажется, что это важно, но на деле — только место занимает.
Он постучал по экрану:
— Поэтому иногда полезно пересмотреть то, что давно кажется «устоявшимся». Избавиться от того, что на самом деле уже мешает. Базу — от ненужных индексов. А себя — от старых фантазий и обид.
#кейс #индексы #хранение
— Ну что, Вася, три недели прошло. Диски вашей базе уже добавили? — Макс усмехнулся, откидываясь на спинку кресла.
Вася горько вздохнул:
— Ха-ха. Прикинь до какого маразма довели — наша заявка уже вторую неделю находится на согласовании у инфобеза. Видимо оценивают, как добавление дисков к базе отчётности привлечет внимание хакерских группировок.
Макс покачал головой:
— А свободное место на диске как было, так и осталось на грани?
— Да, и постоянно уменьшается, — Вася выдохнул. — Но я уже не удивляюсь. Помню как в прошлом году месяц пытался получить права на просмотр логов. До сих пор помню эти бессмысленные переписки и анкеты. А потом вдруг у какого-то начальника не сформировался отчет, подняли шум, и доступ предоставили за 15 минут.
Макс усмехнулся:
— А ты до сих пор это помнишь. Ну, тогда не удивляйся, что у твоей базы тоже столько мёртвых индексов. Она ведь тоже не забывает, что когда-то их создали.
— Мёртвых? — Вася нахмурился. — Что ты имеешь в виду?
— Когда я изучал, что у вас там занимает место, заметил, что у многих таблиц по 10 и больше индексов. Я понимаю, что отчётность, аналитика и всё такое, но это выглядит сильно избыточным. Напомни, у вас статистика автоматически собирается?
— Да, каждое последнее воскресенье месяца. Прошлый раз был три с половиной недели назад, — ответил Вася, почесав затылок.
— Отлично. Три недели — это уже достаточно. Смотри сюда. — Макс развернул ноутбук и открыл скрипт:
SELECT s.schemaname, s.relname table_name, s.indexrelname index_name,
s.idx_scan AS index_scans, pg_size_pretty(pg_relation_size(s.indexrelid)) AS index_size,
pg_get_indexdef(s.indexrelid) AS index_definition, i.indisvalid, i.indisprimary
FROM pg_stat_user_indexes s
JOIN pg_index i ON i.indexrelid = s.indexrelid
WHERE s.idx_scan = 0
ORDER BY pg_relation_size(s.indexrelid) DESC;
— Этот запрос покажет все индексы, которые не были использованы ни разу с момента последнего сброса статистики. Только не спеши их удалять.
— Это почему ещё? — Вася выглядел так, будто только что осознал, что у него в квартире поселился ненужный сосед.
— Ну всё же надо посмотреть на каждый индекс внимательно. Подготовить список для удаления и разослать коллегам. Чтобы не бегали потом с криками «Кто удалил? Прибью когда найду!»
Вася кивнул:
— Спасибо! Теперь у меня есть новая цель.
Они вместе задумчиво посмотрели на результаты запроса. Макс усмехнулся:
— Ну что, Вася, видишь, сколько мёртвого груза в базе накопилось?
— Угу, — кивнул Вася. — Кто бы мог подумать, что забытые и никому не нужные индексы занимают столько места.
Макс отставил кружку на стол и потянулся:
— Да, всё как в жизни. Мы с возрастом тоже так накапливаем. Вроде бы тащим с собой какие-то обиды, надежды, представления о том, как должно быть. Кажется, что это важно, но на деле — только место занимает.
Он постучал по экрану:
— Поэтому иногда полезно пересмотреть то, что давно кажется «устоявшимся». Избавиться от того, что на самом деле уже мешает. Базу — от ненужных индексов. А себя — от старых фантазий и обид.
#кейс #индексы #хранение
👍10
⏳INSERT`ы тормозят
Макс только открыл мониторинг и сделал глоток кофе, как вдруг у него зазвонил телефон.
— Привет, это Сергей из поддержки BI. У нас, с базой что-то не так — инсёрты тормозят.
— Поподробнее. — Макс всё ещё не переключился с текущего графика в Zabbix.
— Очередь сообщений в шине накапливается, очень медленно идет вставка данных в базу.
— Минуту. — Макс подключился к нужной базе, заглянул в pg_stat_activity, в мониторинг шины.
— Диски, память, проц в порядке, это мы посмотрели. Блокировок тоже нет. — Между тем сообщил Сергей.
Макс открыл pg_stat_user_tables, отсортировал по n_dead_tup (количество "мертвых" строк) и с удивлением посмотрел на верхние записи с значениями в несколько десятков миллионов:
— Ого. А вот и причина.
— Что там? — Оживленно спросил Сергей.
— Да у вас там мильёны мертвых строк. Автовакуум вроде работает, но видимо не справляется. Погоди еще минуту.
Макс запустил вручную VACUUM (VERBOSE, ANALYZE). Ответ из базы был математически точен и бесполезен:
DETAIL: cannot remove dead tuples: 36M, oldest xmin: 1411878869
— Ну классика же. Кто-то открыл транзакцию и пошёл домой, — тихо сказал Макс, чувствуя, как кофе становится ещё горче.
Он вывел список активных транзакций:
Нашёл сразу несколько, висящих уже третий день. Причем все от одной службы.
— Почему-то разработчики любят обсуждать утечки памяти, но зато BEGIN без COMMIT их обычно не очень-то волнует! — В сердцах возмутился Макс. Он быстро подключился к нужному серверу и перезапустил нужную службу. Отметив, что как раз три дня назад она и была обновлена.
После этого повторный VACUUM отработал как по маслу за несколько секунд.
— Ты что-то сделал? — Радостно сказал Сергей. — У нас все полетело как на ракете.
Макс рассказал ему причины, допил остывший кофе и с философским спокойствием заметил:
—Так и живём: одни пишут BEGIN, другие ищут, кто забыл COMMIT, а база, как невестка у свекрови, — терпит, пока может. В общем передай разрабам, что транзакции — не кредиты, закрывать их надо вовремя. И настройте мониторинг, а то однажды база вам «Out of Memory» подкинет.|
#кейс #транзакции
Макс только открыл мониторинг и сделал глоток кофе, как вдруг у него зазвонил телефон.
— Привет, это Сергей из поддержки BI. У нас, с базой что-то не так — инсёрты тормозят.
— Поподробнее. — Макс всё ещё не переключился с текущего графика в Zabbix.
— Очередь сообщений в шине накапливается, очень медленно идет вставка данных в базу.
— Минуту. — Макс подключился к нужной базе, заглянул в pg_stat_activity, в мониторинг шины.
— Диски, память, проц в порядке, это мы посмотрели. Блокировок тоже нет. — Между тем сообщил Сергей.
Макс открыл pg_stat_user_tables, отсортировал по n_dead_tup (количество "мертвых" строк) и с удивлением посмотрел на верхние записи с значениями в несколько десятков миллионов:
— Ого. А вот и причина.
— Что там? — Оживленно спросил Сергей.
— Да у вас там мильёны мертвых строк. Автовакуум вроде работает, но видимо не справляется. Погоди еще минуту.
Макс запустил вручную VACUUM (VERBOSE, ANALYZE). Ответ из базы был математически точен и бесполезен:
DETAIL: cannot remove dead tuples: 36M, oldest xmin: 1411878869
— Ну классика же. Кто-то открыл транзакцию и пошёл домой, — тихо сказал Макс, чувствуя, как кофе становится ещё горче.
Он вывел список активных транзакций:
SELECT pid, now() - xact_start AS duration, state, usename, client_addr, application_name, query
FROM pg_stat_activity
WHERE xact_start IS NOT NULL AND state != 'idle'
ORDER BY duration DESC;
Нашёл сразу несколько, висящих уже третий день. Причем все от одной службы.
— Почему-то разработчики любят обсуждать утечки памяти, но зато BEGIN без COMMIT их обычно не очень-то волнует! — В сердцах возмутился Макс. Он быстро подключился к нужному серверу и перезапустил нужную службу. Отметив, что как раз три дня назад она и была обновлена.
После этого повторный VACUUM отработал как по маслу за несколько секунд.
— Ты что-то сделал? — Радостно сказал Сергей. — У нас все полетело как на ракете.
Макс рассказал ему причины, допил остывший кофе и с философским спокойствием заметил:
—Так и живём: одни пишут BEGIN, другие ищут, кто забыл COMMIT, а база, как невестка у свекрови, — терпит, пока может. В общем передай разрабам, что транзакции — не кредиты, закрывать их надо вовремя. И настройте мониторинг, а то однажды база вам «Out of Memory» подкинет.|
#кейс #транзакции
👍9❤1
📩 Обычный рабочий день, Макс допивал очередной стакан кофе.
Тут в общий чат поддержки прилетело сообщение от Арсена:
Спустя минуту в чат посыпались варианты:
Макс любил такие моменты, поэтому отправил в чат свой вариант:
🤔 А как бы вы ответили на такую заявку?
Тут в общий чат поддержки прилетело сообщение от Арсена:
— Зацените заявку. Всем отделом ломаем головы, как лучше ответить:
“Пожалуйста, улучшите память моей почты”
😂😂😂
Спустя минуту в чат посыпались варианты:
«К сожалению, искусственный интеллект вашего почтового ящика пока не прошел курсы мнемоники. Но вы можете помочь ему «разгрузить» воспоминания, удалив ненужные письма.»
«Память почты — как память человека: чем больше в ней мусора, тем медленнее она работает. Рекомендуем провести цифровую медитацию и отпустить старые письма.»
«Ваш почтовый ящик, вероятно, уже все помнит. Возможно, даже слишком хорошо. Если вы хотите, чтобы он сосредоточился на текущих задачах, а не на исторических исследованиях, вот как можно аккуратно подчистить его ‘воспоминания’.»
Макс любил такие моменты, поэтому отправил в чат свой вариант:
Диагноз подтверждён: ваш почтовый ящик страдает от тяжёлой формы PTSD (Post-Traumatic Spam Disorder).
Рекомендуемое лечение:
- Экстренная детоксикация (удалить всё, где тема начинается с «FW: FW:»)
- Курс цифрового минимализма (оставить только жизненно важные письма)
- Профилактика рецидивов (настроить автоудаление старых писем)
Прилагаем инструкцию по терапии. Прогноз благоприятный, если начать сегодня (пока почтовый ящик не потребовал отпуск за свой счёт)
🤔 А как бы вы ответили на такую заявку?
😁9
🧹 Загадка пустых байтов
Вася ворвался в кабинет Макса с горящими глазами и ноутбуком под мышкой.
— Макс, ты не поверишь! Я нашёл ещё один способ освободить место!
Макс медленно отложил кружку, оценивая Васину одержимость.
— Ну-ка, показывай, что ты там на этот раз нашел.
— Смотри, — Вася развернул ноутбук и ткнул пальцем в экран. — В нашу базу для отчетности сливаются таблицы, которые содержат всякие файлы — PDF-ки, Excel и всякие сканы. Но для формирования отчётов они не нужны! Я уже всем всё написал и согласовал их удаление. Даже занулил уже эти поля через UPDATE SET NULL, но…
— Но размер таблиц не уменьшился? — Макс ухмыльнулся.
— Да! Как так? Я же по сути удалил данные!
Макс развернул к себе экран и быстро набрал запрос:
— Вот смотри, — он указал на цифры. — Таблица весит 2 Гб, индексы — 0,5 Гб, а реально места на диске занимает 1300 Гб! Ты занулил bytea-поля, но Postgres не освобождает это место просто так.
— То есть я зря старался? — Вася поник.
— Не совсем. Просто UPDATE, как и DELETE, не уменьшает физический размер таблицы. Postgres помечает старые данные как "мертвые", но они остаются на диске.
— И что делать?
Макс усмехнулся:
— Есть три пути:
✅ VACUUM FULL – перезапишет таблицу, освободив место, но заблокирует её на время.
✅ pg_repack – делает то же самое, но без блокировок (но должно быть установлено расширение).
✅ Создать новую таблицу и перелить данные – долго, но безопасно.
— А если я просто сделаю VACUUM без FULL? У нас же работает автовакуум.
— Автовакуум освобождает место для новых записей, но не вернёт его системе.
Вася задумался:
— Ладно, VACUUM FULL в рабочее время делать рискованно, pg_repack у нас не установлен, перезагрузку базы согласовывать хлопотно… А если я скопирую нужные данные в новую таблицу, а старую дропну?
— Рабочий вариант, — кивнул Макс. — Только не забудь проверить, что никто не ссылается на эти данные через FK или в коде. Также вполне рабочий вариант — запустить VACUUM FULL через планировщик в нерабочее время, но надо учесть все ваши сервисные процессы ночью.
— Понял! — Вася уже лихорадочно строчил запросы.
— Кстати, — Макс поднял бровь, — а ты уверен, что эти файлы в основной системе вообще должны храниться в БД? Может, их лучше в объектное хранилище вынести?
— О! — Вася замер с открытым ртом. — Ты гений!
— Ну, это очевидно, — Макс потягивал кофе. — Но если ты сейчас побежишь это внедрять, то твой начальник сначала убьёт тебя, а потом себя.
— …Потому что это потребует изменений в 100500 сервисах и полгода согласований?
— Бинго.
Вася вздохнул:
— Ладно, начну с VACUUM FULL. Пару терабайт точно выиграю.
Макс одобрительно кивнул:
— Главное — не увлекайся. А то скоро начнёшь сжимать VARCHAR до CHAR и партицировать системные таблицы.
— Эээ… а это плохо?
— Это путь в ад, Вася. В ад.
#кейс #хранение
Вася ворвался в кабинет Макса с горящими глазами и ноутбуком под мышкой.
— Макс, ты не поверишь! Я нашёл ещё один способ освободить место!
Макс медленно отложил кружку, оценивая Васину одержимость.
— Ну-ка, показывай, что ты там на этот раз нашел.
— Смотри, — Вася развернул ноутбук и ткнул пальцем в экран. — В нашу базу для отчетности сливаются таблицы, которые содержат всякие файлы — PDF-ки, Excel и всякие сканы. Но для формирования отчётов они не нужны! Я уже всем всё написал и согласовал их удаление. Даже занулил уже эти поля через UPDATE SET NULL, но…
— Но размер таблиц не уменьшился? — Макс ухмыльнулся.
— Да! Как так? Я же по сути удалил данные!
Макс развернул к себе экран и быстро набрал запрос:
SELECT pg_size_pretty(pg_relation_size('avent.enclosures')) AS table_size,
pg_size_pretty(pg_indexes_size('avent.enclosures')) AS indexes_size,
pg_size_pretty(pg_total_relation_size('avent.enclosures')) AS total_size;— Вот смотри, — он указал на цифры. — Таблица весит 2 Гб, индексы — 0,5 Гб, а реально места на диске занимает 1300 Гб! Ты занулил bytea-поля, но Postgres не освобождает это место просто так.
— То есть я зря старался? — Вася поник.
— Не совсем. Просто UPDATE, как и DELETE, не уменьшает физический размер таблицы. Postgres помечает старые данные как "мертвые", но они остаются на диске.
— И что делать?
Макс усмехнулся:
— Есть три пути:
✅ VACUUM FULL – перезапишет таблицу, освободив место, но заблокирует её на время.
✅ pg_repack – делает то же самое, но без блокировок (но должно быть установлено расширение).
✅ Создать новую таблицу и перелить данные – долго, но безопасно.
— А если я просто сделаю VACUUM без FULL? У нас же работает автовакуум.
— Автовакуум освобождает место для новых записей, но не вернёт его системе.
Вася задумался:
— Ладно, VACUUM FULL в рабочее время делать рискованно, pg_repack у нас не установлен, перезагрузку базы согласовывать хлопотно… А если я скопирую нужные данные в новую таблицу, а старую дропну?
— Рабочий вариант, — кивнул Макс. — Только не забудь проверить, что никто не ссылается на эти данные через FK или в коде. Также вполне рабочий вариант — запустить VACUUM FULL через планировщик в нерабочее время, но надо учесть все ваши сервисные процессы ночью.
— Понял! — Вася уже лихорадочно строчил запросы.
— Кстати, — Макс поднял бровь, — а ты уверен, что эти файлы в основной системе вообще должны храниться в БД? Может, их лучше в объектное хранилище вынести?
— О! — Вася замер с открытым ртом. — Ты гений!
— Ну, это очевидно, — Макс потягивал кофе. — Но если ты сейчас побежишь это внедрять, то твой начальник сначала убьёт тебя, а потом себя.
— …Потому что это потребует изменений в 100500 сервисах и полгода согласований?
— Бинго.
Вася вздохнул:
— Ладно, начну с VACUUM FULL. Пару терабайт точно выиграю.
Макс одобрительно кивнул:
— Главное — не увлекайся. А то скоро начнёшь сжимать VARCHAR до CHAR и партицировать системные таблицы.
— Эээ… а это плохо?
— Это путь в ад, Вася. В ад.
#кейс #хранение
😁9🔥4
