EvApps
199 subscribers
1.24K photos
52 videos
2 files
253 links
IT-aутстафферы из Тулы💚
https://evapps.ru/

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

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

▶️ Посмотреть, как мы живём: https://vk.com/evapps
Download Telegram
🗄 Почему твой SQL-индекс молчит
Знакомая история: повесил индекс, а запрос как тормозил, так и тормозит.
Индекс вроде есть, но планировщик смотрит на него и проходит мимо.
Собрал самые частые причины, из-за которых так происходит.
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 заказов и для каждого лезем за пользователем:
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 и клонов 🌲 Классика боли: пилишь фичу, прилетает срочный фикс.
Обычно ты либо стэшишь недоделку, либо клонируешь репо второй раз.
Ни то, ни другое не нужно:
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 — сколько времени можно не дёргать сервер
Главный заголовок:
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 / Стундент
🧱 Массив там, где нужен был не массив
Почти всё мы решаем массивом и словарём — и обычно норм
Но есть места, где привычная структура на ровном месте начинает тормозить, и на проде это больно
Причина почти всегда одна — взяли не ту структуру под задачу
Собрал частые проблемы и их решения

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
🐌 Почему 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
📄 Почему пагинация тормозит на дальних страницах
Пагинация через 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
Ранее в сериале: ставка 20% похоронила проекты с окупаемостью в три года, бюджеты ушли в ИБ и ИИ, вакансий −13% при +11% резюме, поиск работы растянулся на 3–5 месяцев, конкурент переехал в Азию, а «мидл» перестал означать «два года стажа».

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, онлайн

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

Зарегистрироваться
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
🔒 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
🧩 Микросервисы, которые сделали только хуже

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

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

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

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

💥 За что реально платишь
Как только сервисы разъехались по сети, вылезает то, чего в монолите не было в принципе:
- Вызов функции превращается в сетевой запрос — а сеть падает, тормозит и таймаутит
То, что раньше было 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
👍1
👀 Код-ревью, которое помогает, а не бесит
Ревью обычно скатывается в одну из двух крайностей
Либо «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
🔥2
Пока вы наслаждаетесь пятницей, мы напоминаем, что уже завтра стартует одна из наших любимых IT-конференций - "Город IT" в Томске

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

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

Томск, площадь Ленина, 12А, главный зал
13 сентября 13:30-15:30 по местному времени
Регистрация все еще идет: clck.ru/3Vm5yc
👏1
🔌 "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