Skip Navigation
Telegram
ORM vs Raw SQL 🪑


ORM vs Raw SQL 🪑

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

🪑 Стул №1: SQLAlchemy ORM

ORM — это не про скорость кода, это про скорость поставки фич. Вы работаете с объектами, а не со строками.

Почему это круто:
Unit of Work: Алхимия сама следит за изменениями объектов и пушит их в базу одним транзакционным паком.
Безопасность: SQL-инъекции? Забудьте. Если вы не используете .text(), вы защищены по умолчанию.
Миграции: Синхронизация схемы БД с вашими моделями — это просто магия, которая экономит часы рутины.
Domain Logic: Идеально для сложных E-commerce систем и админок, где связи между таблицами запутаннее, чем сюжет «Интерстеллара».

В чем подвох:
Overhead: Маппинг объектов — дорогая операция. На больших выборках (100k+ строк) вы почувствуете, как Python-процесс начинает жрать память и CPU.
N+1 Problem: Главный киллер производительности. Один забытый .joinedload() — и ваш сервис ложится под градом мелких запросов.

🪑 Стул №2: Raw SQL / asyncpg (Выбор для Highload)

Когда вы упираетесь в производительность, прослойки начинают мешать.

Почему это круто:
Экстремальная скорость: Разница в 2.5 раза — это не шутка. Бинарный протокол asyncpg позволяет выжимать максимум из сетевого канала.
Полный контроль: Вы пишете именно тот SQL, который пойдет в планировщик базы. Никакой лишней магии от ORM.
Специфические фичи БД: Попробуйте эффективно использовать JSONB или специфические индексы Postgres через ORM — иногда это превращается в сизифов труд.

В чем подвох:
Твоя ответственность: Забыл про экранирование параметров? Поздравляю, база слита.
Бойлерплейт: Придется руками мапить кортежи из базы в DTO/Pydantic модели. Это скучно и плодит ошибки.
Поддержка: Читать 500 строк сырого SQL в коде через полгода — сомнительное удовольствие.


💡 Вердикт: Золотой стандарт — гибридный подход

Не нужно выбирать что-то одно. Современная архитектура выглядит так:
1. SQLAlchemy ORM — для 90% задач: CRUD, бизнес-логика, миграции и админка.
2. SQLAlchemy Core — когда ORM-объекты слишком тяжелые, но писать сырые строки еще не хочется.
3. Raw SQL (asyncpg) — для узких мест: аналитические отчеты, массовые инсерты и микросервисы с нагрузкой 10k+ RPS.

Выбирайте стул по размеру нагрузки ☝️

А на чем пишете вы?
👨‍💻 Только ORM, жизнь слишком коротка для написания SQL руками.
👨‍💻 Только Raw SQL, не доверяю этой магии.

#два_стула
Telegram
Деревня Скулево 🔥


Деревня Скулево 🔥

Долго ли, коротко ли, добрались мы до деревни Скулево.
Жизнь тут идет своим чередом.

У ворот спорят про функции, операции и планировщики.
У корчмы мужики громко смеются над байкой, как один смельчак в пятницу сделал DROP DATABASE, а в понедельник уже искал новую работу.

Возле базара стоят сомнительные личности. Не иначе разбойники. Из разговора слышно:
SELECT * без LIMIT в печень. База не вечна.

Место странное. Но дабы мудрость получить, можно закрыть на это глаза 🤔

На входе каждому путнику выдают свиток. А на самом верху свитка объявление:

В лавку мастера по дереву Плотникова требуется помощник-счетовод.
Местный мастер готов обучить делу расчетному.
Опыт не требуется. Голова на плечах желательна 😁

Ниже - краткий свод правил деревни SQL.
1️⃣ Правило первое
SQL - это язык вопросов к данным.

2️⃣ Правило второе
Местные свитки называются таблицами.

3️⃣ Правило третье
Каждый запрос должен отвечать на конкретный вопрос.

4️⃣ Правило четвертое
SELECT * без нужды - привычка разбойничья.

5️⃣Правило пятое
Запрос без проверки - это гадание на щепках.

А теперь пора в лавку. Мастер Плотников уже ждет.
Свитки на столе. Учет, разумеется, не сходится 🤦‍♂️

Я пока соберу базу свитков для отработки навыков.
Каждый новый пост будет одной задачей из жизни лавки 😎

#sql

@data_dzen 🙂

Напоминаю, что сегодня в 19:00 по МСК будет вебинар на тему "Старичок Excel еще может".
Ссылка на встречу:

us06web.zoom.us/j/83865573817
Telegram
Бродячий торговец 🤔


Бродячий торговец 🤔

На пути к деревне «Скьюелево» мы набрели на лесок, возле которого стояла лавка торговца.
Надпись на ней была диковинная: «Лавка мудрости».

За прилавком продавец стоял. Глаза хитрющие, руки загребущие 🪙
Отоварил нас пройдоха на целых четыре свитка мудрости.

Вот и список этих свитков.
1. Логика и мышление на собесах
2. Блок по SQL на собесах.
3. Продуктовый блок ( retention, когорты) простыми аналогиями.
4. А/B тесты и с чем их едят.

Погостили немного у торговца, но пора и честь знать.
Ай да выдвигаться к деревне «Скьюелево» и получать новые свитки знаний
😎

@data_dzen 🙂
Telegram
Весенняя эра открыта 😎


Весенняя эра открыта 😎

Время обновить дизайн на теплые оттенки согревающие душу. Собрал новый дизайн, что бы каждый пост отдавал весенней энергией 🔥

Такс пойдем теперь за вопросы продуктовые поговорим 👇

Тебе дают две таблицы - users и events. И 20 минут на задачу:
«Посчитай батенька retention по когортам M0–M3»

И вот на этом ловят ступор.
А с чего вообще стартовать? 🤔

Проще всего - не с SQL, а с картинки в голове. Представь спортзал.
100 человек купили абонемент - это твоя когорта.
Дальше обычно эту когорту принимают за 100%. Это и есть M0.
Но вот нюанс: это не закон природы, а просто негласное соглашение.
На следующий день пришли 60 человек - Day 1 = 60%.
Через неделю пришли 40 - Day 7 = 40%.

Дальше логика не меняется.
Просто вместо дней - месяцы.
И вместо спортзала - продукт.

1️⃣ Junior-уровень - понять, сколько людей вообще пришло в каждую когорту
SELECT
DATE_TRUNC('month', registration_date) AS cohort_month,
COUNT(DISTINCT user_id) AS new_users
FROM users
GROUP BY 1
ORDER BY 1;

Если уже здесь ошибка, все суши весла. Потому что DATE_TRUNC и DISTINCT - это база продуктового блока.
Хотя бы для PostgreSQL и похожих диалектов.

2️⃣ Следующий уровень - понять, кто вернулся
WITH cohort_dec AS (
SELECT user_id, registration_date
FROM users
WHERE registration_date >= '2023-12-01'
AND registration_date < '2024-01-01'
)
SELECT
COUNT(*) AS cohort_size,
COUNT(DISTINCT CASE WHEN event_date = registration_date + 1 THEN user_id END) AS day1,
COUNT(DISTINCT CASE WHEN event_date = registration_date + 7 THEN user_id END) AS day7
FROM cohort_dec u
LEFT JOIN events e ON u.user_id = e.user_id;

Тут под ноги бросаются грабли 🤔

Такой код нормален, если даты у тебя хранятся как DATE, без времени.
Но если это TIMESTAMP, простое равенство может ломать расчёт.
Потому что у одного событие в 2023-12-08 00:01, у другого в 2023-12-08 19:42, и формально это уже не то же самое значение.

Значит, нужно либо приводить к дате, либо считать через диапазоны.
То есть логика всегда одна и та же:
✅ зафиксировал когорту → посмотрел, кто вернулся

Для M1, M2, M3 всё почти то же самое, но есть принципиальный момент:
👉 сравнивают не просто даты, а смещение по календарным месяцам относительно месяца регистрации ( да сложно звучит сейчас объясню )

То есть не +30 дней. Месяцы разной длины, поэтому такой расчёт съезжает.
Для месячного retention смотрят смещение по календарным месяцам, а не просто прибавляют 30 дней.

Где еще грабли поджидают

1️⃣ Считают события вместо людей
Один пользователь сделал 5 событий.
И внезапно retention у тебя больше 100%.
Значит, ты считаешь не возврат людей, а активность.

2️⃣ Ставят INNER JOIN
И в выборке остаются только те, кто вернулся.
Все, кто отвалился, просто исчезают.
Получается красивая цифра. И полностью фальшивая картина.

3️⃣ Не фиксируют базу расчёта
Размер когорты обычно принимают за 100%, и уже от него считают всё дальше.
Если база у тебя гуляет, проценты превращаются в мусор.

А дальше начинается главная беда... тест на мышление и софты. Вот уж где самая тернистая тропа если мало опыта.
Потому что на собесе тебя не спросят:
«Как посчитать retention?»

А спросят:
«D30 упал. Почему?»

Приехали...И вот тут SQL уже никого не впечатляет.

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

Ответ который сразу поставит крест на вашем диалоге
«Ну… retention снизился, потому что пользователи стали хуже возвращаться..»

Спасибо товарищ Капитан. Но это не ответ аналитика, а догадка.

Если простыми словами:
Retention - это не про SQL, это про поведение.
SQL - это просто лопата, которой ты выкапываешь цифру.


Если ты не можешь объяснить, почему люди перестали возвращаться,
то сами по себе твои проценты ни о чем не расскажут 🤷‍♀️

У вас бывали проблемы на продуктовом блоке? И вообще любо ли вам продуктовое направление или больше нравиться инженерная часть ? 👇
Telegram
Поговорим о собесах (часть 2) 😎


Поговорим о собесах (часть 2) 😎

В прошлом посте затронули софты, методологию STAR и Excel. Это базовый набор. Теперь идём дальше, SQL.

Если вы хорошо освоили Excel, вы уже понимаете основы: как данные связываются, какие проблемы бывают при связках, как работают группировки. Переход к SQL на старте это по сути 5 команд: SELECT, FROM, WHERE, JOIN, GROUP BY. Оконные функции и временные таблицы полезны, но на старте не критичны.

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

1️⃣NOT IN + NULL: тишина вместо данных

В SQL: NULL ≠ пусто ≠ 0. Кто изучал Python, знает: ноль это ноль, а NULL это пустота, ничто. Держите в голове.

Задача: найти пользователей не из чёрного списка.


SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM blocked)

Казалось бы, всё верно. Но если в blocked хоть одна строка с user_id = NULL, результат: 0 строк. SQL не может сравнить с NULL и молча возвращает пустоту.


SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM blocked b
WHERE b.user_id = u.id
)

Кстати, NOT EXISTS ещё и быстрее: берёт id, идёт во вторую таблицу, нашёл первое вхождение, остановился. NOT IN шерстит таблицу сверху донизу целиком. На больших таблицах разница заметная.

2️⃣ LEFT JOIN, который молча стал INNER JOIN

Про типы JOIN подробно разберём в следующих постах серии. Сейчас только суть ловушки.

SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o ON c.id = o.customer_id
WHERE o.status = 'completed'

Что происходит: WHERE убивает NULL-строки из правой таблицы. Клиенты без заказов просто исчезли. LEFT JOIN превратился в INNER JOIN. А результат мимо.

SELECT c.name, o.amount
FROM customers c
LEFT JOIN orders o
ON c.id = o.customer_id
AND o.status = 'completed'

Одна строчка через AND. Разница принципиальная.

3️⃣AVG врёт красиво

Вот вам задачка. 10 строк по продажам. У трёх NULL. Сколько вернёт AVG? Среднее по 7, не по 10. Если значения похожи на правду, подвох заметить сложно. Цифра реалистична, но искажена.

SELECT AVG(salary) FROM employees



SELECT AVG(COALESCE(salary, 0)) FROM employees

COALESCE: встретил NULL, заменил на 0. Можно и без него, но тогда объясните интервьюеру, что понимаете поведение AVG и принимаете его осознанно. Оба варианта зачтутся 👌

4️⃣Порядок выполнения: главный подвох

Пишем: SELECT → FROM → WHERE → GROUP BY.
Выполняется: FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY.

Нелогично? Ещё как 🤷‍♀️


SELECT category, AVG(sales) AS avg_sales
FROM orders
WHERE avg_sales > 1000
GROUP BY category

Алиас avg_sales ещё не существует на этапе WHERE. Система просто не знает этого поля.


SELECT category, AVG(sales) AS avg_sales
FROM orders
GROUP BY category
HAVING AVG(sales) > 1000

HAVING это фильтрация после группировки. Подробнее разберём в отдельном посте серии.

5️⃣Забыли синтаксис? Не паникуйте

Бывает: нервы, давно не касались темы. Ну с кем не бывает. Говорите прямо: «Я понимаю логику, могу описать шаги. Точный синтаксис загуглил бы». Любой адекватный специалист это оценит. Логику понимать одно дело. Функцию загуглить = 30 секунд.

Бонус напоследок: первый вопрос при тестовом = «Какая у вас СУБД?».
DATE_TRUNC — PostgreSQL. DATE_FORMAT — MySQL. Правильный запрос для неправильной базы = ошибка на ровном месте.

В следующем посте SQL: Retention и когорты. А завтра пост и статья про кейс моей команды с компанией Гранд-Альфа. Занимаются кормами для животных.

Вопрос к вам. У вас бывало такое, что забыли ответ и хоть убей не помнишь ? Как действовали в этой ситуации? 👇

#sql
@data_dzen