TGViewer
Channel Public Channel
EvApps

EvApps

@evapps_team

IT-aутстафферы из Тулы💚
https://evapps.ru/

Здесь пишем про веб- и мобильную разработку

▶️ Наш чат для системных аналитиков: https://t.me/pro_sa_evapps

▶️ Посмотреть, как мы живём: https://vk.com/evapps
Subscribers
199
Photos
1.2K
Videos
52
Links
253
Recent Posts 20 shown
Post #1542 30
🧩 Что тут не так? Код-загадка
Формат новый - показываю код, ты угадываешь подвох, ниже разбор
Питон, но грабли универсальные
def add_item(item, cart=[]):
cart.append(item)
return cart

Функция кладёт товар в корзину
Если корзину не передали - создаёт пустую
Логично же?

Вопрос: что вернёт код, если вызвать функцию три раза подряд для разных юзеров, каждый раз без передачи корзины?
add_item("яблоко")   # ?
add_item("хлеб") # ?
add_item("молоко") # ?

Интуиция говорит: каждый вызов - новая пустая корзина, значит вернётся ["яблоко"], потом ["хлеб"], потом ["молоко"]
. . .
А на деле:
["яблоко"]
["яблоко", "хлеб"]
["яблоко", "хлеб", "молоко"]
Корзина одна на всех, и товары в неё накапливаются
Три разных юзера сложили покупки в общую тележку

🔍 Почему так
Значение по умолчанию (этот самый []) создаётся один раз - в момент, когда питон читает определение функции, а не при каждом вызове
Дальше все вызовы, которые не передали свой аргумент, работают с одним и тем же списком
Он живёт между вызовами и копит всё, что в него положили
Особенно весело это выстреливает на проде: локально по одному запросу всё чисто, а под потоком данные разных юзеров начинают протекать друг к другу
Ловится потом такое тяжело - код же выглядит правильным

🛠 Как чинить
Дефолтом ставим не список, а None, и создаём свежий список внутри:
def add_item(item, cart=None):
if cart is None:
cart = []
cart.append(item)
return cart

Теперь каждый вызов без корзины получает свою собственную, пустую
Изменяемый объект (список, словарь, множество) в значении по умолчанию - почти всегда мина
Ставь None и создавай внутри

Кто угадал подвох с первого взгляда - press f? 🤔

#python #dev #programming #backend #bugs #квиз
Post #1541 46
🎭 Мифы про производительность, в которые верят даже опытные

Миф 1: "Меньше строк кода - быстрее работает"

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

Миф 2: "Оптимизировать надо везде"
На самом деле почти весь код не на горячем пути - он выполняется редко, и его скорость никого не волнует
Реальные тормоза сидят в паре мест, и найти их можно только профайлером, а не на глаз
Оптимизировать всё подряд - значит усложнять код там, где это не даёт ничего, и раздувать сроки

Миф 3: "Кэш всегда ускоряет"
На самом деле кэш добавляет отдельный слой со своими багами - инвалидацией, устареванием, лавинами на протухших ключах
Иногда убрать лишний запрос к базе или повесить индекс даёт тот же выигрыш, но без нового источника проблем
Кэш - это размен скорости на сложность

Миф 4: "Больше потоков - быстрее"
На самом деле только до предела
Потоков больше, чем ядер и ресурсов, - и они начинают толкаться, драться за блокировки и тратить время на переключение
В какой-то момент добавление потоков не ускоряет, а замедляет
Параллелизм помогает ровно до того места, где упираешься в реальное железо

Миф 5: "База медленная, вынесу логику в код"
На самом деле база десятилетиями заточена под работу с данными - фильтрацию, сортировку, агрегацию
Вытащить всё в приложение и крутить руками обычно медленнее, чем один нормальный запрос
Тот же N+1 - это как раз попытка сделать в коде то, что база сделала бы одним движением

Общий смысл под всеми пятью один: производительность - это не про интуицию и красивые приёмы, а про измерения, и использование только там где надо
Не гадай, где медленно - померь профайлером, и часто окажется, что тормозит совсем не там, где ты собирался оптимизировать

А какие мифы вы слышали слышали и сами так думали до определенного момента? 🤔

#performance #backend #optimization #dev #programming #database
  • 🔥 1
Post #1540 58
🚨 Как перевод денег уронил нам прод
⏰ 19:10

Задеплоили долгожданное - переводы между кошельками юзеров. Списываем с одного, зачисляем на другой, всё в одной транзакции, чтобы деньги не потерялись
На стейдже гоняли, всё зелёное

⏰ 19:40
Посыпались ошибки, часть переводов падает
В логах - "deadlock detected"
Не таймаут, не отвал базы, а взаимная блокировка
Вылезает только когда переводов много и идут они параллельно

19:55
Картина складывается
Перевод от Ани к Боре берёт блокировку на строку Ани, потом тянется за строкой Бори
А в ту же секунду перевод от Бори к Ане блокирует строку Бори и тянется за строкой Ани
Оба ждут друг друга, никто не отпустит

База ловит это сама: видит цикл ожидания, убивает одну из транзакций с ошибкой дедлока
Юзер получает "перевод не прошёл", хотя по сути ничего не сломано

20:20
Причина не в переводах, а в порядке
Мы блокировали строки как "сначала отправитель, потом получатель"
Направление у переводов разное, вот встречные пары и встают лицом к лицу

Лочим строки в одном и том же порядке, независимо от того, кто кому платит
Взяли сортировку по id: сначала строка с меньшим id, потом с большим
Теперь две встречные транзакции идут за строками одинаково, и вместо взаимной блокировки одна ждёт другую

🧠 Что забрали с собой
- Дедлок - это про порядок захвата ресурсов, а не про их количество
Двум транзакциям хватает взять два одинаковых замка в разной последовательности
- На стейдже такое не ловится, там нет параллельной нагрузки
Дедлок живёт только там, где реально пересекаются одновременные операции
- Трогаешь в транзакции несколько строк - бери их в детерминированном порядке
По id, по ключу, как угодно, лишь бы одинаково у всех
- И держи в голове: база всё равно иногда будет кидать дедлоки, это норма под нагрузкой
Ретрай на такую ошибку - не костыль, а штатный сценарий

Одна строчка сортировки, а стоила вечера и пачки упавших переводов

А вы ловили дедлоки? На чём - переводы, счётчики, обновление связанных таблиц? 🤔

#backend #database #postgres #concurrency #dev #incident
Post #1539 65
🗃 Кэш поставили, а он отдаёт старьё
В программировании две сложные вещи - инвалидация кэша и придумывание имён
С кэшем так и вышло
Закэшировать - минута работы, а заставить вовремя отдавать свежее - вот где начинается веселье Разберём, почему кэш врёт и как с этим жить

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

⏳ Вариант 1: TTL, само протухает по времени

Кладёшь данные на N минут, дальше они испаряются, и следующий запрос тянет свежак из базы

Плюс - просто, ничего не забудешь
Минус - есть окно: данные уже поменялись, а TTL ещё не вышел, и юзер видит старое
Для ленты или счётчика лайков норм, минутная задержка никого не убьёт
Для баланса или прав доступа - уже стрёмно

🎯 Вариант 2: сбрасывать кэш при изменении
Поменял данные - сразу выкинул их из кэша (или перезаписал)
Точность идеальная, старья нет

Звучит красиво, но тут прячется главная подстава
Данные меняются не в одном месте
Есть основная ручка апдейта - её помнят
А ещё админка, фоновый джоб, миграция, импорт, соседний сервис - и если хоть один путь поменял данные, но забыл сбросить кэш, получаешь вечно устаревшую запись, которую потом фиг найдёшь
Чем больше мест правит данные, тем легче забыть про одно

💥 И ещё пара граблей
- Лавина (стампед). Популярный ключ протух - и в ту же секунду сотня запросов ломанулась в базу пересчитывать одно и то же
Кэш, который снимал нагрузку, наоборот устраивает базе микро-DDoS
Лечится так: пересчитывает кто-то один, остальные ждут результат
- Сброс из пушки по воробьям. Из страха отдать старьё некоторые на любое изменение чистят пол-кэша
Формально всё свежее, а толку ноль - кэш вечно пустой и не работает

🛠 Как выбирать
- Данные терпят лёгкое устаревание (лента, счётчики, справочники) - бери TTL, просто и без риска забыть
- Устаревание недопустимо (деньги, права, статусы) - сбрасывай явно при записи, но тогда честно найди ВСЕ места, где данные меняются, и сбрось в каждом
Пропустил одно - поймал баг
- Не уверен, что надёжно инвалидируешь - честнее не кэшировать, чем потом ловить призрачные баги со старьём

Прежде чем кэшировать, ответь себе, как будешь это оттуда выкидывать
Нет ответа - не кэшируй

А вас кэш подставлял? 🤔

#backend #performance #architecture #dev #programming #cache
  • ❤ 1
Post #1538 73
🔌 "Too many connections" - и почему база падает под нагрузкой
Под нагрузкой прилетает 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
Post #1537 59
Пока вы наслаждаетесь пятницей, мы напоминаем, что уже завтра стартует одна из наших любимых IT-конференций - "Город IT" в Томске

В этом году наш CEO Альфред Столяров и зам.директора по персоналу ООО "ЦИТ" Евгения Добижа расскажут, почему прекрасная эпоха в IT закончилась - и как нам с этим жить

Приглашаем разработчиков, аналитиков, продактов и проджектов, а также ИТ-руководителей на секцию "Как меняется IT в России — взгляд со стороны бизнеса".

Томск, площадь Ленина, 12А, главный зал
13 сентября 13:30-15:30 по местному времени
Регистрация все еще идет: clck.ru/3Vm5yc
  • 👏 1
Post #1536 62
🧠 Почему AI-агент тупеет к концу длинного диалога

Начинаешь сессию с агентом - он бодрый, умный, всё схватывает
Через час работы в том же чате он начинает забывать, что ты просил в начале, путаться, повторяться и городить чушь Кажется, что модель "устала"
На самом деле ты уперся в то, как устроен контекст - и это чинится, если понимать механику

📏 У модели есть окно, и оно не резиновое
Модель не помнит диалог как человек
На каждый запрос ей заново скармливается весь ваш разговор целиком - вся история сообщений, файлы, что ты кидал, её собственные ответы
Это и есть контекстное окно, и оно ограничено
Чем дольше сессия, тем оно забитее

И проблема не только в том, что "место кончается"
Проблема в том, что происходит с вниманием модели по мере заполнения

🌀 Context rot - почему растёт мусор, падает толк
Когда контекст маленький и по делу - модель держит всё в фокусе
Когда он раздувается до тысяч строк переписки, отладочных логов и десяти версий одного файла - внимание модели размазывается по всей этой каше
Нужные детали тонут среди неактуального

Отсюда знакомые симптомы:
- забывает инструкцию, которую ты дал в начале
- тащит устаревший вариант кода, который вы давно переписали
- повторяет то, что уже делал
- начинает уверенно выдумывать

Есть ещё эффект "потерянного в середине": модель лучше всего держит начало и конец контекста, а то, что застряло где-то посередине длинной простыни, замечает хуже
Так что важная деталь из середины часового диалога легко проходит мимо

💸 Плюс это ещё и дорого
Раз весь диалог гоняется на каждый запрос, то чем он длиннее, тем больше токенов ты жжёшь и тем дольше ждёшь ответ
Раздутый контекст бьёт и по качеству, и по кошельку, и по скорости одновременно

🛠 Что с этим делать
Приёмы простые, но их мало кто применяет:
- Новая задача — новая сессия
Не тащи в свежую фичу контекст, забитый вчерашней отладкой
Чистый чат почти всегда умнее замусоренного
- Подкидывай только релевантное
Не вываливай весь репозиторий "на всякий случай" — дай те два-три файла, что реально нужны
Больше контекста ≠ умнее, часто наоборот
- Делай /compact по ходу
Уперлись в длинный диалог — сделал /compat и с этой выжимкой начинай заново, выкинув простыню
- Держи инструкции ближе к концу
То, что критично прямо сейчас, повтори в свежем сообщении, а не надейся, что модель помнит это из начала

Агент не "тупеет от усталости", он тонет в собственном контексте
Держи контекст чистым и по делу — и модель будет хорошо отрабатывать сколько угодно

А вы как работаете с агентом — льёте всё в один длинный чат или дробите на чистые сессии? 🤔

#ai #llm #dev #programming #productivity #tools
  • 🔥 2
Post #1535 66
👀 Код-ревью, которое помогает, а не бесит
Ревью обычно скатывается в одну из двух крайностей
Либо «LGTM 👍» по диагонали через тридцать секунд — и тогда оно не ловит вообще ничего
Либо сорок комментариев про кавычки, отступы и «а я бы назвал переменную иначе» — и тогда автор просто начинает ненавидеть ревью
Обе крайности бесполезны
Разберём, что делает ревью реально работающим

🎯 Раздели блокеры и придирки

Главная беда плохого ревью — всё свалено в кучу
Баг в логике и пробел не там лежат в комментариях с одинаковым весом, и автор тонет( переписать всю строку)
Раздели явно:
- Блокеры - то, что нельзя мержить:
баги, дыры в безопасности, сломанная логика, архитектурная ошибка, которую потом дорого разгребать
- Придирки - вкусовщина и мелочи
Помечай их прямо, например nit: - мол, «на твоё усмотрение, мержу в любом случае»
Когда автор видит, где горит, а где просто мнение — он чинит важное и не залипает на ерунде

🤖 Стиль — не человеческая работа
Если у тебя в комментариях к PR всплывают отступы, кавычки, порядок импортов и длина строки — это провал процесса, а не ревью
Всё это отдаётся линтеру и автоформаттеру, которые гоняются в CI
Человек не должен тратить внимание на то, что машина проверит идеально и без обид
Освободи ревью для того, что машина не умеет — смысл и решения

💬 Спрашивай, а не приказывай
Тон решает
«Исправь тут» звучит как приговор и включает у автора защиту
«А что будет, если сюда придёт null?» — это вопрос, который ведёт автора к проблеме самого
Часто выясняется, что он предусмотрел то, чего ты не заметил — или наоборот, сам натыкается на дыру
Ревью — это диалог, а не проверка домашки красной ручкой

📦 Маленькие PR — половина успеха
Это уже к автору
Гигантский PR на две тысячи строк физически невозможно отревьюить внимательно — глаз замыливается, и ревьюер начинает пролистывать и штамповать «ок»
Чем больше диф, тем поверхностнее ревью, это работает железно
Режь задачу на маленькие куски — их реально прочитать вдумчиво, и баги ловятся, а не проскакивают
Ну и главное, ради чего всё это
Ты ревьюишь не «к чему бы придраться», а отвечаешь на один вопрос: понимаю ли я это решение и готов ли поддерживать его завтра, когда автор будет в отпуске
Если да — мержишь
Если нет — разбираешься, пока не да
Всё остальное — шум

А у вас ревью — это про поиск багов и понимание, или больше про вкусовщину и «поставь пробел»? 🤔

#career #teamwork #codereview #dev #programming #softskills
  • ❤ 2
Post #1534 75
🚀 Как уронить прод одной миграцией
Выкатываешь релиз, в нём миграция - вроде добавить колонку, ерунда
А прод на сорок секунд встаёт колом, запросы висят, алерты орут
Миграции на живой базе - это минное поле, где безобидный 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
Post #1533 78
🧩 Микросервисы, которые сделали только хуже

Распилили монолит на пятнадцать сервисов - вроде как «сделали по-взрослому»
А через полгода деплой стал сложнее, баг тянется через пять сервисов, латентность выросла, и никто уже не держит в голове, как оно всё связано
История настолько частая, что пора проговорить: микросервисы решают не ту проблему, которую им обычно приписывают

🎯 Микросервисы - это про команды, а не про код

Главное заблуждение: «проект большой, значит пора делить на микросервисы»
Но размер кода тут вообще ни при чём
Микросервисы нужны, когда у тебя много команд, и им тесно в одном репозитории - они мешают друг другу деплоить, релизы стопорятся, каждый выкат - согласование с пятью отделами
Вот тогда распил по сервисам даёт независимость: каждая команда катит своё, когда хочет

А если у тебя одна-две команды - ты этой независимости не получишь, зато купишь все минусы распределённой системы
И минусы не пустяковые

💥 За что реально платишь
Как только сервисы разъехались по сети, вылезает то, чего в монолите не было в принципе:
- Вызов функции превращается в сетевой запрос — а сеть падает, тормозит и таймаутит
То, что раньше было if, теперь требует ретраев, таймаутов и обработки «а если сосед не ответил»
- Транзакции больше не работают как раньше
В монолите ты завернул всё в один BEGIN/COMMIT
А между сервисами общей транзакции нет - и консистентность приходится собирать руками через саги, компенсации и очереди
- Отладка становится квестом
Баг проходит через пять сервисов, и чтобы понять, где сломалось, нужен распределённый трейсинг
Без него ты просто смотришь в пять разных логов и гадаешь
- Версионирование API
Поменял формат ответа - и сломал троих соседей, которые про это не знали

Всё это - нормальная плата за независимость команд
Но если независимости нет, вы просто так усложняете себе жизнь

🧟 Худший вариант - распределённый монолит

Самое опасное - распил, после которого сервисы всё равно связаны намертво
Как понять, что ты попал именно в это:
- сервисы деплоятся только все вместе, по одному никак
- они лезут в одну общую базу и её таблицы
- один запрос юзера - это синхронная цепочка из пяти сервисов, и если падает любой, падает всё

Это худшее из двух миров: сложность распределённой системы и жёсткость монолита одновременно


🛠 Как по уму
Начинай с монолита — но аккуратного, с чёткими внутренними модулями и границами
Внутри одного процесса проводишь линии там, где логика реально разделяется
Когда (и если) команда вырастет или какой-то кусок реально упрётся в нагрузку и потребует отдельного масштабирования — вот тогда отрезаешь его в сервис по уже готовому шву
Резать по живому, «на всякий случай», заранее — почти всегда преждевременно

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

А у вас микросервисы реально по делу — или распилили, потому что «так модно», и теперь мучаетесь? 🤔

#architecture #backend #microservices #dev #programming #system
  • 👍 1
Post #1532 70
🔒 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
  • 🔥 1
Post #1531 75
💸 Деньги во 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
Post #1530 237
Скоро осень - а значит и новый деловой сезон 🍁
Приурочили к его началу новый вебинар для ИТ-руководителей!

Точечные интеграции рано или поздно превращаются в клубок, который тормозит развитие. Но нужна ли вам полноценная шина — или можно обойтись без неё? И если нужна, кто её внедрит и будет поддерживать?

29 сентября разберём вопрос с двух сторон — технологии и ресурсы:

Сергей Скирдин (CTO «Белый Код») — когда шина действительно нужна, какие ESB-решения есть на российском рынке и как их внедрять
Альфред Столяров (CEO EvApps) — как собрать команду под проект: найм, фикс-прайс, аутстафф или гибрид, и как не остаться без экспертизы после сдачи проекта

Для CDO, руководителей ИТ и технических лидеров в промышленности, логистике, ритейле и финансах.

29 сентября, 12:00–13:00, онлайн

Участие бесплатно, но регистрация обязательна: по итогам вебинара пришлем всем зарегистрировавшимся полезные материалы

Зарегистрироваться
Post #1529 80
Есть новость для тех четырех РП, что сидят в этом чате🤗
Post #1528 292
Ранее в сериале: ставка 20% похоронила проекты с окупаемостью в три года, бюджеты ушли в ИБ и ИИ, вакансий −13% при +11% резюме, поиск работы растянулся на 3–5 месяцев, конкурент переехал в Азию, а «мидл» перестал означать «два года стажа».

13 сентября наш CEO Альфред Столяров и Евгения Добижа, зам. директора по персоналу ООО «ЦИТ», выступят на конференции Город IT в Томске с докладом о том, как теперь со всем этим жить.

Взгляд сразу с двух сторон: от тех, кто продаёт экспертизу разработки, и от тех, кто её покупает.

Обсудим:

✅ три числа, по которым судят команду: стоимость, ошибки, сроки
✅ метрики ценности инженера — Utilization, Cycle Time, Bug Rate, MTTR. Не ощущения тимлида, а цифры
✅ почему разработчик выходит из тени — на демо и в диалог с заказчиком
✅ ИИ как стажёр, который не устаёт: где работает, а где мы обожглись
✅ репутацию вместо «+50% за прыжок»

Утешительных прогнозов не будет. Похорон отрасли — тоже.

🤓 Полезнее всего будет разработчикам и аналитикам, руководителям IT-компаний и тем, кто нанимает разработку.

📍 Город IT 2026, 13 сентября
Томск, БКЗ филармонии, пл. Ленина, 12А

📎 Регистрация уже вовсю идет здесь
  • 🔥 1
Post #1527 72
🕐 Почему вечно «едут» даты
Со временем всё просто — до первого бага
У юзера отчёт за «вчера» показывает не то, напоминалка падает на час раньше, а после перевода часов вообще всё разъехалось
И самое обидное — локально не повторить, у тебя-то сходится
Разберём, обо что тут все спотыкаются и как перестать воевать

🌍 Правило номер один: всё в 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
Post #1526 69
📄 Почему пагинация тормозит на дальних страницах
Пагинация через 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
Post #1525 97
🐌 Почему UUID в первичном ключе тихо душит базу
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
Post #1524 88
🧱 Массив там, где нужен был не массив
Почти всё мы решаем массивом и словарём — и обычно норм
Но есть места, где привычная структура на ровном месте начинает тормозить, и на проде это больно
Причина почти всегда одна — взяли не ту структуру под задачу
Собрал частые проблемы и их решения

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 самых маленьких» — nsmallest

5. Группировка с вечной проверкой «а есть уже такой ключ?» 🏗
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
Older posts →

About this channel

How can I read @evapps_team without a Telegram account?
TGViewer shows the public web preview Telegram publishes for EvApps: recent posts, photos, videos and the subscriber count, with no app, login or account.
How many subscribers does EvApps have?
EvApps (@evapps_team) has 199 subscribers on Telegram, refreshed roughly every 30 minutes.
Does EvApps know I viewed it here?
No. Public channel previews carry no viewer identity, and TGViewer has no accounts or tracking of what you look up.
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →