SQL-ловушка: этот UPDATE может незаметно испортить данные в продакшене
Код корректный, PostgreSQL не выдаст ошибку. Но результат не так очевиден.
Есть таблица товаров:
Применяем скидки:
Что вернёт последний SELECT?
А)
Б)
В)
Г)
Один запрос, две скидки на один товар. Как PostgreSQL обработает такой UPDATE?
Подвох связан с поведением
Код корректный, PostgreSQL не выдаст ошибку. Но результат не так очевиден.
Есть таблица товаров:
CREATE TABLE products (
id INT PRIMARY KEY,
price INT
);
INSERT INTO products VALUES (1, 100);
Применяем скидки:
WITH discounts(id, amount) AS (
VALUES (1, 10), (1, 20)
)
UPDATE products p
SET price = p.price - d.amount
FROM discounts d
WHERE p.id = d.id;
SELECT price FROM products;
Что вернёт последний SELECT?
А)
90 Б)
80 В)
70 Г)
80 или 90, результат не гарантированОдин запрос, две скидки на один товар. Как PostgreSQL обработает такой UPDATE?
Подвох связан с поведением
UPDATE ... FROM, о котором легко забыть даже при работе с реальными базами.👍11❤6🔥1
ORM превратила один SQL-запрос в 51. Разработчик даже не заметил
Знакомая ситуация: код выглядит чисто, никаких SQL-запросов вручную, всё через ORM.
Например, в Django:
Кажется, что мы просто получаем 50 постов с авторами. Но на практике это может вызвать 51 запрос к базе данных.
Один запрос загружает посты, а ещё 50 отдельно получают авторов.
Это классическая проблема N+1 queries. Причём чем больше записей, тем больше обращений к БД.
Исправляется одной строкой:
Теперь Django использует
ORM избавляет от написания SQL, но не от необходимости понимать, какой SQL выполняется.
Именно поэтому при оптимизации API полезнее сначала посмотреть на количество запросов, чем переписывать Python-код ради нескольких миллисекунд.
Знакомая ситуация: код выглядит чисто, никаких SQL-запросов вручную, всё через ORM.
Например, в Django:
posts = Post.objects.all()[:50]
for post in posts:
print(post.author.name)
Кажется, что мы просто получаем 50 постов с авторами. Но на практике это может вызвать 51 запрос к базе данных.
Один запрос загружает посты, а ещё 50 отдельно получают авторов.
Это классическая проблема N+1 queries. Причём чем больше записей, тем больше обращений к БД.
Исправляется одной строкой:
posts = Post.objects.select_related("author")[:50]
Теперь Django использует
JOIN и получает посты вместе с авторами одним запросом.ORM избавляет от написания SQL, но не от необходимости понимать, какой SQL выполняется.
Именно поэтому при оптимизации API полезнее сначала посмотреть на количество запросов, чем переписывать Python-код ради нескольких миллисекунд.
👍7❤3🔥2😁1
Если давно хотел разобраться в теме, но постоянно откладывал или терялся среди десятков материалов, этот курс на Stepik поможет выстроить обучение последовательно.
Что получишь:
- Понятные объяснения без лишней воды.
- Практические задания для закрепления знаний.
- Возможность учиться в удобном темпе.
- Структурированный подход вместо хаотичного поиска информации.
Подойдёт тем, кто начинает с нуля или хочет систематизировать знания.
Начни прокачивать навыки уже сейчас:
https://stepik.org/course/303477/
Please open Telegram to view this post
VIEW IN TELEGRAM
👍3❤2👎2🔥2
Даже опытные разработчики иногда ошибаются с оконными функциями PostgreSQL.
Есть таблица событий:
CREATE TABLE events (
id INT PRIMARY KEY,
user_id INT,
ts TIMESTAMP
);
INSERT INTO events VALUES
(1, 10, '2026-01-01 10:00'),
(2, 10, '2026-01-01 10:05'),
(3, 10, '2026-01-01 10:05'),
(4, 10, '2026-01-01 10:20');
Выполняем запрос:
SELECT
id,
COUNT(*) OVER (
PARTITION BY user_id
ORDER BY ts
) AS cnt
FROM events
ORDER BY id;
Что вернёт PostgreSQL?
А)
1, 2, 3, 4Б)
1, 3, 3, 4В)
4, 4, 4, 4Г)
1, 2, 2, 4Правильный ответ:
По умолчанию PostgreSQL использует оконную рамку:
```sql
RANGE BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
```
При
RANGEts10:05cnt = 3Чтобы получить обычный накопительный счётчик, нужно явно указать
ROWS```sql
SELECT
id,
COUNT(*) OVER (
PARTITION BY user_id
ORDER BY ts, id
ROWS BETWEEN UNBOUNDED PRECEDING
AND CURRENT ROW
) AS cnt
FROM events
ORDER BY id;
```
Теперь результат:
1, 2, 3, 4Подвох:
COUNT(*) OVER (ORDER BY ...)Please open Telegram to view this post
VIEW IN TELEGRAM
👍13❤5🔥4
SQL-задача с подвохом: найдите пользователей, которые вернулись после 30 дней молчания
Представьте, что вы анализируете активность пользователей сервиса. Нужно найти тех, кто вернулся после длительного перерыва.
Есть таблица PostgreSQL:
Задача: для каждого пользователя найти все даты возвращения после 30 полных календарных дней без активности.
Ожидаемый результат:
| user_id | return_date |
|---|---|
| 1 | 2026-11-01 |
| 3 | 2026-11-05 |
Условия: несколько событий в один день считаются одной активностью. День последней активности и день возвращения в период молчания не входят. Например, между 1 октября и 31 октября только 29 полных дней без активности, а между 1 октября и 1 ноября уже 30.
Ограничение: решите задачу одним SQL-запросом, без процедур, временных таблиц и циклов.
На первый взгляд достаточно
Как бы вы решили эту задачу на PostgreSQL?
Представьте, что вы анализируете активность пользователей сервиса. Нужно найти тех, кто вернулся после длительного перерыва.
Есть таблица PostgreSQL:
CREATE TABLE events (
user_id INT,
event_time TIMESTAMP
);
INSERT INTO events VALUES
(1, '2026-10-01 10:00'),
(1, '2026-10-01 12:00'),
(1, '2026-11-01 09:00'),
(1, '2026-11-02 14:00'),
(2, '2026-10-01 10:00'),
(2, '2026-10-31 10:00'),
(3, '2026-09-01 10:00'),
(3, '2026-11-05 10:00'),
(3, '2026-11-05 11:00');
Задача: для каждого пользователя найти все даты возвращения после 30 полных календарных дней без активности.
Ожидаемый результат:
| user_id | return_date |
|---|---|
| 1 | 2026-11-01 |
| 3 | 2026-11-05 |
Условия: несколько событий в один день считаются одной активностью. День последней активности и день возвращения в период молчания не входят. Например, между 1 октября и 31 октября только 29 полных дней без активности, а между 1 октября и 1 ноября уже 30.
Ограничение: решите задачу одним SQL-запросом, без процедур, временных таблиц и циклов.
На первый взгляд достаточно
LAG(), но если не учесть дубли событий и границы календарных дней, результат окажется неправильным.Как бы вы решили эту задачу на PostgreSQL?
👍7🔥5❤2🥰1