🗄 Почему твой SQL-индекс молчит
Знакомая история: повесил индекс, а запрос как тормозил, так и тормозит.
Индекс вроде есть, но планировщик смотрит на него и проходит мимо.
Собрал самые частые причины, из-за которых так происходит.
1. Обернул колонку в функцию 🔧 Вот это ломает индекс чаще всего:
Как только колонка попадает внутрь функции, индекс по ней уже не применить — и привет, seq scan по всей таблице. Лечится диапазоном:
Ну или заводишь функциональный индекс, если без функции совсем никак.
2. LIKE, который начинается с
B-tree умеет искать по началу строки, а не по середине. Если тебе реально нужен поиск по куску внутри — это уже полнотекстовый индекс или триграммы (в постгресе pg_trgm).
3. Порядок колонок в составном индексе 📚 Индекс
4. Типы не совпали 🎭 Классика, на которой все хоть раз спотыкались:
База молча приведёт типы сама, а заодно тихо выкинет индекс.
Так что следи, чтобы тип значения совпадал с колонкой.
5. Индекс на колонке, где всего два значения 🎲
Если под условие подходит половина таблицы, планировщику дешевле прочитать её целиком, чем скакать туда-сюда по индексу.
И он тут прав.
Индексы хороши там, где значение отсекает много строк, а не половину.
6.
Красота. Но стоит написать
В общем, индекс — это не «создал и забыл».
Прежде чем гадать на кофейной гуще, открой
Он-то не соврёт.
А у вас какой случай в духе «индекс есть, но его как бы нет» бесил сильнее всего? 🤔
#sql #database #postgres #backend #performance #dev
Знакомая история: повесил индекс, а запрос как тормозил, так и тормозит.
Индекс вроде есть, но планировщик смотрит на него и проходит мимо.
Собрал самые частые причины, из-за которых так происходит.
1. Обернул колонку в функцию 🔧 Вот это ломает индекс чаще всего:
WHERE YEAR(created_at) = 2024
Как только колонка попадает внутрь функции, индекс по ней уже не применить — и привет, seq scan по всей таблице. Лечится диапазоном:
WHERE created_at >= '2024-01-01'
AND created_at < '2025-01-01'
Ну или заводишь функциональный индекс, если без функции совсем никак.
2. LIKE, который начинается с
% 🔍 Тут всё просто:WHERE name LIKE '%anton' -- бесполезно
WHERE name LIKE 'anton%' -- работает
B-tree умеет искать по началу строки, а не по середине. Если тебе реально нужен поиск по куску внутри — это уже полнотекстовый индекс или триграммы (в постгресе pg_trgm).
3. Порядок колонок в составном индексе 📚 Индекс
(a, b) устроен как телефонная книга: сначала сортировка по a, и только внутри неё по b. Поэтому запрос, где есть только b, его не подхватит:INDEX (user_id, created_at)
WHERE user_id = 5 -- норм
WHERE user_id = 5 AND created_at… -- норм
WHERE created_at > … -- мимо, левого префикса нет
4. Типы не совпали 🎭 Классика, на которой все хоть раз спотыкались:
-- phone у нас VARCHAR
WHERE phone = 89991234567 -- число против строки, индекс мимо
WHERE phone = '89991234567' -- порядок
База молча приведёт типы сама, а заодно тихо выкинет индекс.
Так что следи, чтобы тип значения совпадал с колонкой.
5. Индекс на колонке, где всего два значения 🎲
is_active, gender и прочие флаги индексировать почти бессмысленно. Если под условие подходит половина таблицы, планировщику дешевле прочитать её целиком, чем скакать туда-сюда по индексу.
И он тут прав.
Индексы хороши там, где значение отсекает много строк, а не половину.
6.
SELECT * мешает covering index 📦 Иногда индекс уже содержит все поля, которые тебе нужны, и база может отдать ответ прямо из него, не заглядывая в таблицу. Красота. Но стоит написать
SELECT * — и она вынуждена лезть в таблицу за каждой строкой ради остальных колонок. Бери только то, что реально используешь.В общем, индекс — это не «создал и забыл».
Прежде чем гадать на кофейной гуще, открой
EXPLAIN ANALYZE и посмотри, что там планировщик на самом деле делает. Он-то не соврёт.
А у вас какой случай в духе «индекс есть, но его как бы нет» бесил сильнее всего? 🤔
#sql #database #postgres #backend #performance #dev
⚡️ Бэкенд тормозит? Скорее всего это N+1
Когда какой-нибудь эндпоинт вдруг начинает отвечать по полсекунды, все первым делом лезут оптимизировать код: асинхронщина, воркеры, микрооптимизации.
А причина обычно куда скучнее — ты просто дёргаешь базу сотню раз там, где хватило бы одного запроса.
🐘 Как выглядит N+1 Берём список из 50 заказов и для каждого лезем за пользователем:
Один запрос на список и ещё по одному на каждую строку.
Отсюда и название — N+1.
На локалке с десятком записей ты это даже не заметишь.
А на проде, где строк тысячи и база стоит на другом сервере, каждый такой поход — это отдельный сетевой запрос туда-обратно.
Вот они и набегают в те самые полсекунды.
🔧 Как чинить Идея одна: вытащить всех пользователей сразу, одним-двумя запросами, а не по штучке в цикле.
В разных ORM это включается по-разному:
Под капотом это либо JOIN, либо второй запрос вида
Было 51 обращение — стало два.
На реальных данных разница огромная.
🔍 Как заметить Беда в том, что глазами N+1 не видно — код выглядит чистым.
Поэтому: — смотри SQL-лог в дев-режиме: если на один запрос к API летит пачка одинаковых SELECT-ов — вот он, красавец; — поставь профайлер (django-silk, bullet, rack-mini-profiler) — они прямо тычут носом; — или тупо считай запросы на эндпоинт.
Больше десятка на простую страницу — уже звоночек.
💾 И про кэш, пока не разогнались
Как только заходит разговор про скорость, все сразу хотят прикрутить Redis.
Но кэш — это не «сделать быстро», это «отложить проблему и получить новую»: теперь надо думать, когда его чистить.
Прежде чем кэшировать, честно ответь себе: — эти данные вообще часто читают и редко меняют?
если нет — кэш ни к чему; — что будет, когда они устареют и я отдам юзеру старьё? — как я вообще пойму, что пора обновлять кэш?
Часто выходит, что убрать N+1 и повесить нормальный индекс дают те же 200 мс выигрыша — только без лишнего слоя, который потом будет отдавать неактуальные данные и портить тебе вечера.
Правило простое: сначала померь и убери явную дичь в запросах, и только потом тащи кэш.
Наоборот — почти всегда дорога к боли.
#backend #performance #database #orm #optimization #dev
Когда какой-нибудь эндпоинт вдруг начинает отвечать по полсекунды, все первым делом лезут оптимизировать код: асинхронщина, воркеры, микрооптимизации.
А причина обычно куда скучнее — ты просто дёргаешь базу сотню раз там, где хватило бы одного запроса.
🐘 Как выглядит N+1 Берём список из 50 заказов и для каждого лезем за пользователем:
orders = Order.all # 1 запрос — забрали заказы
for o in orders:
print(o.user.name) # +50 запросов, по одному на заказ
Один запрос на список и ещё по одному на каждую строку.
Отсюда и название — N+1.
На локалке с десятком записей ты это даже не заметишь.
А на проде, где строк тысячи и база стоит на другом сервере, каждый такой поход — это отдельный сетевой запрос туда-обратно.
Вот они и набегают в те самые полсекунды.
🔧 Как чинить Идея одна: вытащить всех пользователей сразу, одним-двумя запросами, а не по штучке в цикле.
В разных ORM это включается по-разному:
# Django
Order.objects.select_related("user")
# SQLAlchemy
session.query(Order).options(joinedload(Order.user))
# Rails
Order.includes(:user)
Под капотом это либо JOIN, либо второй запрос вида
WHERE user_id IN (...). Было 51 обращение — стало два.
На реальных данных разница огромная.
🔍 Как заметить Беда в том, что глазами N+1 не видно — код выглядит чистым.
Поэтому: — смотри SQL-лог в дев-режиме: если на один запрос к API летит пачка одинаковых SELECT-ов — вот он, красавец; — поставь профайлер (django-silk, bullet, rack-mini-profiler) — они прямо тычут носом; — или тупо считай запросы на эндпоинт.
Больше десятка на простую страницу — уже звоночек.
💾 И про кэш, пока не разогнались
Как только заходит разговор про скорость, все сразу хотят прикрутить Redis.
Но кэш — это не «сделать быстро», это «отложить проблему и получить новую»: теперь надо думать, когда его чистить.
Прежде чем кэшировать, честно ответь себе: — эти данные вообще часто читают и редко меняют?
если нет — кэш ни к чему; — что будет, когда они устареют и я отдам юзеру старьё? — как я вообще пойму, что пора обновлять кэш?
Часто выходит, что убрать N+1 и повесить нормальный индекс дают те же 200 мс выигрыша — только без лишнего слоя, который потом будет отдавать неактуальные данные и портить тебе вечера.
Правило простое: сначала померь и убери явную дичь в запросах, и только потом тащи кэш.
Наоборот — почти всегда дорога к боли.
#backend #performance #database #orm #optimization #dev
🧙 Git-команды, которыми мало кто пользуется (а зря)
add-commit-push знают все. reflog и stash — почти все. А вот это — уже реже, хотя именно оно превращает Git из «системы контроля версий» в нормальный рабочий инструмент.
Погнали по тем командам, до которых обычно доходишь годам к трём коммерческого стажа.
1. git worktree — несколько веток одновременно, без stash и клонов 🌲 Классика боли: пилишь фичу, прилетает срочный фикс.
Обычно ты либо стэшишь недоделку, либо клонируешь репо второй раз.
Ни то, ни другое не нужно:
Это создаёт ВТОРУЮ рабочую папку рядом, на отдельной ветке, с тем же общим репозиторием.
Чинишь хотфикс в
Никакого переключения контекста. Закончил —
2. git bisect run — ищет баг сам, пока ты пьёшь кофе 🤖 Про bisect многие слышали, но вручную помечать good/bad на двадцати коммитах — тоска. Отдай это скрипту:
Git сам прогонит бинарный поиск: на каждом коммите запускает
Работает с любым тестом, хоть с одной строкой на
3. git log -S — когда именно появилась (или исчезла) эта строка 🕵️ Ситуация: в коде есть странный костыль или, наоборот, из кода пропала нужная строчка, и никто не помнит когда. Pickaxe:
Он находит не где строка есть, а коммиты, где её количество поменялось — то есть где её добавили или удалили. Мгновенно приводит к автору и контексту.
4. git rerere — разрешаешь один и тот же конфликт один раз ♻️ Долгий ребейз или регулярный мёрж, где постоянно всплывает один и тот же конфликт в одном месте?
Включи один раз:
Теперь Git запоминает, как ты разрулил конфликт, и в следующий раз применяет то же решение автоматически.
При длинных ребейзах экономит кучу однотипной ручной работы.
5. git commit --fixup + autosquash — чистая история без мучений ✨ Ревьюер попросил поправить коммит из середины ветки.
Обычно это интерактивный ребейз и ручное перетаскивание. Проще:
Первая команда делает коммит-заплатку, привязанную к нужному.
Вторая — автоматически расставляет их по местам и схлопывает.
Ты просто сохраняешь файл.
История выглядит так, будто ты с первого раза всё сделал правильно.
6. git log -L — история одной функции, а не всего файла 📜 Когда надо понять, как эволюционировал конкретный кусок кода:
Git покажет только те коммиты и диффы, что трогали именно эту функцию.
Никакого продирания через историю файла на 2000 строк.
Суть простая: Git умеет сильно больше, чем «сохранить и откатить».
Половина рутины, которую мы делаем руками, у него уже автоматизирована — просто про эти команды редко рассказывают.
Что из этого уже в вашем арсенале? И чем сами пользуетесь, чего тут нет? 🤔
#git #dev #backend #tools #programming #devops
add-commit-push знают все. reflog и stash — почти все. А вот это — уже реже, хотя именно оно превращает Git из «системы контроля версий» в нормальный рабочий инструмент.
Погнали по тем командам, до которых обычно доходишь годам к трём коммерческого стажа.
1. git worktree — несколько веток одновременно, без stash и клонов 🌲 Классика боли: пилишь фичу, прилетает срочный фикс.
Обычно ты либо стэшишь недоделку, либо клонируешь репо второй раз.
Ни то, ни другое не нужно:
git worktree add ../hotfix -b hotfix main
Это создаёт ВТОРУЮ рабочую папку рядом, на отдельной ветке, с тем же общим репозиторием.
Чинишь хотфикс в
../hotfix, а твоя недоделанная фича лежит нетронутой в основной папке. Никакого переключения контекста. Закончил —
git worktree remove.2. git bisect run — ищет баг сам, пока ты пьёшь кофе 🤖 Про bisect многие слышали, но вручную помечать good/bad на двадцати коммитах — тоска. Отдай это скрипту:
git bisect start HEAD <старый_рабочий_хэш>
git bisect run ./test.sh
Git сам прогонит бинарный поиск: на каждом коммите запускает
test.sh, exit 0 — коммит хороший, не 0 — плохой. Через минуту он выдаёт точный коммит, который всё сломал. Работает с любым тестом, хоть с одной строкой на
curl.3. git log -S — когда именно появилась (или исчезла) эта строка 🕵️ Ситуация: в коде есть странный костыль или, наоборот, из кода пропала нужная строчка, и никто не помнит когда. Pickaxe:
git log -S "old_feature_flag" --oneline
Он находит не где строка есть, а коммиты, где её количество поменялось — то есть где её добавили или удалили. Мгновенно приводит к автору и контексту.
-G — то же самое, но по регулярке.4. git rerere — разрешаешь один и тот же конфликт один раз ♻️ Долгий ребейз или регулярный мёрж, где постоянно всплывает один и тот же конфликт в одном месте?
Включи один раз:
git config --global rerere.enabled true
Теперь Git запоминает, как ты разрулил конфликт, и в следующий раз применяет то же решение автоматически.
При длинных ребейзах экономит кучу однотипной ручной работы.
5. git commit --fixup + autosquash — чистая история без мучений ✨ Ревьюер попросил поправить коммит из середины ветки.
Обычно это интерактивный ребейз и ручное перетаскивание. Проще:
git commit --fixup=<хэш_нужного_коммита>
git rebase -i --autosquash <база>
Первая команда делает коммит-заплатку, привязанную к нужному.
Вторая — автоматически расставляет их по местам и схлопывает.
Ты просто сохраняешь файл.
История выглядит так, будто ты с первого раза всё сделал правильно.
6. git log -L — история одной функции, а не всего файла 📜 Когда надо понять, как эволюционировал конкретный кусок кода:
git log -L :имя_функции:file.py
Git покажет только те коммиты и диффы, что трогали именно эту функцию.
Никакого продирания через историю файла на 2000 строк.
Суть простая: Git умеет сильно больше, чем «сохранить и откатить».
Половина рутины, которую мы делаем руками, у него уже автоматизирована — просто про эти команды редко рассказывают.
Что из этого уже в вашем арсенале? И чем сами пользуетесь, чего тут нет? 🤔
#git #dev #backend #tools #programming #devops
👍2🔥1
🌐 Кэш, про который все забывают
Все гоняются за скоростью: CDN, Redis, сжимаем картинки.
А самый простой кэш уже встроен в каждый браузер — это HTTP-кэширование.
Только большинство лепит заголовки наугад, а потом ловит классику: «я выкатил фикс, а у юзеров всё ещё старая версия».
🔀 Есть два разных режима, и их вечно путают
1. «Вообще не спрашивай сервер».
Браузер берёт ответ из своей памяти и всё, никакого запроса по сети.
Самый быстрый вариант — данные появляются мгновенно.
2. «Спроси, но по-дешёвому».
Браузер стучится на сервер, но качает тело ответа только если оно реально поменялось.
Запрос лёгкий, мегабайты по сети не летят.
Нормальный кэш — это когда ты грамотно миксуешь эти два режима.
⏳ Cache-Control — сколько времени можно не дёргать сервер
Главный заголовок:
Переводится как «час эти данные считаются свежими, не трогай сервер».
Весь час браузер отдаёт ответ из памяти.
Ещё пара слов, которые важно не перепутать: —
Типичный прострел себе в ногу: влепить большой
Потом выкатываешь обновление, а у людей висит старая страница ещё час, и ты ничего не сделаешь.
🏷 ETag — «слушай, а оно вообще менялось?» Вот тут и живёт дешёвая проверка.
Сервер вешает на ответ что-то вроде отпечатка:
Браузер его запоминает и в следующий раз спрашивает: «у меня вот такой отпечаток, всё ещё актуально?».
Если на сервере тот же — он отвечает
Браузер берёт данные из кэша, а по сети улетел только копеечный запрос вместо перекачки всего файла.
Красота.
🧩 Как это делают на нормальных проектах Весь фокус — разделить файлы на два типа.
Статика с хэшем в имени (
Год жизни,
Поменял код — поменялся хэш в названии, а значит это уже новый файл с новым адресом.
Старый кэш сам отваливается за ненадобностью.
HTML — почти не кэшируем, но проверяем:
И вот тут главный подвох:
Это «кэшируй, но каждый раз перепроверяй через ETag».
То есть HTML всегда свежий, а тяжёлые скрипты и картинки грузятся из кэша по вечному адресу.
Быстро и без залипшей старой версии.
Короче:
Прежде чем прикручивать очередной Redis, глянь, что уже отдают твои заголовки — там частенько бесплатно лежит половина скорости.
А вы заголовки кэша руками крутите или как фреймворк из коробки поставил, так и живёте? 🤔
#web #http #performance #backend #frontend #dev
Все гоняются за скоростью: CDN, Redis, сжимаем картинки.
А самый простой кэш уже встроен в каждый браузер — это HTTP-кэширование.
Только большинство лепит заголовки наугад, а потом ловит классику: «я выкатил фикс, а у юзеров всё ещё старая версия».
🔀 Есть два разных режима, и их вечно путают
1. «Вообще не спрашивай сервер».
Браузер берёт ответ из своей памяти и всё, никакого запроса по сети.
Самый быстрый вариант — данные появляются мгновенно.
2. «Спроси, но по-дешёвому».
Браузер стучится на сервер, но качает тело ответа только если оно реально поменялось.
Запрос лёгкий, мегабайты по сети не летят.
Нормальный кэш — это когда ты грамотно миксуешь эти два режима.
⏳ Cache-Control — сколько времени можно не дёргать сервер
Главный заголовок:
Cache-Control: max-age=3600
Переводится как «час эти данные считаются свежими, не трогай сервер».
Весь час браузер отдаёт ответ из памяти.
Ещё пара слов, которые важно не перепутать: —
public — можно кэшировать и по пути, на CDN и прокси; — private — только в браузере юзера, для персональных данных; — no-store — не кэшировать нигде и никогда (пароли, оплата).Типичный прострел себе в ногу: влепить большой
max-age на HTML. Потом выкатываешь обновление, а у людей висит старая страница ещё час, и ты ничего не сделаешь.
🏷 ETag — «слушай, а оно вообще менялось?» Вот тут и живёт дешёвая проверка.
Сервер вешает на ответ что-то вроде отпечатка:
ETag: "v23-abc123"
Браузер его запоминает и в следующий раз спрашивает: «у меня вот такой отпечаток, всё ещё актуально?».
Если на сервере тот же — он отвечает
304 Not Modified с пустым телом. Браузер берёт данные из кэша, а по сети улетел только копеечный запрос вместо перекачки всего файла.
Красота.
🧩 Как это делают на нормальных проектах Весь фокус — разделить файлы на два типа.
Статика с хэшем в имени (
app.a1b2c3.js) — кэшируем жёстко и надолго:Cache-Control: public, max-age=31536000, immutable
Год жизни,
immutable — «даже не перепроверяй». Поменял код — поменялся хэш в названии, а значит это уже новый файл с новым адресом.
Старый кэш сам отваливается за ненадобностью.
HTML — почти не кэшируем, но проверяем:
Cache-Control: no-cache
И вот тут главный подвох:
no-cache — это НЕ «не кэшировать». Это «кэшируй, но каждый раз перепроверяй через ETag».
То есть HTML всегда свежий, а тяжёлые скрипты и картинки грузятся из кэша по вечному адресу.
Быстро и без залипшей старой версии.
Короче:
no-cache и no-store — это вообще про разное, и половина болей с кэшем именно из-за того, что их считают одним и тем же. Прежде чем прикручивать очередной Redis, глянь, что уже отдают твои заголовки — там частенько бесплатно лежит половина скорости.
А вы заголовки кэша руками крутите или как фреймворк из коробки поставил, так и живёте? 🤔
#web #http #performance #backend #frontend #dev
✍1🔥1
Канал растёт, а я до сих пор толком не знаю, кто по ту сторону экрана
Давайте это исправим — заодно решим, про что писать больше
Расскажите, чем вы занимаетесь?
Давайте это исправим — заодно решим, про что писать больше
Расскажите, чем вы занимаетесь?
Anonymous Poll
25%
Backend разработчик
25%
Frontend разработчик
17%
Fullstack
17%
Mobile
25%
DevOps / SRE / инфра
0%
Data / ML
0%
QA
33%
РП / ПМ
17%
Не из IT / Стундент
И давайте сразу еще один:
Про что писать больше?
Про что писать больше?
Anonymous Poll
44%
Бэкенд и базы данных
22%
Фронтенд и браузер
33%
Оптимизация и производительность
56%
AI в разработке
56%
Архитектура и проектирование
22%
DevOps и инфраструктура
44%
Карьера и софт-скиллы
22%
Git и инструменты
22%
Разборы статей и книг
11%
Кодовые гочи в формате «было / стало»
🧱 Массив там, где нужен был не массив
Почти всё мы решаем массивом и словарём — и обычно норм
Но есть места, где привычная структура на ровном месте начинает тормозить, и на проде это больно
Причина почти всегда одна — взяли не ту структуру под задачу
Собрал частые проблемы и их решения
1. Проверка «есть ли такой элемент» в списке 🐌 Классика:
Каждая такая проверка перебирает весь список от начала до конца, пока не найдёт
Один раз — ерунда
А если это внутри цикла по другому списку — получаешь перебор в переборе, и на десятках тысяч элементов всё встаёт
Причём в дебаге ты этого не увидишь
Если тебе нужно только «есть или нет» — бери множество (set):
Set устроен так, что находит элемент сразу, не перебирая
Заплатил один раз за построение множества — дальше все проверки почти ничего не стоят
2. Удаление из начала списка ✂️ Тоже коварное:
Удалил первый элемент — и все остальные физически сьехало на одну позицию
На маленьком списке незаметно, а на большом каждый такой pop проходит по всему массиву
Есть deque — очередь, из которой можно быстро брать и с начала, и с конца:
Внутри это не сплошной кусок памяти, а связанные блоки, поэтому выдернуть элемент с любого конца — мгновенно
3. Считаем, сколько раз что встретилось 🔢
Обычно это пишут руками:
Знакомо?
А между тем для этого есть готовый инструмент:
Одна строчка вместо цикла с проверками — и сразу без багов. Плюс
4. Нужно вытащить десять самых больших значений из миллиона 🗑
Ты отсортировал целый массив, чтобы взять с хвоста десять штук — остальные 999 990 отсортировал впустую
Для этого есть куча (heap) — она держит в уме только нужные элементы, а не гоняет весь массив:
Куче надо следить всего за десятью значениями, поэтому на больших данных она заметно быстрее полной сортировки.
То же и для «топ-N самых маленьких» —
5. Группировка с вечной проверкой «а есть уже такой ключ?» 🏗
defaultdict убирает всю эту возню:
Просишь ключ, которого нет — он сам подставит пустой список, и можно сразу пихать
Тот же приём с
А как часто вы используете это или пишите по старинке? 🤔
#algorithms #python #performance #backend #dev #programming
Почти всё мы решаем массивом и словарём — и обычно норм
Но есть места, где привычная структура на ровном месте начинает тормозить, и на проде это больно
Причина почти всегда одна — взяли не ту структуру под задачу
Собрал частые проблемы и их решения
1. Проверка «есть ли такой элемент» в списке 🐌 Классика:
if user_id in users_list: # бежит по всему списку
Каждая такая проверка перебирает весь список от начала до конца, пока не найдёт
Один раз — ерунда
А если это внутри цикла по другому списку — получаешь перебор в переборе, и на десятках тысяч элементов всё встаёт
Причём в дебаге ты этого не увидишь
Если тебе нужно только «есть или нет» — бери множество (set):
users = set(users_list)
if user_id in users: # мгновенно, по хэшу
Set устроен так, что находит элемент сразу, не перебирая
Заплатил один раз за построение множества — дальше все проверки почти ничего не стоят
2. Удаление из начала списка ✂️ Тоже коварное:
queue.pop(0) # весь список сдвигается влево
Удалил первый элемент — и все остальные физически сьехало на одну позицию
На маленьком списке незаметно, а на большом каждый такой pop проходит по всему массиву
Есть deque — очередь, из которой можно быстро брать и с начала, и с конца:
from collections import deque
q = deque()
q.append(x) # в конец
q.popleft() # с начала, без сдвигов
Внутри это не сплошной кусок памяти, а связанные блоки, поэтому выдернуть элемент с любого конца — мгновенно
3. Считаем, сколько раз что встретилось 🔢
Обычно это пишут руками:
counts = {}
for x in items:
if x in counts:
counts[x] += 1
else:
counts[x] = 1Знакомо?
А между тем для этого есть готовый инструмент:
from collections import Counter
counts = Counter(items)
counts.most_common(3) # ещё и топ-3 сразу отдаст
Одна строчка вместо цикла с проверками — и сразу без багов. Плюс
Counter умеет складываться и вычитаться, так что если считаешь по кускам, можно просто сложить результаты4. Нужно вытащить десять самых больших значений из миллиона 🗑
sorted(data)[-10:] # перелопатил всё ради десятки
Ты отсортировал целый массив, чтобы взять с хвоста десять штук — остальные 999 990 отсортировал впустую
Для этого есть куча (heap) — она держит в уме только нужные элементы, а не гоняет весь массив:
import heapq
heapq.nlargest(10, data)
Куче надо следить всего за десятью значениями, поэтому на больших данных она заметно быстрее полной сортировки.
То же и для «топ-N самых маленьких» —
nsmallest5. Группировка с вечной проверкой «а есть уже такой ключ?» 🏗
groups = {}
for user in users:
if user.city not in groups:
groups[user.city] = []
groups[user.city].append(user)defaultdict убирает всю эту возню:
from collections import defaultdict
groups = defaultdict(list)
for user in users:
groups[user.city].append(user) # пустой список создастся сам
Просишь ключ, которого нет — он сам подставит пустой список, и можно сразу пихать
Тот же приём с
defaultdict(int) для счётчиков, если Counter почему-то не подходитА как часто вы используете это или пишите по старинке? 🤔
#algorithms #python #performance #backend #dev #programming
🐌 Почему UUID в первичном ключе тихо душит базу
UUID как id — вроде идеально: генерится на клиенте, не угадать, не конфликтует между сервисами
Все привыкли лепить его как первичный ключ и не думать
А потом база на паре миллионов строк начинает необъяснимо тупить на вставках
Разберём, почему так и что с этим делать
🎲 Корень зла — случайность Обычный UUID (v4) полностью случайный
Выглядит безобидно, но вот в чём засада: первичный ключ в базе хранится не кучей, а в отсортированном виде (B-tree индекс)
Когда id идёт по порядку (обычный автоинкремент 1, 2, 3...), каждая новая строка ложится в конец
А случайный UUID лезет каждый раз в новое случайное место в середину индекса
Базе приходится раздвигать уже забитые страницы, чтобы впихнуть строку между существующими
На потоке вставок таких разрывов тысячи, индекс пухнет и фрагментируется
💾 Мимо кэша Второй удар — по кэшу.
База держит в оперативке горячие страницы (buffer pool)
Когда вставки идут по порядку, ты всё время работаешь с одним и тем же «хвостом» индекса, он всегда в памяти
А со случайным UUID каждая вставка дёргает случайную страницу — сегодня эту, через миг вон ту
В память всё не влезает, и база начинает гонять страницы с диска
На больших таблицах это разница между «мгновенно» и «чего оно висит»
📏 И просто жирнее UUID — это 16 байт, а bigint — 8
Вроде мелочь, но первичный ключ тащится во все индексы и во все внешние ключи, что на него ссылаются Помножь на миллионы строк и десяток ссылающихся таблиц — набегает ощутимо
А если ты ещё и хранишь UUID строкой (
Так делать не надо.
🛠 Что делать Хорошая новость: отказываться от UUID не нужно, надо взять правильный
UUIDv7 — это UUID, у которого в начале зашито время создания
Он всё так же уникален и генерится на клиенте, но при этом растёт по порядку — новые id всегда больше старых Для базы он ложится так же аккуратно, как автоинкремент: в конец, без разрывов страниц и без прыжков по памяти
Забираешь все плюсы UUID и убираешь главный минус
Если UUIDv7 под рукой нет — есть ULID, работает по тому же принципу (время + случайность, растёт по порядку)
А если тебе вообще не нужна генерация на клиенте и распределённость — старый добрый bigint-автоинкремент по скорости не переплюнуть
И маленькое, но важное: храни UUID родным типом (
Иначе ты сверху к случайности добавляешь ещё и лишний вес
⚖️ Когда UUID реально оправдан
Чтобы не подумали, что UUID — это плохо
Он отлично заходит, когда id нужно сгенерить ДО похода в базу: например, клиент создаёт запись офлайн, или несколько сервисов пишут в одну таблицу и не должны драться за общий счётчик
Ещё UUID не даёт угадать соседние записи по id (с автоинкрементом любой видит, что до него было
Вопрос не в том, брать UUID или нет, а в том, чтобы он был упорядоченный, а не случайный
А вы уже переехали на v7? 🤔
#database #postgres #backend #performance #sql #dev
UUID как id — вроде идеально: генерится на клиенте, не угадать, не конфликтует между сервисами
Все привыкли лепить его как первичный ключ и не думать
А потом база на паре миллионов строк начинает необъяснимо тупить на вставках
Разберём, почему так и что с этим делать
🎲 Корень зла — случайность Обычный UUID (v4) полностью случайный
Выглядит безобидно, но вот в чём засада: первичный ключ в базе хранится не кучей, а в отсортированном виде (B-tree индекс)
Когда id идёт по порядку (обычный автоинкремент 1, 2, 3...), каждая новая строка ложится в конец
А случайный UUID лезет каждый раз в новое случайное место в середину индекса
Базе приходится раздвигать уже забитые страницы, чтобы впихнуть строку между существующими
На потоке вставок таких разрывов тысячи, индекс пухнет и фрагментируется
💾 Мимо кэша Второй удар — по кэшу.
База держит в оперативке горячие страницы (buffer pool)
Когда вставки идут по порядку, ты всё время работаешь с одним и тем же «хвостом» индекса, он всегда в памяти
А со случайным UUID каждая вставка дёргает случайную страницу — сегодня эту, через миг вон ту
В память всё не влезает, и база начинает гонять страницы с диска
На больших таблицах это разница между «мгновенно» и «чего оно висит»
📏 И просто жирнее UUID — это 16 байт, а bigint — 8
Вроде мелочь, но первичный ключ тащится во все индексы и во все внешние ключи, что на него ссылаются Помножь на миллионы строк и десяток ссылающихся таблиц — набегает ощутимо
А если ты ещё и хранишь UUID строкой (
varchar(36)) вместо родного типа — это уже 36+ байт, и вообще больТак делать не надо.
🛠 Что делать Хорошая новость: отказываться от UUID не нужно, надо взять правильный
UUIDv7 — это UUID, у которого в начале зашито время создания
Он всё так же уникален и генерится на клиенте, но при этом растёт по порядку — новые id всегда больше старых Для базы он ложится так же аккуратно, как автоинкремент: в конец, без разрывов страниц и без прыжков по памяти
Забираешь все плюсы UUID и убираешь главный минус
-- вместо случайного uuid_generate_v4()
-- берём time-ordered v7 (в свежих версиях уже из коробки)
Если UUIDv7 под рукой нет — есть ULID, работает по тому же принципу (время + случайность, растёт по порядку)
А если тебе вообще не нужна генерация на клиенте и распределённость — старый добрый bigint-автоинкремент по скорости не переплюнуть
И маленькое, но важное: храни UUID родным типом (
uuid в постгресе, binary(16) в mysql), а не строкойИначе ты сверху к случайности добавляешь ещё и лишний вес
⚖️ Когда UUID реально оправдан
Чтобы не подумали, что UUID — это плохо
Он отлично заходит, когда id нужно сгенерить ДО похода в базу: например, клиент создаёт запись офлайн, или несколько сервисов пишут в одну таблицу и не должны драться за общий счётчик
Ещё UUID не даёт угадать соседние записи по id (с автоинкрементом любой видит, что до него было
/order/1041, а значит есть и /order/1040)Вопрос не в том, брать UUID или нет, а в том, чтобы он был упорядоченный, а не случайный
А вы уже переехали на v7? 🤔
#database #postgres #backend #performance #sql #dev
👍2
📄 Почему пагинация тормозит на дальних страницах
Пагинация через
Первая страница летает, а где-нибудь на 500-й всё еле ползёт
Хотя отдаёшь ты те же 20 строк
Как сделать, чтобы скорость не зависела от номера страницы
🐌 Что не так с OFFSET:
Ты думаешь «дай мне 20 штук со сдвигом»
А база понимает это буквально: она проходит первые 100 000 строк, отсчитывает их одну за другой, выбрасывает — и только потом отдаёт следующие 20
То есть чем дальше страница, тем больше строк она перелопачивает впустую
На первой странице OFFSET 0, летает
На тысячной — прогоняет сотню тысяч строк ради двадцати
🚀 Keyset-пагинация
Идея простая: не считать сдвиг, а запоминать, на чём остановились, и просить «дай мне то, что идёт после вот этого»
Здесь база не отсчитывает ничего — она по индексу сразу прыгает в нужное место и берёт двадцать штук
Скорость одинаковая что на первой странице, что на миллионной
Ты просто передаёшь с каждой страницей «якорь» последней строки (обычно id или дату), а на следующий запрос отдаёшь его обратно
⚖️ В чём подвох Keyset не панацея, у него есть ограничение: нельзя прыгнуть сразу на «страницу 500»
Ты можешь идти только вперёд и назад, от текущего места
Для нумерации страниц 1-2-3...500 это не годится
Но честно — а где тебе реально нужны номера страниц?
В бесконечной ленте, подгрузке по скроллу, выгрузке данных пачками, API с курсором — везде листают последовательно, и keyset там идеален
А классический OFFSET оставь для админок и мест, где страниц пара десятков и на скорость плевать
🎭 Бонусная беда OFFSET — он ещё и врёт
Про скорость понятно, но есть второй, менее очевидный косяк
Пока юзер листает, в таблицу сыплются новые записи
Добавили пару строк в начало (а сортировка-то по свежести) — и весь список поехал на две позиции вниз
В итоге на второй странице юзер снова видит записи, которые уже пролистал на первой
Классика лент, где при скролле «мелькают одинаковые посты»
Keyset этим не болеет: якорь привязан к конкретной строке, а не к порядковому номеру, поэтому новые вставки список не сдвигают
И маленький нюанс по самому keyset: сортируй по чему-то уникальному
Если у двух строк одинаковая дата, а якорь только по дате — на границе страниц можно потерять или задублировать запись
С уникальным ключом такого не будет
💡 Про счётчик «страница 3 из 500»
Частый вопрос: а как же показать общее число страниц, если мы листаем по Keyset?
На больших таблицах точный total ты и с OFFSET безболезненно не получишь,
Поэтому в больших лентах его обычно и не показывают: либо примерное число («около 10k+»), либо просто кнопка «ещё»
Если точный счётчик реально нужен — это отдельная история с кэшированием, а не то, ради чего стоит держать медленный OFFSET
А вы как реализуете большие списки? 🤔
#database #postgres #backend #performance #sql #dev
Пагинация через
LIMIT / OFFSET есть почти везде — и работает отлично ровно до тех пор, пока юзер не улистал далекоПервая страница летает, а где-нибудь на 500-й всё еле ползёт
Хотя отдаёшь ты те же 20 строк
Как сделать, чтобы скорость не зависела от номера страницы
🐌 Что не так с OFFSET:
SELECT * FROM posts
ORDER BY created_at DESC
LIMIT 20 OFFSET 100000;
Ты думаешь «дай мне 20 штук со сдвигом»
А база понимает это буквально: она проходит первые 100 000 строк, отсчитывает их одну за другой, выбрасывает — и только потом отдаёт следующие 20
То есть чем дальше страница, тем больше строк она перелопачивает впустую
На первой странице OFFSET 0, летает
На тысячной — прогоняет сотню тысяч строк ради двадцати
🚀 Keyset-пагинация
Идея простая: не считать сдвиг, а запоминать, на чём остановились, и просить «дай мне то, что идёт после вот этого»
SELECT * FROM posts
WHERE created_at < :last_seen_created_at
ORDER BY created_at DESC
LIMIT 20;
Здесь база не отсчитывает ничего — она по индексу сразу прыгает в нужное место и берёт двадцать штук
Скорость одинаковая что на первой странице, что на миллионной
Ты просто передаёшь с каждой страницей «якорь» последней строки (обычно id или дату), а на следующий запрос отдаёшь его обратно
⚖️ В чём подвох Keyset не панацея, у него есть ограничение: нельзя прыгнуть сразу на «страницу 500»
Ты можешь идти только вперёд и назад, от текущего места
Для нумерации страниц 1-2-3...500 это не годится
Но честно — а где тебе реально нужны номера страниц?
В бесконечной ленте, подгрузке по скроллу, выгрузке данных пачками, API с курсором — везде листают последовательно, и keyset там идеален
А классический OFFSET оставь для админок и мест, где страниц пара десятков и на скорость плевать
🎭 Бонусная беда OFFSET — он ещё и врёт
Про скорость понятно, но есть второй, менее очевидный косяк
Пока юзер листает, в таблицу сыплются новые записи
Добавили пару строк в начало (а сортировка-то по свежести) — и весь список поехал на две позиции вниз
В итоге на второй странице юзер снова видит записи, которые уже пролистал на первой
Классика лент, где при скролле «мелькают одинаковые посты»
Keyset этим не болеет: якорь привязан к конкретной строке, а не к порядковому номеру, поэтому новые вставки список не сдвигают
И маленький нюанс по самому keyset: сортируй по чему-то уникальному
Если у двух строк одинаковая дата, а якорь только по дате — на границе страниц можно потерять или задублировать запись
С уникальным ключом такого не будет
💡 Про счётчик «страница 3 из 500»
Частый вопрос: а как же показать общее число страниц, если мы листаем по Keyset?
На больших таблицах точный total ты и с OFFSET безболезненно не получишь,
COUNT(*) по миллионам строк сам по себе тормозитПоэтому в больших лентах его обычно и не показывают: либо примерное число («около 10k+»), либо просто кнопка «ещё»
Если точный счётчик реально нужен — это отдельная история с кэшированием, а не то, ради чего стоит держать медленный OFFSET
А вы как реализуете большие списки? 🤔
#database #postgres #backend #performance #sql #dev
👍2
🕐 Почему вечно «едут» даты
Со временем всё просто — до первого бага
У юзера отчёт за «вчера» показывает не то, напоминалка падает на час раньше, а после перевода часов вообще всё разъехалось
И самое обидное — локально не повторить, у тебя-то сходится
Разберём, обо что тут все спотыкаются и как перестать воевать
🌍 Правило номер один: всё в UTC
Главная засада — хранить в базе местное время
Пока сервис в одном городе — вроде норм
А как появятся юзеры из разных зон (или сервак переедет) — и ты уже сам не понимаешь, что за 14:00 лежит в базе
По Москве? По серверу? По юзеру? Фиг знает
Как надо: внутри системы всё живёт в UTC — и база, и логика
А местное время — это просто «как показать юзеру», его считаешь в самый последний момент
Прилетело время от клиента — сразу перегнал в UTC и забыл
Отдаёшь наружу — перегнал в зону юзера
🎭 Время без зоны — это мина
В коде время бывает двух видов: с зоной и без
Без зоны — это голое 2026-08-19 14:00, и непонятно, это 14:00 вообще где
Такие штуки нельзя спокойно сравнивать и складывать — рано или поздно бахнет
dt = datetime(2026, 8, 19, 14, 0, tzinfo=timezone.utc)
🌗 Перевод часов — отдельная боль
Даже если зона одна, есть летнее/зимнее время, и тут два прикола, про которые вспоминают только когда уже горит: — один и тот же час может случиться дважды (когда стрелки крутят назад); — а иногда его не бывает вообще (когда крутят вперёд — час просто исчезает)
Поэтому и нельзя держать местное время и думать, что отмотаешь назад
📅 «Вчера» — это когда вообще?
Классика аналитики: юзер хочет отчёт за «вчера», а сутки у всех кончаются в разное время
У чувака в Москве день закрылся в 21:00 UTC, у другого в Нью-Йорке — совсем в другой момент
Режешь сутки по серверному времени — у половины народа «вчера» будет кривым
Границы дня надо считать в зоне юзера, и только потом гнать в UTC для запроса
🔢 Ещё пара вещей, чтоб не наступить
В Postgres храни момент в timestamptz, а не в timestamp
Первый — момент времени, второй — цифры без привязки, и это ловушка
Не парси даты строками руками
Есть ISO 8601 (2026-08-19T14:00:00Z) — понятный формат с зоной, гоняй время между сервисами только так. — Зоны бери из базы IANA (Europe/Moscow), а не хардкодь смещение +3
Страны меняют переводы часов, база обновляется — хардкод нет
⏱️ А ещё есть Unix timestamp
Отдельная штука, которую любят и часто понимают наполовину
Unix timestamp — это число секунд, прошедших с 1 января 1970
Штука отличная: это UTC по своей природе, зоны в нём нет, сравнивать и хранить — одно удовольствие, никаких «а это по чьему времени»
Но есть нюанс: он говорит только про момент и ничего не помнит про то, какая это была дата у юзера
Так что timestamp — это не замена зонам, а просто удобный способ хранить сам момент.
Держи момент в UTC и половина мистических багов с датами отваливается сама.
Расскажите, про баги, которые возникали у вас из-за проблем со временем? 🤔
#backend #database #dev #programming #datetime #postgres
Со временем всё просто — до первого бага
У юзера отчёт за «вчера» показывает не то, напоминалка падает на час раньше, а после перевода часов вообще всё разъехалось
И самое обидное — локально не повторить, у тебя-то сходится
Разберём, обо что тут все спотыкаются и как перестать воевать
🌍 Правило номер один: всё в UTC
Главная засада — хранить в базе местное время
Пока сервис в одном городе — вроде норм
А как появятся юзеры из разных зон (или сервак переедет) — и ты уже сам не понимаешь, что за 14:00 лежит в базе
По Москве? По серверу? По юзеру? Фиг знает
Как надо: внутри системы всё живёт в UTC — и база, и логика
А местное время — это просто «как показать юзеру», его считаешь в самый последний момент
Прилетело время от клиента — сразу перегнал в UTC и забыл
Отдаёшь наружу — перегнал в зону юзера
🎭 Время без зоны — это мина
В коде время бывает двух видов: с зоной и без
Без зоны — это голое 2026-08-19 14:00, и непонятно, это 14:00 вообще где
Такие штуки нельзя спокойно сравнивать и складывать — рано или поздно бахнет
dt = datetime(2026, 8, 19, 14, 0, tzinfo=timezone.utc)
🌗 Перевод часов — отдельная боль
Даже если зона одна, есть летнее/зимнее время, и тут два прикола, про которые вспоминают только когда уже горит: — один и тот же час может случиться дважды (когда стрелки крутят назад); — а иногда его не бывает вообще (когда крутят вперёд — час просто исчезает)
Поэтому и нельзя держать местное время и думать, что отмотаешь назад
📅 «Вчера» — это когда вообще?
Классика аналитики: юзер хочет отчёт за «вчера», а сутки у всех кончаются в разное время
У чувака в Москве день закрылся в 21:00 UTC, у другого в Нью-Йорке — совсем в другой момент
Режешь сутки по серверному времени — у половины народа «вчера» будет кривым
Границы дня надо считать в зоне юзера, и только потом гнать в UTC для запроса
🔢 Ещё пара вещей, чтоб не наступить
В Postgres храни момент в timestamptz, а не в timestamp
Первый — момент времени, второй — цифры без привязки, и это ловушка
Не парси даты строками руками
Есть ISO 8601 (2026-08-19T14:00:00Z) — понятный формат с зоной, гоняй время между сервисами только так. — Зоны бери из базы IANA (Europe/Moscow), а не хардкодь смещение +3
Страны меняют переводы часов, база обновляется — хардкод нет
⏱️ А ещё есть Unix timestamp
Отдельная штука, которую любят и часто понимают наполовину
Unix timestamp — это число секунд, прошедших с 1 января 1970
Штука отличная: это UTC по своей природе, зоны в нём нет, сравнивать и хранить — одно удовольствие, никаких «а это по чьему времени»
Но есть нюанс: он говорит только про момент и ничего не помнит про то, какая это была дата у юзера
Так что timestamp — это не замена зонам, а просто удобный способ хранить сам момент.
Держи момент в UTC и половина мистических багов с датами отваливается сама.
Расскажите, про баги, которые возникали у вас из-за проблем со временем? 🤔
#backend #database #dev #programming #datetime #postgres
13 сентября наш CEO Альфред Столяров и Евгения Добижа, зам. директора по персоналу ООО «ЦИТ», выступят на конференции Город IT в Томске с докладом о том, как теперь со всем этим жить.
Взгляд сразу с двух сторон: от тех, кто продаёт экспертизу разработки, и от тех, кто её покупает.
Обсудим:
✅ три числа, по которым судят команду: стоимость, ошибки, сроки
✅ метрики ценности инженера — Utilization, Cycle Time, Bug Rate, MTTR. Не ощущения тимлида, а цифры
✅ почему разработчик выходит из тени — на демо и в диалог с заказчиком
✅ ИИ как стажёр, который не устаёт: где работает, а где мы обожглись
✅ репутацию вместо «+50% за прыжок»
Утешительных прогнозов не будет. Похорон отрасли — тоже.
🤓 Полезнее всего будет разработчикам и аналитикам, руководителям IT-компаний и тем, кто нанимает разработку.
📍 Город IT 2026, 13 сентября
Томск, БКЗ филармонии, пл. Ленина, 12А
📎 Регистрация уже вовсю идет здесь
🔥1
Скоро осень - а значит и новый деловой сезон 🍁
Приурочили к его началу новый вебинар для ИТ-руководителей!
Точечные интеграции рано или поздно превращаются в клубок, который тормозит развитие. Но нужна ли вам полноценная шина — или можно обойтись без неё? И если нужна, кто её внедрит и будет поддерживать?
29 сентября разберём вопрос с двух сторон — технологии и ресурсы:
Сергей Скирдин (CTO «Белый Код») — когда шина действительно нужна, какие ESB-решения есть на российском рынке и как их внедрять
Альфред Столяров (CEO EvApps) — как собрать команду под проект: найм, фикс-прайс, аутстафф или гибрид, и как не остаться без экспертизы после сдачи проекта
Для CDO, руководителей ИТ и технических лидеров в промышленности, логистике, ритейле и финансах.
29 сентября, 12:00–13:00, онлайн
Участие бесплатно, но регистрация обязательна: по итогам вебинара пришлем всем зарегистрировавшимся полезные материалы
Зарегистрироваться
Приурочили к его началу новый вебинар для ИТ-руководителей!
Точечные интеграции рано или поздно превращаются в клубок, который тормозит развитие. Но нужна ли вам полноценная шина — или можно обойтись без неё? И если нужна, кто её внедрит и будет поддерживать?
29 сентября разберём вопрос с двух сторон — технологии и ресурсы:
Сергей Скирдин (CTO «Белый Код») — когда шина действительно нужна, какие ESB-решения есть на российском рынке и как их внедрять
Альфред Столяров (CEO EvApps) — как собрать команду под проект: найм, фикс-прайс, аутстафф или гибрид, и как не остаться без экспертизы после сдачи проекта
Для CDO, руководителей ИТ и технических лидеров в промышленности, логистике, ритейле и финансах.
29 сентября, 12:00–13:00, онлайн
Участие бесплатно, но регистрация обязательна: по итогам вебинара пришлем всем зарегистрировавшимся полезные материалы
Зарегистрироваться
Please open Telegram to view this post
VIEW IN TELEGRAM
💸 Деньги во float — и почему баланс однажды не сойдётся
0.1 + 0.2 в float даёт не 0.3, а 0.30000000000000004
Само по себе это ерунда, но именно на этом хвосте держится половина багов с деньгами — и вылезают они не там, где хотелось бы
Разберём, где именно и как хранить деньги, чтобы потом не искать расхождения по всему проекту
🧮 Погрешность не страшна поштучно — страшна на потоке
Одно сложение с хвостом в пятнадцатом знаке погоды не делает
Проблема в накоплении: тысячи операций — суммы, проценты, скидки, конвертации — и хвосты потихоньку копятся
В какой-то момент SUM по строкам не сходится с общим итогом на копейку
И это уже не «ну почти», а расхождение в отчёте, к которому приходит финотдел
На копейку в деньгах ответ «это округление float» не принимается
🛠 Как хранить нормально
Два рабочих варианта, оба правильные
Первый — целые в минимальных единицах
Хранишь не 19.99, а 1999 копеек, целым числом
Целые складываются и вычитаются идеально точно, никаких двоичных хвостов в принципе
На показ юзеру делишь на сотню
Просто и надёжно, так живёт большинство платёжек.
Второй — специальный денежный тип, который считает в десятичной системе, а не в двоичной
В базах это NUMERIC (он же DECIMAL), в языках — готовые классы: BigDecimal в Java, Decimal в C#, тип Decimal из модуля decimal в Python
Внутри он хранит число как есть, по десятичным разрядам, поэтому 0.1 + 0.2 там честно даёт 0.3, без хвоста
Считает медленнее обычного float, но на денежных объёмах эта разница роли не играет
Что выбрать: целые копейки удобнее, когда операции простые (сложить, вычесть, показать)
Денежный тип приятнее там, где много процентов, дробей и округлений — он сам держит нужную точность и правила округления
Оба варианта рабочие, главное — не float.
➗ Целые копейки не отменяют математику
С копейками точно всё, пока не доходит до деления
Раскидай 100 рублей на троих: 33.33 + 33.33 + 33.33 = 99.99
Копейка испарилась
Обычно остаток отдают кому-то одному (первому, последнему — как договоришься), но сумма кусков обязана сойтись с целым
То же с процентами и кэшбэком: реши заранее, как округляешь — вверх, вниз или по-банковски к чётному — и держи это в одном месте
💱 Множитель ×100 — тоже ловушка
Работает, пока валюта одна
Появилась вторая — и ломается: у иены копеек нет вообще (множитель 1), у динара Бахрейна три знака (×1000)
Поэтому сумму держим в паре с валютой (amount + currency), а множитель тянем из справочника, а не хардкодим * 100 по всему проекту
Иначе на другой валюте всё может разъехаться в сто раз, и хорошо если это рано заметят
🌐 Когда отдаешь суммы - за этим тоже надо следить
Внутри всё правильно — а потом отдал наружу 19.99 числом в JSON, и на том конце парсер прочитал его обратно как float с хвостом
Гоняй между сервисами строкой ("19.99") или целыми единицами (1999) плюс валюта
Тогда на обоих концах сумма останется той же, что была
Короче: деньги — это целые копейки или decimal, всегда с валютой рядом, и точность нельзя терять ни при делении, ни на границе API
Float оставь в покое— в кошельках ему не место.
#backend #database #dev #programming #fintech #sql
0.1 + 0.2 в float даёт не 0.3, а 0.30000000000000004
Само по себе это ерунда, но именно на этом хвосте держится половина багов с деньгами — и вылезают они не там, где хотелось бы
Разберём, где именно и как хранить деньги, чтобы потом не искать расхождения по всему проекту
🧮 Погрешность не страшна поштучно — страшна на потоке
Одно сложение с хвостом в пятнадцатом знаке погоды не делает
Проблема в накоплении: тысячи операций — суммы, проценты, скидки, конвертации — и хвосты потихоньку копятся
В какой-то момент SUM по строкам не сходится с общим итогом на копейку
И это уже не «ну почти», а расхождение в отчёте, к которому приходит финотдел
На копейку в деньгах ответ «это округление float» не принимается
🛠 Как хранить нормально
Два рабочих варианта, оба правильные
Первый — целые в минимальных единицах
Хранишь не 19.99, а 1999 копеек, целым числом
Целые складываются и вычитаются идеально точно, никаких двоичных хвостов в принципе
На показ юзеру делишь на сотню
Просто и надёжно, так живёт большинство платёжек.
Второй — специальный денежный тип, который считает в десятичной системе, а не в двоичной
В базах это NUMERIC (он же DECIMAL), в языках — готовые классы: BigDecimal в Java, Decimal в C#, тип Decimal из модуля decimal в Python
Внутри он хранит число как есть, по десятичным разрядам, поэтому 0.1 + 0.2 там честно даёт 0.3, без хвоста
Считает медленнее обычного float, но на денежных объёмах эта разница роли не играет
Что выбрать: целые копейки удобнее, когда операции простые (сложить, вычесть, показать)
Денежный тип приятнее там, где много процентов, дробей и округлений — он сам держит нужную точность и правила округления
Оба варианта рабочие, главное — не float.
➗ Целые копейки не отменяют математику
С копейками точно всё, пока не доходит до деления
Раскидай 100 рублей на троих: 33.33 + 33.33 + 33.33 = 99.99
Копейка испарилась
Обычно остаток отдают кому-то одному (первому, последнему — как договоришься), но сумма кусков обязана сойтись с целым
То же с процентами и кэшбэком: реши заранее, как округляешь — вверх, вниз или по-банковски к чётному — и держи это в одном месте
💱 Множитель ×100 — тоже ловушка
Работает, пока валюта одна
Появилась вторая — и ломается: у иены копеек нет вообще (множитель 1), у динара Бахрейна три знака (×1000)
Поэтому сумму держим в паре с валютой (amount + currency), а множитель тянем из справочника, а не хардкодим * 100 по всему проекту
Иначе на другой валюте всё может разъехаться в сто раз, и хорошо если это рано заметят
🌐 Когда отдаешь суммы - за этим тоже надо следить
Внутри всё правильно — а потом отдал наружу 19.99 числом в JSON, и на том конце парсер прочитал его обратно как float с хвостом
Гоняй между сервисами строкой ("19.99") или целыми единицами (1999) плюс валюта
Тогда на обоих концах сумма останется той же, что была
Короче: деньги — это целые копейки или decimal, всегда с валютой рядом, и точность нельзя терять ни при делении, ни на границе API
Float оставь в покое— в кошельках ему не место.
#backend #database #dev #programming #fintech #sql
🔒 BEGIN/COMMIT не спасёт так, как ты думаешь
Обернул код в транзакцию — и кажется, что теперь всё под защитой, гонки не страшны, данные консистентны
На деле транзакция даёт меньше, чем от неё ждут, а самое интересное прячется в уровнях изоляции, которые почти никто не трогает
Разберём, что реально гарантирует транзакция и где она молча тебя подведёт
📦 Что даёт транзакция на самом деле
Главное, что делает BEGIN ... COMMIT — это атомарность: либо применились все изменения, либо ни одного
Списал с одного счёта, зачислил на другой, между ними упало — откатится всё, половина денег нигде не зависнет
Это работает и это ценно
Но есть второй вопрос, про который забывают: а что транзакция видит, пока рядом крутятся другие?
Вот это и есть изоляция — и у неё несколько уровней, от слабого к строгому
По умолчанию стоит не самый строгий
🎚 Уровни изоляции по-простому Read Committed
Ты видишь только то, что другие уже закоммитили
Звучит норм, но засада: два одинаковых SELECT внутри одной твоей транзакции могут вернуть разное — если между ними кто-то успел закоммитить изменение
То есть данные под тобой могут поменяться прямо по ходу
Repeatable Read
Транзакция работает как будто со снимком данных на момент своего старта
Сколько раз ни спроси — видишь одно и то же, даже если снаружи уже всё поменяли
В MySQL/InnoDB, кстати, это дефолт, а не Read Committed — приятная разница, о которую спотыкаются при переезде между базами
Serializable
Самый строгий: база ведёт себя так, будто транзакции шли строго по очереди, одна за другой
Максимальная защита, но за неё платишь — если база видит, что две транзакции конфликтуют, она откатывает одну с ошибкой сериализации, и ты должен её поймать и повторить
💥 Где это стреляет
Списание с баланса из поста про гонки:
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- прочитал 100
-- посчитал в коде: 100 - 50 = 50
UPDATE accounts SET balance = 50 WHERE id = 1;
COMMIT;
Многие думают: «я же в транзакции, всё ок»
А вот и нет
На дефолтном Read Committed две такие транзакции спокойно выполнятся параллельно, обе прочитают 100, обе запишут 50 — одно списание потерялось
Транзакция тут не помогла, потому что проблема не в атомарности, а в изоляции
🛠 Что с этим делать
Три рабочих пути, по ситуации:
Считать атомарно в самой базе, не таща значение в код:
UPDATE accounts SET balance = balance - 50
WHERE id = 1 AND balance >= 50;
Заблокировать строку на время работы через SELECT ... FOR UPDATE — тогда вторая транзакция подождёт, пока не закоммитишь
Поднять уровень до Serializable — база сама поймает конфликт, но тогда готовь ретрай на ошибку сериализации
⏱️ И держи транзакции короткими
Пока транзакция открыта, она держит блокировки и мешает другим
Открыл транзакцию, а внутри пошёл в чужой API или задумался на пользовательском вводе — и вот уже полбазы стоит в очереди за твоими строками
В транзакции должно быть только то, что реально должно примениться разом, и ничего медленного
А вы уровень изоляции вообще трогали — или живёте на дефолте и не паритесь? 🤔
#database #postgres #backend #sql #dev #programming
Обернул код в транзакцию — и кажется, что теперь всё под защитой, гонки не страшны, данные консистентны
На деле транзакция даёт меньше, чем от неё ждут, а самое интересное прячется в уровнях изоляции, которые почти никто не трогает
Разберём, что реально гарантирует транзакция и где она молча тебя подведёт
📦 Что даёт транзакция на самом деле
Главное, что делает BEGIN ... COMMIT — это атомарность: либо применились все изменения, либо ни одного
Списал с одного счёта, зачислил на другой, между ними упало — откатится всё, половина денег нигде не зависнет
Это работает и это ценно
Но есть второй вопрос, про который забывают: а что транзакция видит, пока рядом крутятся другие?
Вот это и есть изоляция — и у неё несколько уровней, от слабого к строгому
По умолчанию стоит не самый строгий
🎚 Уровни изоляции по-простому Read Committed
Ты видишь только то, что другие уже закоммитили
Звучит норм, но засада: два одинаковых SELECT внутри одной твоей транзакции могут вернуть разное — если между ними кто-то успел закоммитить изменение
То есть данные под тобой могут поменяться прямо по ходу
Repeatable Read
Транзакция работает как будто со снимком данных на момент своего старта
Сколько раз ни спроси — видишь одно и то же, даже если снаружи уже всё поменяли
В MySQL/InnoDB, кстати, это дефолт, а не Read Committed — приятная разница, о которую спотыкаются при переезде между базами
Serializable
Самый строгий: база ведёт себя так, будто транзакции шли строго по очереди, одна за другой
Максимальная защита, но за неё платишь — если база видит, что две транзакции конфликтуют, она откатывает одну с ошибкой сериализации, и ты должен её поймать и повторить
💥 Где это стреляет
Списание с баланса из поста про гонки:
BEGIN;
SELECT balance FROM accounts WHERE id = 1; -- прочитал 100
-- посчитал в коде: 100 - 50 = 50
UPDATE accounts SET balance = 50 WHERE id = 1;
COMMIT;
Многие думают: «я же в транзакции, всё ок»
А вот и нет
На дефолтном Read Committed две такие транзакции спокойно выполнятся параллельно, обе прочитают 100, обе запишут 50 — одно списание потерялось
Транзакция тут не помогла, потому что проблема не в атомарности, а в изоляции
🛠 Что с этим делать
Три рабочих пути, по ситуации:
Считать атомарно в самой базе, не таща значение в код:
UPDATE accounts SET balance = balance - 50
WHERE id = 1 AND balance >= 50;
Заблокировать строку на время работы через SELECT ... FOR UPDATE — тогда вторая транзакция подождёт, пока не закоммитишь
Поднять уровень до Serializable — база сама поймает конфликт, но тогда готовь ретрай на ошибку сериализации
⏱️ И держи транзакции короткими
Пока транзакция открыта, она держит блокировки и мешает другим
Открыл транзакцию, а внутри пошёл в чужой API или задумался на пользовательском вводе — и вот уже полбазы стоит в очереди за твоими строками
В транзакции должно быть только то, что реально должно примениться разом, и ничего медленного
А вы уровень изоляции вообще трогали — или живёте на дефолте и не паритесь? 🤔
#database #postgres #backend #sql #dev #programming
🔥1
🧩 Микросервисы, которые сделали только хуже
Распилили монолит на пятнадцать сервисов - вроде как «сделали по-взрослому»
А через полгода деплой стал сложнее, баг тянется через пять сервисов, латентность выросла, и никто уже не держит в голове, как оно всё связано
История настолько частая, что пора проговорить: микросервисы решают не ту проблему, которую им обычно приписывают
🎯 Микросервисы - это про команды, а не про код
Главное заблуждение: «проект большой, значит пора делить на микросервисы»
Но размер кода тут вообще ни при чём
Микросервисы нужны, когда у тебя много команд, и им тесно в одном репозитории - они мешают друг другу деплоить, релизы стопорятся, каждый выкат - согласование с пятью отделами
Вот тогда распил по сервисам даёт независимость: каждая команда катит своё, когда хочет
А если у тебя одна-две команды - ты этой независимости не получишь, зато купишь все минусы распределённой системы
И минусы не пустяковые
💥 За что реально платишь
Как только сервисы разъехались по сети, вылезает то, чего в монолите не было в принципе:
- Вызов функции превращается в сетевой запрос — а сеть падает, тормозит и таймаутит
То, что раньше было
- Транзакции больше не работают как раньше
В монолите ты завернул всё в один
А между сервисами общей транзакции нет - и консистентность приходится собирать руками через саги, компенсации и очереди
- Отладка становится квестом
Баг проходит через пять сервисов, и чтобы понять, где сломалось, нужен распределённый трейсинг
Без него ты просто смотришь в пять разных логов и гадаешь
- Версионирование API
Поменял формат ответа - и сломал троих соседей, которые про это не знали
Всё это - нормальная плата за независимость команд
Но если независимости нет, вы просто так усложняете себе жизнь
🧟 Худший вариант - распределённый монолит
Самое опасное - распил, после которого сервисы всё равно связаны намертво
Как понять, что ты попал именно в это:
- сервисы деплоятся только все вместе, по одному никак
- они лезут в одну общую базу и её таблицы
- один запрос юзера - это синхронная цепочка из пяти сервисов, и если падает любой, падает всё
Это худшее из двух миров: сложность распределённой системы и жёсткость монолита одновременно
🛠 Как по уму
Начинай с монолита — но аккуратного, с чёткими внутренними модулями и границами
Внутри одного процесса проводишь линии там, где логика реально разделяется
Когда (и если) команда вырастет или какой-то кусок реально упрётся в нагрузку и потребует отдельного масштабирования — вот тогда отрезаешь его в сервис по уже готовому шву
Резать по живому, «на всякий случай», заранее — почти всегда преждевременно
Микросервисы — это инструмент под конкретную боль: много команд, независимые релизы, части системы с разной нагрузкой
Нет этой боли — модульный монолит сделает то же самое
А у вас микросервисы реально по делу — или распилили, потому что «так модно», и теперь мучаетесь? 🤔
#architecture #backend #microservices #dev #programming #system
Распилили монолит на пятнадцать сервисов - вроде как «сделали по-взрослому»
А через полгода деплой стал сложнее, баг тянется через пять сервисов, латентность выросла, и никто уже не держит в голове, как оно всё связано
История настолько частая, что пора проговорить: микросервисы решают не ту проблему, которую им обычно приписывают
🎯 Микросервисы - это про команды, а не про код
Главное заблуждение: «проект большой, значит пора делить на микросервисы»
Но размер кода тут вообще ни при чём
Микросервисы нужны, когда у тебя много команд, и им тесно в одном репозитории - они мешают друг другу деплоить, релизы стопорятся, каждый выкат - согласование с пятью отделами
Вот тогда распил по сервисам даёт независимость: каждая команда катит своё, когда хочет
А если у тебя одна-две команды - ты этой независимости не получишь, зато купишь все минусы распределённой системы
И минусы не пустяковые
💥 За что реально платишь
Как только сервисы разъехались по сети, вылезает то, чего в монолите не было в принципе:
- Вызов функции превращается в сетевой запрос — а сеть падает, тормозит и таймаутит
То, что раньше было
if, теперь требует ретраев, таймаутов и обработки «а если сосед не ответил»- Транзакции больше не работают как раньше
В монолите ты завернул всё в один
BEGIN/COMMITА между сервисами общей транзакции нет - и консистентность приходится собирать руками через саги, компенсации и очереди
- Отладка становится квестом
Баг проходит через пять сервисов, и чтобы понять, где сломалось, нужен распределённый трейсинг
Без него ты просто смотришь в пять разных логов и гадаешь
- Версионирование API
Поменял формат ответа - и сломал троих соседей, которые про это не знали
Всё это - нормальная плата за независимость команд
Но если независимости нет, вы просто так усложняете себе жизнь
🧟 Худший вариант - распределённый монолит
Самое опасное - распил, после которого сервисы всё равно связаны намертво
Как понять, что ты попал именно в это:
- сервисы деплоятся только все вместе, по одному никак
- они лезут в одну общую базу и её таблицы
- один запрос юзера - это синхронная цепочка из пяти сервисов, и если падает любой, падает всё
Это худшее из двух миров: сложность распределённой системы и жёсткость монолита одновременно
🛠 Как по уму
Начинай с монолита — но аккуратного, с чёткими внутренними модулями и границами
Внутри одного процесса проводишь линии там, где логика реально разделяется
Когда (и если) команда вырастет или какой-то кусок реально упрётся в нагрузку и потребует отдельного масштабирования — вот тогда отрезаешь его в сервис по уже готовому шву
Резать по живому, «на всякий случай», заранее — почти всегда преждевременно
Микросервисы — это инструмент под конкретную боль: много команд, независимые релизы, части системы с разной нагрузкой
Нет этой боли — модульный монолит сделает то же самое
А у вас микросервисы реально по делу — или распилили, потому что «так модно», и теперь мучаетесь? 🤔
#architecture #backend #microservices #dev #programming #system
👍1
🚀 Как уронить прод одной миграцией
Выкатываешь релиз, в нём миграция - вроде добавить колонку, ерунда
А прод на сорок секунд встаёт колом, запросы висят, алерты орут
Миграции на живой базе - это минное поле, где безобидный ALTER TABLE блокирует всю таблицу
Обо что спотыкаются и как катить схему без даунтайма
🔒 Почему прод встаёт
Многие операции над таблицей берут блокировку, и пока она держится, все запросы к таблице ждут
На маленькой таблице это миллисекунды, а на большой - секунды и десятки секунд, в течение которых сайт фактически лежит
Три главных нарушителя:
- Добавление колонки с NOT NULL и значением по умолчанию
В старых версиях баз это переписывало всю таблицу целиком, с полной блокировкой
Чем больше строк - тем дольше стоишь
- Создание индекса обычным CREATE INDEX - блокирует запись в таблицу на всё время построения
- Переименование или удаление колонки, на которую ещё смотрит работающий код
И самое коварное: во время деплоя одновременно крутятся и старая, и новая версия приложения
Схема уже поменялась, а половина инстансов ещё живёт по-старому
Если миграция ломает старый код — часть юзеров ловит ошибки прямо во время выката
🧩 Главный принцип: expand → migrate → contract
Идея в том, чтобы никогда не менять схему одним резким движением, а разбить на шаги, где на каждом и старый, и новый код живут спокойно:
Expand — расширяешь схему обратимо: добавляешь новое, ничего не ломая
Новая колонка — обязательно nullable, без жёстких констрейнтов
Migrate — переносишь данные и переключаешь код на новое
Бэкфилл делаешь пачками, а не одним UPDATE на миллион строк (иначе снова блокировка и распухание)
Contract — только когда всё переехало и старый код мёртв, убираешь лишнее и навешиваешь констрейнты
Между шагами катятся отдельные релизы
Да, дольше, зато без даунтайма
🛠 Конкретные приёмы — Индексы строй
CREATE INDEX CONCURRENTLY — он не блокирует запись, строится в фоне
Медленнее, но прод жив
- Колонку добавляй сначала nullable, потом бэкфилли данные пачками, и только потом, отдельным шагом, вешай NOT NULL.
- Колонки не переименовывай на живой базе
Добавь новую, дублируй запись в обе, переведи чтение на новую, старую убери потом
Прямой RENAME мгновенно рассинхронит схему и работающий код
- Удаление - тоже в два захода: сперва перестань использовать в коде, выкати, убедись что ничего не отвалилось, и только следующим релизом дропай из базы
Общая мысль: код всегда должен уметь работать и со старой, и с новой схемой одновременно — потому что в момент деплоя ровно так и происходит
Пляши от этого, и миграции перестанут ронять прод
А вас миграция когда-нибудь роняла на проде? На чём именно поймали — индекс, NOT NULL, переименование? 🤔
#backend #database #postgres #devops #dev #sql
Выкатываешь релиз, в нём миграция - вроде добавить колонку, ерунда
А прод на сорок секунд встаёт колом, запросы висят, алерты орут
Миграции на живой базе - это минное поле, где безобидный ALTER TABLE блокирует всю таблицу
Обо что спотыкаются и как катить схему без даунтайма
🔒 Почему прод встаёт
Многие операции над таблицей берут блокировку, и пока она держится, все запросы к таблице ждут
На маленькой таблице это миллисекунды, а на большой - секунды и десятки секунд, в течение которых сайт фактически лежит
Три главных нарушителя:
- Добавление колонки с NOT NULL и значением по умолчанию
В старых версиях баз это переписывало всю таблицу целиком, с полной блокировкой
Чем больше строк - тем дольше стоишь
- Создание индекса обычным CREATE INDEX - блокирует запись в таблицу на всё время построения
- Переименование или удаление колонки, на которую ещё смотрит работающий код
И самое коварное: во время деплоя одновременно крутятся и старая, и новая версия приложения
Схема уже поменялась, а половина инстансов ещё живёт по-старому
Если миграция ломает старый код — часть юзеров ловит ошибки прямо во время выката
🧩 Главный принцип: expand → migrate → contract
Идея в том, чтобы никогда не менять схему одним резким движением, а разбить на шаги, где на каждом и старый, и новый код живут спокойно:
Expand — расширяешь схему обратимо: добавляешь новое, ничего не ломая
Новая колонка — обязательно nullable, без жёстких констрейнтов
Migrate — переносишь данные и переключаешь код на новое
Бэкфилл делаешь пачками, а не одним UPDATE на миллион строк (иначе снова блокировка и распухание)
Contract — только когда всё переехало и старый код мёртв, убираешь лишнее и навешиваешь констрейнты
Между шагами катятся отдельные релизы
Да, дольше, зато без даунтайма
🛠 Конкретные приёмы — Индексы строй
CREATE INDEX CONCURRENTLY — он не блокирует запись, строится в фоне
Медленнее, но прод жив
- Колонку добавляй сначала nullable, потом бэкфилли данные пачками, и только потом, отдельным шагом, вешай NOT NULL.
- Колонки не переименовывай на живой базе
Добавь новую, дублируй запись в обе, переведи чтение на новую, старую убери потом
Прямой RENAME мгновенно рассинхронит схему и работающий код
- Удаление - тоже в два захода: сперва перестань использовать в коде, выкати, убедись что ничего не отвалилось, и только следующим релизом дропай из базы
Общая мысль: код всегда должен уметь работать и со старой, и с новой схемой одновременно — потому что в момент деплоя ровно так и происходит
Пляши от этого, и миграции перестанут ронять прод
А вас миграция когда-нибудь роняла на проде? На чём именно поймали — индекс, NOT NULL, переименование? 🤔
#backend #database #postgres #devops #dev #sql
👍1
👀 Код-ревью, которое помогает, а не бесит
Ревью обычно скатывается в одну из двух крайностей
Либо «LGTM 👍» по диагонали через тридцать секунд — и тогда оно не ловит вообще ничего
Либо сорок комментариев про кавычки, отступы и «а я бы назвал переменную иначе» — и тогда автор просто начинает ненавидеть ревью
Обе крайности бесполезны
Разберём, что делает ревью реально работающим
🎯 Раздели блокеры и придирки
Главная беда плохого ревью — всё свалено в кучу
Баг в логике и пробел не там лежат в комментариях с одинаковым весом, и автор тонет( переписать всю строку)
Раздели явно:
- Блокеры - то, что нельзя мержить:
баги, дыры в безопасности, сломанная логика, архитектурная ошибка, которую потом дорого разгребать
- Придирки - вкусовщина и мелочи
Помечай их прямо, например
Когда автор видит, где горит, а где просто мнение — он чинит важное и не залипает на ерунде
🤖 Стиль — не человеческая работа
Если у тебя в комментариях к PR всплывают отступы, кавычки, порядок импортов и длина строки — это провал процесса, а не ревью
Всё это отдаётся линтеру и автоформаттеру, которые гоняются в CI
Человек не должен тратить внимание на то, что машина проверит идеально и без обид
Освободи ревью для того, что машина не умеет — смысл и решения
💬 Спрашивай, а не приказывай
Тон решает
«Исправь тут» звучит как приговор и включает у автора защиту
«А что будет, если сюда придёт null?» — это вопрос, который ведёт автора к проблеме самого
Часто выясняется, что он предусмотрел то, чего ты не заметил — или наоборот, сам натыкается на дыру
Ревью — это диалог, а не проверка домашки красной ручкой
📦 Маленькие PR — половина успеха
Это уже к автору
Гигантский PR на две тысячи строк физически невозможно отревьюить внимательно — глаз замыливается, и ревьюер начинает пролистывать и штамповать «ок»
Чем больше диф, тем поверхностнее ревью, это работает железно
Режь задачу на маленькие куски — их реально прочитать вдумчиво, и баги ловятся, а не проскакивают
Ну и главное, ради чего всё это
Ты ревьюишь не «к чему бы придраться», а отвечаешь на один вопрос: понимаю ли я это решение и готов ли поддерживать его завтра, когда автор будет в отпуске
Если да — мержишь
Если нет — разбираешься, пока не да
Всё остальное — шум
А у вас ревью — это про поиск багов и понимание, или больше про вкусовщину и «поставь пробел»? 🤔
#career #teamwork #codereview #dev #programming #softskills
Ревью обычно скатывается в одну из двух крайностей
Либо «LGTM 👍» по диагонали через тридцать секунд — и тогда оно не ловит вообще ничего
Либо сорок комментариев про кавычки, отступы и «а я бы назвал переменную иначе» — и тогда автор просто начинает ненавидеть ревью
Обе крайности бесполезны
Разберём, что делает ревью реально работающим
🎯 Раздели блокеры и придирки
Главная беда плохого ревью — всё свалено в кучу
Баг в логике и пробел не там лежат в комментариях с одинаковым весом, и автор тонет( переписать всю строку)
Раздели явно:
- Блокеры - то, что нельзя мержить:
баги, дыры в безопасности, сломанная логика, архитектурная ошибка, которую потом дорого разгребать
- Придирки - вкусовщина и мелочи
Помечай их прямо, например
nit: - мол, «на твоё усмотрение, мержу в любом случае»Когда автор видит, где горит, а где просто мнение — он чинит важное и не залипает на ерунде
🤖 Стиль — не человеческая работа
Если у тебя в комментариях к PR всплывают отступы, кавычки, порядок импортов и длина строки — это провал процесса, а не ревью
Всё это отдаётся линтеру и автоформаттеру, которые гоняются в CI
Человек не должен тратить внимание на то, что машина проверит идеально и без обид
Освободи ревью для того, что машина не умеет — смысл и решения
💬 Спрашивай, а не приказывай
Тон решает
«Исправь тут» звучит как приговор и включает у автора защиту
«А что будет, если сюда придёт null?» — это вопрос, который ведёт автора к проблеме самого
Часто выясняется, что он предусмотрел то, чего ты не заметил — или наоборот, сам натыкается на дыру
Ревью — это диалог, а не проверка домашки красной ручкой
📦 Маленькие PR — половина успеха
Это уже к автору
Гигантский PR на две тысячи строк физически невозможно отревьюить внимательно — глаз замыливается, и ревьюер начинает пролистывать и штамповать «ок»
Чем больше диф, тем поверхностнее ревью, это работает железно
Режь задачу на маленькие куски — их реально прочитать вдумчиво, и баги ловятся, а не проскакивают
Ну и главное, ради чего всё это
Ты ревьюишь не «к чему бы придраться», а отвечаешь на один вопрос: понимаю ли я это решение и готов ли поддерживать его завтра, когда автор будет в отпуске
Если да — мержишь
Если нет — разбираешься, пока не да
Всё остальное — шум
А у вас ревью — это про поиск багов и понимание, или больше про вкусовщину и «поставь пробел»? 🤔
#career #teamwork #codereview #dev #programming #softskills
❤2
🧠 Почему AI-агент тупеет к концу длинного диалога
Начинаешь сессию с агентом - он бодрый, умный, всё схватывает
Через час работы в том же чате он начинает забывать, что ты просил в начале, путаться, повторяться и городить чушь Кажется, что модель "устала"
На самом деле ты уперся в то, как устроен контекст - и это чинится, если понимать механику
📏 У модели есть окно, и оно не резиновое
Модель не помнит диалог как человек
На каждый запрос ей заново скармливается весь ваш разговор целиком - вся история сообщений, файлы, что ты кидал, её собственные ответы
Это и есть контекстное окно, и оно ограничено
Чем дольше сессия, тем оно забитее
И проблема не только в том, что "место кончается"
Проблема в том, что происходит с вниманием модели по мере заполнения
🌀 Context rot - почему растёт мусор, падает толк
Когда контекст маленький и по делу - модель держит всё в фокусе
Когда он раздувается до тысяч строк переписки, отладочных логов и десяти версий одного файла - внимание модели размазывается по всей этой каше
Нужные детали тонут среди неактуального
Отсюда знакомые симптомы:
- забывает инструкцию, которую ты дал в начале
- тащит устаревший вариант кода, который вы давно переписали
- повторяет то, что уже делал
- начинает уверенно выдумывать
Есть ещё эффект "потерянного в середине": модель лучше всего держит начало и конец контекста, а то, что застряло где-то посередине длинной простыни, замечает хуже
Так что важная деталь из середины часового диалога легко проходит мимо
💸 Плюс это ещё и дорого
Раз весь диалог гоняется на каждый запрос, то чем он длиннее, тем больше токенов ты жжёшь и тем дольше ждёшь ответ
Раздутый контекст бьёт и по качеству, и по кошельку, и по скорости одновременно
🛠 Что с этим делать
Приёмы простые, но их мало кто применяет:
- Новая задача — новая сессия
Не тащи в свежую фичу контекст, забитый вчерашней отладкой
Чистый чат почти всегда умнее замусоренного
- Подкидывай только релевантное
Не вываливай весь репозиторий "на всякий случай" — дай те два-три файла, что реально нужны
Больше контекста ≠ умнее, часто наоборот
- Делай /compact по ходу
Уперлись в длинный диалог — сделал /compat и с этой выжимкой начинай заново, выкинув простыню
- Держи инструкции ближе к концу
То, что критично прямо сейчас, повтори в свежем сообщении, а не надейся, что модель помнит это из начала
Агент не "тупеет от усталости", он тонет в собственном контексте
Держи контекст чистым и по делу — и модель будет хорошо отрабатывать сколько угодно
А вы как работаете с агентом — льёте всё в один длинный чат или дробите на чистые сессии? 🤔
#ai #llm #dev #programming #productivity #tools
Начинаешь сессию с агентом - он бодрый, умный, всё схватывает
Через час работы в том же чате он начинает забывать, что ты просил в начале, путаться, повторяться и городить чушь Кажется, что модель "устала"
На самом деле ты уперся в то, как устроен контекст - и это чинится, если понимать механику
📏 У модели есть окно, и оно не резиновое
Модель не помнит диалог как человек
На каждый запрос ей заново скармливается весь ваш разговор целиком - вся история сообщений, файлы, что ты кидал, её собственные ответы
Это и есть контекстное окно, и оно ограничено
Чем дольше сессия, тем оно забитее
И проблема не только в том, что "место кончается"
Проблема в том, что происходит с вниманием модели по мере заполнения
🌀 Context rot - почему растёт мусор, падает толк
Когда контекст маленький и по делу - модель держит всё в фокусе
Когда он раздувается до тысяч строк переписки, отладочных логов и десяти версий одного файла - внимание модели размазывается по всей этой каше
Нужные детали тонут среди неактуального
Отсюда знакомые симптомы:
- забывает инструкцию, которую ты дал в начале
- тащит устаревший вариант кода, который вы давно переписали
- повторяет то, что уже делал
- начинает уверенно выдумывать
Есть ещё эффект "потерянного в середине": модель лучше всего держит начало и конец контекста, а то, что застряло где-то посередине длинной простыни, замечает хуже
Так что важная деталь из середины часового диалога легко проходит мимо
💸 Плюс это ещё и дорого
Раз весь диалог гоняется на каждый запрос, то чем он длиннее, тем больше токенов ты жжёшь и тем дольше ждёшь ответ
Раздутый контекст бьёт и по качеству, и по кошельку, и по скорости одновременно
🛠 Что с этим делать
Приёмы простые, но их мало кто применяет:
- Новая задача — новая сессия
Не тащи в свежую фичу контекст, забитый вчерашней отладкой
Чистый чат почти всегда умнее замусоренного
- Подкидывай только релевантное
Не вываливай весь репозиторий "на всякий случай" — дай те два-три файла, что реально нужны
Больше контекста ≠ умнее, часто наоборот
- Делай /compact по ходу
Уперлись в длинный диалог — сделал /compat и с этой выжимкой начинай заново, выкинув простыню
- Держи инструкции ближе к концу
То, что критично прямо сейчас, повтори в свежем сообщении, а не надейся, что модель помнит это из начала
Агент не "тупеет от усталости", он тонет в собственном контексте
Держи контекст чистым и по делу — и модель будет хорошо отрабатывать сколько угодно
А вы как работаете с агентом — льёте всё в один длинный чат или дробите на чистые сессии? 🤔
#ai #llm #dev #programming #productivity #tools
🔥2
Пока вы наслаждаетесь пятницей, мы напоминаем, что уже завтра стартует одна из наших любимых IT-конференций - "Город IT" в Томске
В этом году наш CEO Альфред Столяров и зам.директора по персоналу ООО "ЦИТ" Евгения Добижа расскажут, почему прекрасная эпоха в IT закончилась - и как нам с этим жить
Приглашаем разработчиков, аналитиков, продактов и проджектов, а также ИТ-руководителей на секцию "Как меняется IT в России — взгляд со стороны бизнеса".
Томск, площадь Ленина, 12А, главный зал
13 сентября 13:30-15:30 по местному времени
Регистрация все еще идет: clck.ru/3Vm5yc
В этом году наш CEO Альфред Столяров и зам.директора по персоналу ООО "ЦИТ" Евгения Добижа расскажут, почему прекрасная эпоха в IT закончилась - и как нам с этим жить
Приглашаем разработчиков, аналитиков, продактов и проджектов, а также ИТ-руководителей на секцию "Как меняется IT в России — взгляд со стороны бизнеса".
Томск, площадь Ленина, 12А, главный зал
13 сентября 13:30-15:30 по местному времени
Регистрация все еще идет: clck.ru/3Vm5yc
👏1
🔌 "Too many connections" - и почему база падает под нагрузкой
Под нагрузкой прилетает
Первая мысль - "надо поднять лимит соединений в базе"
Обычно это лечение симптома, а не причины
А теперь, откуда берётся упор в соединения и почему пул решает это правильно
💰 Соединение к базе - дорогое удовольствие
Каждое подключение к базе - это не бесплатная абстракция
В том же Postgres на каждое соединение заводится отдельный процесс со своей памятью
Их число жёстко ограничено (
Поэтому "просто поднять лимит до 5000" - плохая идея
Ты не уберёшь проблему, а перенесёшь базу из состояния "отказывает" в состояние "еле дышит"
💥 Как упираешься в потолок
Приложение открывает новое соединение на каждый запрос и закрывает после
На малой нагрузке норм
Но под потоком - сто параллельных запросов открывают сто соединений одновременно, тысяча запросов - тысячу Лимит выбивается мгновенно, и новые запросы получают отказ
При этом само соединение ещё и открывается не моментально - на установку уходит время, так что ты платишь дважды
🏊 Пул соединений
Идея простая - не открывать соединение под каждый запрос, а держать наготове небольшой набор уже открытых и переиспользовать их
Запросу нужна база - он берёт свободное соединение из пула, отработал - вернул обратно, не закрывая
Соединений мало, они постоянно в деле, и на установку время не тратится
Пул бывает двух видов:
встроенный в приложение (HikariCP в Java, пулеры в других языках) и внешний - отдельный процесс перед базой (для Postgres это PgBouncer)
Внешний особенно важен, когда инстансов приложения много
⚠️ Ловушка масштабирования
У тебя двадцать инстансов приложения, у каждого свой пул на 20 соединений
Двадцать раз по двадцать — это уже 400 соединений к базе, даже если сейчас тихо
Раскатал автоскейлингом полсотни подов - и снова упёрся в лимит, хотя пулы вроде есть
Вот тут и ставят один общий внешний пулер (PgBouncer) перед базой: приложения ходят в него, а он держит небольшое число реальных соединений к базе
📉 И размер пула - не "чем больше, тем лучше"
Ещё контринтуитивная штука - огромный пул часто медленнее маленького
Если соединений больше, чем база способна реально обрабатывать параллельно (а это упирается в число ядер и диски), они начинают толкаться и мешать друг другу
Небольшой пул, где запросы аккуратно ждут своей очереди, нередко даёт больший throughput, чем раздутый
Так что размер подбирают под возможности базы, а не "на глаз побольше"
А вы на чём ловили «too many connections» — забыли пул, разъехались по инстансам, или крутанули autoscaling? 🤔
#backend #database #postgres #performance #devops #dev
Под нагрузкой прилетает
FATAL: too many connections, база отказывается принимать запросы, всё стоитПервая мысль - "надо поднять лимит соединений в базе"
Обычно это лечение симптома, а не причины
А теперь, откуда берётся упор в соединения и почему пул решает это правильно
💰 Соединение к базе - дорогое удовольствие
Каждое подключение к базе - это не бесплатная абстракция
В том же Postgres на каждое соединение заводится отдельный процесс со своей памятью
Их число жёстко ограничено (
max_connections), и не просто так: тысяча соединений - это тысяча процессов, которые сжирают память и заставляют базу тратить силы на переключение между ними, а не на работуПоэтому "просто поднять лимит до 5000" - плохая идея
Ты не уберёшь проблему, а перенесёшь базу из состояния "отказывает" в состояние "еле дышит"
💥 Как упираешься в потолок
Приложение открывает новое соединение на каждый запрос и закрывает после
На малой нагрузке норм
Но под потоком - сто параллельных запросов открывают сто соединений одновременно, тысяча запросов - тысячу Лимит выбивается мгновенно, и новые запросы получают отказ
При этом само соединение ещё и открывается не моментально - на установку уходит время, так что ты платишь дважды
🏊 Пул соединений
Идея простая - не открывать соединение под каждый запрос, а держать наготове небольшой набор уже открытых и переиспользовать их
Запросу нужна база - он берёт свободное соединение из пула, отработал - вернул обратно, не закрывая
Соединений мало, они постоянно в деле, и на установку время не тратится
Пул бывает двух видов:
встроенный в приложение (HikariCP в Java, пулеры в других языках) и внешний - отдельный процесс перед базой (для Postgres это PgBouncer)
Внешний особенно важен, когда инстансов приложения много
⚠️ Ловушка масштабирования
У тебя двадцать инстансов приложения, у каждого свой пул на 20 соединений
Двадцать раз по двадцать — это уже 400 соединений к базе, даже если сейчас тихо
Раскатал автоскейлингом полсотни подов - и снова упёрся в лимит, хотя пулы вроде есть
Вот тут и ставят один общий внешний пулер (PgBouncer) перед базой: приложения ходят в него, а он держит небольшое число реальных соединений к базе
📉 И размер пула - не "чем больше, тем лучше"
Ещё контринтуитивная штука - огромный пул часто медленнее маленького
Если соединений больше, чем база способна реально обрабатывать параллельно (а это упирается в число ядер и диски), они начинают толкаться и мешать друг другу
Небольшой пул, где запросы аккуратно ждут своей очереди, нередко даёт больший throughput, чем раздутый
Так что размер подбирают под возможности базы, а не "на глаз побольше"
too many connections - это почти всегда поставь пул, а под много инстансов — общий внешний пулер, и потолок перестанет быть потолкомА вы на чём ловили «too many connections» — забыли пул, разъехались по инстансам, или крутанули autoscaling? 🤔
#backend #database #postgres #performance #devops #dev
👍1🤩1