0Pricing
Coding Interview Prep · Урок

Самая длинная серия для пользователя

Вычисление максимальной длины последовательного ряда внутри каждой группы

«Самая длинная серия для пользователя» — бесплатный урок 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 — локальная установка не требуется.

Все уроки этого курса

  1. Поиск последовательных календарных дней
  2. Самая длинная серия для пользователя
  3. N последовательных строк, соответствующих условию
  4. Текущая активная серия на сегодня
← Назад к Coding Interview Prep