Самая длинная серия для пользователя
Вычисление максимальной длины последовательного ряда внутри каждой группы
«Самая длинная серия для пользователя» — бесплатный урок Coding Interview Prep на CoddyKit. Это урок 2 из 4. Ты можешь прочитать весь урок бесплатно ниже — а потом практиковать его прямо в браузере с встроенным редактором кода и ИИ-репетитором 24/7. Это часть пути обучения Coding Interview Prep, и твой прогресс синхронизируется между веб-версией и приложением CoddyKit. Курс Coding Interview Prep содержит 4 уроков всего.
Вопрос
Частый дополнительный вопрос после поиска последовательных дней: «Какова для каждого пользователя его самая длинная серия последовательных дней активности?» Команды продукта и развития постоянно задают этот вопрос, чтобы измерять вовлечённость.
Вы уже знаете, как находить каждую серию. Новый шаг — найти максимальную длину серии для каждого пользователя и часто также вернуть даты этой лучшей серии. Этот урок напрямую продолжает схему разрывов и островов.
Вспоминаем построение островов
В предыдущем уроке для группировки каждой серии использовалось login_date - ROW_NUMBER() как опорное значение острова. У каждого пользователя может быть несколько островов; сначала мы вычислим по одной строке на остров, а затем сведём результат к одной строке на пользователя.
Держите в уме этот план из двух уровней: сначала построить острова, затем агрегировать острова.
WITH numbered AS (
SELECT user_id, login_date,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY login_date
) AS rn
FROM logins
)
SELECT user_id, login_date - rn AS grp
FROM numbered;Одна строка на остров
Сверните каждый остров в одну сводную строку, содержащую его длину и диапазон дат. Сгруппируйте данные по пользователю и опорному значению, затем вычислите показатели.
Назовём этот CTE islands, чтобы следующий уровень мог удобно читать данные из него.
WITH numbered AS (
SELECT user_id, login_date,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY login_date
) AS rn
FROM logins
),
islands AS (
SELECT user_id,
MIN(login_date) AS streak_start,
MAX(login_date) AS streak_end,
COUNT(*) AS streak_len
FROM numbered
GROUP BY user_id, login_date - rn
)
SELECT * FROM islands;Простой ответ: длина MAX
Если интервьюеру нужна только длина, последний шаг выполняется одной строкой: сгруппируйте острова по пользователю и найдите MAX длины.
Это самый простой ответ, когда даты начала и конца не требуются.
-- ...numbered and islands CTEs as before...
SELECT
user_id,
MAX(streak_len) AS longest_streak
FROM islands
GROUP BY user_id
ORDER BY user_id;Возврат дат
Часто интервьюер добавляет: «и покажите, когда произошла эта серия». Обычная MAX не сообщает, какой остров оказался лучшим. Нужно ранжировать острова внутри каждого пользователя и оставить ранг 1.
Используйте ROW_NUMBER с сортировкой по убыванию длины, чтобы лучшая серия каждого пользователя получила ранг 1. Добавьте дополнительный критерий сортировки, чтобы одинаковые значения разрешались детерминированно.
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY streak_len DESC, streak_start ASC
) AS rnkРанжирование и фильтрация
Поместите ранжирование в CTE, затем отфильтруйте строки по условию rnk = 1. Нельзя напрямую фильтровать оконную функцию в WHERE, поэтому дополнительный уровень обязателен.
WITH numbered AS (
SELECT user_id, login_date,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY login_date
) AS rn
FROM logins
),
islands AS (
SELECT user_id,
MIN(login_date) AS streak_start,
MAX(login_date) AS streak_end,
COUNT(*) AS streak_len
FROM numbered
GROUP BY user_id, login_date - rn
),
ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY streak_len DESC, streak_start
) AS rnk
FROM islands
)
SELECT user_id, streak_start, streak_end, streak_len
FROM ranked
WHERE rnk = 1;RANK и ROW_NUMBER при совпадениях
Что делать, если у пользователя есть две серии одинаковой максимальной длины и интервьюер хочет вернуть обе? Замените ROW_NUMBER на RANK и оставьте rnk = 1.
ROW_NUMBER— ровно один победитель для каждого пользователя (при совпадениях выбор произволен, если не добавить дополнительный критерий).RANK— все самые длинные серии с одинаковым результатом получают ранг 1 и сохраняются.
Уточните, какое поведение требуется: это показывает внимание к особым случаям.
RANK() OVER (
PARTITION BY user_id
ORDER BY streak_len DESC
) AS rnk -- keep all rnk = 1Разбор примера
Предположим, пользователь 7 входил в систему с 1 по 4 января, затем 10 и 11 января, а затем с 20 по 23 января. Получаются три острова длиной 4, 2 и 4. Максимальная длина равна 4, и здесь есть совпадение.
- С
ROW_NUMBERи дополнительным критериемstreak_start: возвращается только серия с 1 по 4 января. - С
RANK: возвращаются обе серии — с 1 по 4 января и с 20 по 23 января.
Если проговорить это вслух, Вы покажете, что учли дубликаты результатов.
Работа с пользователями без входов в систему
Интервьюер может спросить: "А что насчёт пользователей, которые никогда не входили в систему?" Для таких пользователей нет строк в logins, поэтому они исчезают из результата. Если они должны отображаться с серией длиной 0, используйте LEFT JOIN для полной таблицы users и COALESCE.
SELECT u.user_id,
COALESCE(MAX(i.streak_len), 0) AS longest_streak
FROM users u
LEFT JOIN islands i ON i.user_id = u.user_id
GROUP BY u.user_id;Замечания о производительности
Этот шаблон выполняет один упорядоченный проход по данным, а затем группирует их. Чтобы запрос оставался быстрым:
- Убедитесь, что существует индекс по
(user_id, login_date), чтобы оконная сортировка ORDER BY не требовала отдельной сортировки. - Удаляйте дубликаты на раннем этапе, если источник содержит несколько событий за день.
- Не оборачивайте
login_dateв функции внутри ORDER BY: это может заблокировать использование индекса.
Для очень больших таблиц этот подход существенно производительнее любого варианта с самосоединением.
Полный ответ на собеседовании
Ниже приведён полный, отшлифованный запрос, который возвращает самую длинную серию каждого пользователя вместе с её датами — именно такую версию стоит написать на доске.
WITH numbered AS (
SELECT user_id, login_date,
ROW_NUMBER() OVER (
PARTITION BY user_id ORDER BY login_date
) AS rn
FROM logins
),
islands AS (
SELECT user_id,
MIN(login_date) AS streak_start,
MAX(login_date) AS streak_end,
COUNT(*) AS streak_len
FROM numbered
GROUP BY user_id, login_date - rn
),
ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY user_id
ORDER BY streak_len DESC, streak_start
) AS rnk
FROM islands
)
SELECT user_id, streak_start, streak_end, streak_len
FROM ranked
WHERE rnk = 1
ORDER BY user_id;Быстрая проверка
Выберите подходящий инструмент для требования.
Итоги
Чтобы вычислить самую длинную серию для каждого пользователя:
- Постройте острова с помощью якоря
login_date - ROW_NUMBER(). - Сверните каждый остров до длины и диапазона дат.
- Если нужна только длина, используйте
MAX(streak_len), сгруппировав данные по пользователю. - Если нужны также даты, присвойте островам ранг для каждого пользователя и оставьте ранг 1 — используйте
RANK, чтобы включить равные результаты, иROW_NUMBER, чтобы выбрать одного победителя. - Используйте LEFT JOIN с таблицей users, чтобы показать пользователей с нулевой серией.
Далее: поиск N последовательных строк, удовлетворяющих условию.
Часто задаваемые вопросы
Урок «Самая длинная серия для пользователя» бесплатный?
Да — полный текст урока «Самая длинная серия для пользователя» бесплатно доступен здесь в веб-версии. Чтобы практиковать его интерактивно (встроенный редактор кода и ИИ-репетитор 24/7) и разблокировать остальной курс Coding Interview Prep, подпишись на CoddyKit PRO. Курс Coding Interview Prep содержит 4 уроков всего.
Чему я научусь в уроке «Самая длинная серия для пользователя»?
Вычисление максимальной длины последовательного ряда внутри каждой группы Ты практикуешь Coding Interview Prep с помощью реального кода, который запускаешь прямо в браузере, и ИИ-репетитор 24/7 отвечает на твои вопросы во время урока.
Нужен ли мне опыт, чтобы начать Coding Interview Prep?
Предыдущий опыт не требуется. Coding Interview Prep на CoddyKit структурирован для всех уровней — от новичков до продвинутых, поэтому ты можешь начать отсюда или с самого начала и учиться в своем темпе. Это урок 2 из 4.
Сколько времени занимает урок «Самая длинная серия для пользователя»?
Большинство уроков CoddyKit занимают около 5–10 минут. Каждый из них компактный и интерактивный, поэтому ты постоянно делаешь прогресс и продолжаешь с того же места в веб-версии и приложении.
Можно ли писать и запускать код в этом уроке Coding Interview Prep?
Да. Каждый урок Coding Interview Prep включает встроенный редактор кода, поэтому ты пишешь и запускаешь реальный код прямо в браузере и получаешь моментальную обратную связь от AI — локальная установка не требуется.
Все уроки этого курса
- Поиск последовательных календарных дней
- Самая длинная серия для пользователя
- N последовательных строк, соответствующих условию
- Текущая активная серия на сегодня