Как выглядит подзапрос для нахождения последней покупки пользователя
Что проверяют
Знаете ли вы несколько способов достать «последнюю строку в группе» и понимаете ли, чем они отличаются по производительности. Почти всегда задачу усложняют до «всех пользователей сразу» и спрашивают про индекс и вызов из Go.
Как отвечать
Для одного пользователя подзапрос не нужен: WHERE user_id = $1 ORDER BY purchased_at DESC, id DESC LIMIT 1. Второй ключ сортировки по id снимает ничью, когда две покупки пришли в одну секунду. Если просят именно подзапрос, для всех пользователей подходят варианты:
- коррелированный подзапрос:
WHERE p.purchased_at = (SELECT max(purchased_at) FROM purchases WHERE user_id = p.user_id). Работает везде, но при совпадении времени вернёт две строки; - оконная функция:
row_number() OVER (PARTITION BY user_id ORDER BY purchased_at DESC, id DESC)в подзапросе и внешний фильтрrn = 1. Ничья снимается явно; DISTINCT ON (user_id): короткий вариант для PostgreSQL, сортировка должна начинаться сuser_id;LATERAL: для каждого пользователя изusersвыполняет запрос сLIMIT 1. Хорош, когда пользователей немного, а покупок у каждого много.
Все варианты держатся на составном индексе (user_id, purchased_at DESC): база находит нужную строку по индексу, а не сортирует покупки пользователя.
В Go одиночный запрос выполняется через db.QueryRowContext(ctx, query, userID).Scan(...). Отсутствие покупок приходит как sql.ErrNoRows (в pgx это pgx.ErrNoRows), и это нормальный ответ «покупок нет», а не ошибка сервера. Плейсхолдеры зависят от драйвера: $1 для PostgreSQL, ? для MySQL.
Где спотыкаются
Собирают запрос через fmt.Sprintf с подстановкой userID. Это SQL-инъекция, параметры всегда передаются отдельными аргументами.
Забывают про NULL. В PostgreSQL при DESC значения NULL по умолчанию идут первыми, и «последней» окажется покупка без даты, если не написать NULLS LAST или WHERE purchased_at IS NOT NULL.
При выборке по всем пользователям пропускают defer rows.Close(): соединение не возвращается в пул, и под нагрузкой сервис упирается в SetMaxOpenConns. Заодно не проверяют rows.Err() после цикла и теряют ошибку, прервавшую чтение.