← Все курсы · Курс SQL · Все руководства

Месячный рост SQL: LAG, формула роста и ловушки

Этот месяц по сравнению с прошлым месяцем, и три причины, по которым SQL незаметно ошибается · Часть Analyst Prep Kit

К , аналитик данных · Опубликовано

К концу этой страницы вы можете написать запрос на ежемесячный рост и доверять его ответу. Вы узнаете формулу роста и как ее сказать в предложении, что LAG() на самом деле так и есть, а также три ловушки, которые выдают неправильные проценты без ошибок: целочисленное деление, пустой первый месяц и отсутствующий месяц. Это около двадцати минут.

Вот чем на самом деле заняться сегодня. Прежде чем доверять уже написанному запросу роста, запустите одну проверку: подсчитайте отдельные месяцы в ваших данных и сравните их с календарем. Если какой-либо месяц отсутствует, по крайней мере один из ваших показателей роста сравнивает не ту пару месяцев.

Краткая версия: рост в этом месяце минус прошлый месяц, разделенный на последний месяц. LAG() извлекает значение прошлого месяца в строку этого месяца. Но LAG() извлекает предыдущую строку, а не предыдущий месяц, и это одно и то же, если не пропущен ни один месяц.

Это последнее различие очень острое, поэтому оно дает представление о картине.

LAG reads the previous row, not the previous month Two side-by-side panels, each a vertical column of month rows. In the left panel the rows are January, February, March, and then May, because April is missing from the data. Curved arrows run from each row up to the row above it: February points to January, March points to February, and May points to March. The May-to-March arrow is drawn in a warning color with a cross beside it, because May is being compared to March, skipping the missing April. In the right panel the same months appear but a dashed April row has been inserted between March and May. Now the arrow from May points to April, and every arrow connects true calendar neighbors. A check mark sits beside the May-to-April arrow. The picture shows that LAG connects adjacent rows, so growth is only month-over-month when every calendar month has a row. Jan Feb Mar May ✕ Jan Feb Mar Apr · 0 May ✓
LAG соединяет каждую строку со строкой над ней. Слева апрель отсутствует, поэтому май спокойно сравнивает себя с мартом. Справа календарная строка за апрель делает каждую стрелку настоящим соседом.
Что вы узнаете
  1. Формула роста и как ее сказать в предложении
  2. Сначала одна строка в месяц, затем сравнение
  3. ЛАГ в обиходных словах
  4. Две тихие ловушки: целочисленное деление и NULL в первый месяц.
  5. Ловушка пропавшего месяца показана на самом деле
  6. Полное до и после
  7. Крайние случаи: чтение негативов, форматирование, соотношение месячного месяца к годовому отчету
  8. Почему это работает
  9. Использование этого в вашем собственном проекте
  10. Все на одном экране
Проработанный пример реальный. Каждый запрос на этой странице выполнялся в SQLite по таблице заказов из двенадцати строк, показанной ниже, и каждый вывод вставлялся из выполнения. В SQLite есть оконные функции начиная с версии 3.25, поэтому сегодня вы можете воспроизвести все это на своем компьютере. Если сама группировка новая, прочтите GROUP BY и HAVING сначала и возвращайся.

Вот весь набор данных. Двенадцать заказов, пять месяцев, а в апреле заказов вообще нет. Этот пустой апрель нарочно, потому что это звезда ловушки номер три.

идентификатор заказадата_заказаКоличество
12025-01-04120
22025-01-1880
32025-01-2750
42025-02-0390
52025-02-14130
62025-02-2580
72025-03-08200
82025-03-1960
92025-03-30100
102025-05-06110
112025-05-21140
122025-06-11250

1. Формула роста и как ее выразить в предложении

До пояснения: выручка была 250 в январе и 300 в феврале. Прежде чем прочитать формулу, произнесите вслух темпы роста за февраль.

Месячный рост равен этому месяцу минус прошлый месяц, разделенному на последний месяц. В цифрах: 300 минус 250 — 50, а 50 разделить на 250 — 0,2, что составляет 20%. Деление — это часть, которую люди пропускают при разговоре, и именно она делает число сопоставимым. Скачок на 50 — это огромная ошибка для тележки с кофе и ошибка округления для авиакомпании. Деление на последний месяц превращает необработанные изменения в долю от того, с чего вы начали.

Версия предложения имеет значение, потому что вас будут спрашивать об этом на собеседованиях и в коридорах. Практикуйте именно такую ​​формулу: «Доход вырос на 20% по сравнению с предыдущим месяцем, с 250 в январе до 300 в феврале». Сначала оцените, затем два необработанных числа. Использование обоих защитит вас, потому что процент без базы скрывает небольшие числа, а необработанное изменение без базы скрывает масштаб.

Одна словарная заметка. «Месяц за месяцем» всегда означает текущий месяц по сравнению с месяцем, предшествующим ему. Сравнивать июнь с июнем прошлого года — это «год по сравнению с прошлым годом», и они отвечают на разные вопросы. Седьмой раздел возвращается к этому.

2. Сначала одна строка в месяц, затем сравнение

Перед пояснением: таблица заказов состоит из двенадцати строк, охватывающих пять месяцев. Сколько строк должна иметь таблица, в которой вы сравниваете соседей?

Пять. Рост — это сравнение месяцев, поэтому, прежде чем какое-либо сравнение может произойти, данные должны стать одной строкой в ​​месяц. Это зерновой ход. Уровень детализации — это то, что означает одна строка: сейчас одна строка — это один заказ, а вопрос требует, чтобы одна строка соответствовала одному месяцу. GROUP BY делает этот шаг, и GROUP BY и HAVING покрывает его полностью, если коллапс кажется волшебным.

SELECT strftime('%Y-%m', order_date) AS month,
       SUM(amount) AS revenue
FROM orders
GROUP BY strftime('%Y-%m', order_date)
ORDER BY month;

тот strftime('%Y-%m', ...) часть сокращает полную дату, например 2025-01-04, до месяца, 2025-01. В других базах данных это записано по-другому: DATE_TRUNC('month', order_date) в PostgreSQL, FORMAT(order_date, 'yyyy-MM') в SQL Serverе. Везде одна и та же идея. Вот реальный результат.

МесяцВыручка
2025-01250
2025-02300
2025-03360
2025-05250
2025-06250

Проверьте это вручную один раз, потому что доверие к остальной части страницы вытекает из этой таблицы. Январь — это 120 плюс 80 плюс 50, что составляет 250. Март — это 200 плюс 60 плюс 100, что составляет 360. И обратите внимание, чего в таблице нет: апрельской строки. Нулевые заказы означают отсутствие строк для группировки, поэтому апрель здесь не равен нулю. Оно отсутствует. Имейте это в виду.

3. ЛАГ в повседневных словах

Перед объяснением: у вас есть пять ежемесячных строк, и вам нужно, чтобы каждая строка также знала доход предыдущей строки. Куда физически пойдет это число?

В ту же строку, в новом столбце. В этом вся работа LAG(). Говоря повседневными словами: отсортируйте строки, затем для каждой строки доберитесь до строки выше и скопируйте значение вниз. LAG(revenue) OVER (ORDER BY month) говорит: «Отсортируйте по месяцам, а затем передайте в каждую строку доход из предыдущей строки». Это оконная функция, что означает, что она может видеть соседние строки, не сворачивая их. Оконные функции охватывает эту семью должным образом.

WITH monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         SUM(amount) AS revenue
  FROM orders
  GROUP BY strftime('%Y-%m', order_date)
)
SELECT month,
       revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_revenue
FROM monthly
ORDER BY month;
МесяцВыручкаprev_revenue
2025-01250NULL
2025-02300250
2025-03360300
2025-05250360
2025-06250250

Если прошлый месяц находится рядом с этим месяцем, рост представляет собой простую арифметическую операцию в одной строке: доход минус prev_revenue, деленный на prev_revenue. Но сначала посмотрите внимательно на этот результат. Две из пяти строк уже содержат оставшиеся ловушки на этой странице. Значение prev_revenue за январь равно NULL. А prev_revenue Мэй — 360, что является числом марта. У обоих есть свой раздел.

тот ORDER BY month внутри круглых скобок это не украшение. LAG понятия не имеет, что такое месяц. Он знает только «строку перед этой в том порядке, который мне дали». Выполните сортировку по неправильному столбцу, и LAG с радостью выдаст вам доход по какой-то несвязанной строке.

4. Две тихие ловушки: целочисленное деление и нулевой первый месяц.

Перед пояснением: 50 разделить на 250 равно 0,2. Что ответит база данных, если оба числа храниться как целые числа?

Ноль. В большинстве баз данных при делении одного целого числа на другое остаток выбрасывается, поскольку столбец целых чисел обещает целые числа. Так (revenue - prev_revenue) / prev_revenue равно 50/250, то есть целочисленное деление округляет до 0. Любой темп роста между минус 99% и плюс 99% становится нулевым, запрос выполняется без нареканий, и в отчете говорится, что рост остается неизменным. Я сравнивал именно это с этой таблицей и каждый месяц получал 0.

Исправление состоит в том, чтобы принудительно выполнить десятичную математику до того, как произойдет деление. Умножение на 100.0 сначала делает это, потому что одно десятичное число в выражении делает все выражение десятичным:

ROUND(100.0 * (revenue - prev_revenue) / prev_revenue, 1) AS growth_pct

Это возвращает 20,0 для февраля вместо 0. CAST(revenue AS REAL) выполняет ту же работу, если вы предпочитаете, чтобы это было прописано.

Вторая тихая ловушка – январь. Его prev_revenue равно NULL, потому что LAG добрался до первой строки и ничего там не нашел. NULL — это SQL-маркер неизвестного, дыра, куда может попасть значение, и NULL в SQL это целое руководство о том, как ведут себя эти дыры. Здесь поведение на самом деле милосердное: арифметика с NULL дает NULL, поэтому январский рост печатается как NULL, а не как поддельное число. Оставьте это так. НУЛЕВЫЙ первый месяц — это честный ответ, потому что рост до начала ваших данных действительно неизвестен. Единственная ошибка — «фиксировать» его на ноль, что означает фиксированный рост, для которого у вас нет доказательств.

5. Ловушка пропавшего месяца, происходящая на самом деле

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

WITH monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         SUM(amount) AS revenue
  FROM orders
  GROUP BY strftime('%Y-%m', order_date)
)
SELECT month,
       revenue,
       LAG(revenue) OVER (ORDER BY month) AS prev_revenue,
       ROUND(100.0 * (revenue - LAG(revenue) OVER (ORDER BY month))
             / LAG(revenue) OVER (ORDER BY month), 1) AS growth_pct
FROM monthly
ORDER BY month;
МесяцВыручкаprev_revenueрост_процт
2025-01250NULLNULL
2025-0230025020.0
2025-0336030020.0
2025-05250360-30.6
2025-062502500.0

Майский ряд сообщает о падении на 30,6%. Это число указано из месяца в месяц, а не из месяца в месяц. В апреле не было заказов, поэтому в апреле нет строки, поэтому строка над маем — это март. LAG отлично справился со своей задачей: достал предыдущий ряд. Предыдущий ряд был два календарных месяца назад. Ничто не ошибается, ничего не предупреждает, и теперь в отчете говорится о снижении на один месяц, хотя на самом деле это двухмесячный спад с мертвым месяцем посередине.

Прежде чем читать исправление, скажите вслух, почему Мэй сравнила себя с Мартом. Если вы сможете это сформулировать, вы сможете найти это в любом запросе до конца вашей карьеры.

Решение состоит в том, чтобы самостоятельно создать календарь и прикрепить к нему данные, чтобы в каждом месяце была строка, независимо от того, были ли в нем заказы. В SQLite рекурсивный CTE генерирует месяцы. PostgreSQL имеет generate_series для одного и того же задания, а на складе вы обычно присоединяетесь к уже существующей таблице измерений даты.

WITH RECURSIVE calendar(month) AS (
  SELECT '2025-01'
  UNION ALL
  SELECT strftime('%Y-%m', date(month || '-01', '+1 month'))
  FROM calendar
  WHERE month < '2025-06'
),
monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         SUM(amount) AS revenue
  FROM orders
  GROUP BY strftime('%Y-%m', order_date)
)
SELECT c.month,
       COALESCE(m.revenue, 0) AS revenue,
       LAG(COALESCE(m.revenue, 0)) OVER (ORDER BY c.month) AS prev_revenue,
       ROUND(
         100.0 * (COALESCE(m.revenue, 0)
                  - LAG(COALESCE(m.revenue, 0)) OVER (ORDER BY c.month))
         / NULLIF(LAG(COALESCE(m.revenue, 0)) OVER (ORDER BY c.month), 0),
       1) AS growth_pct
FROM calendar c
LEFT JOIN monthly m ON m.month = c.month
ORDER BY c.month;

Три небольшие детали несут вес. LEFT JOIN сохраняется каждый календарный месяц, даже если нет соответствующих заказов. COALESCE(m.revenue, 0) превращает пустой апрель в настоящий ноль, и это правильно, потому что нулевые заказы действительно означали нулевой доход. И NULLIF(..., 0) защищает деление: рост с нулевого месяца будет делиться на ноль, поэтому вместо этого возвращается NULL. Вот реальный результат.

МесяцВыручкаprev_revenueрост_процт
2025-01250NULLNULL
2025-0230025020.0
2025-0336030020.0
2025-040360-100.0
2025-052500NULL
2025-062502500.0

Теперь история правдива. Апрель упал на 100% и ни к чему. Мэйский рост равен NULL, поскольку процентное увеличение от нуля не определено, а NULL — это честный способ вывести неопределенное значение. Фальшивое месячное падение на 30,6% исчезло, его заменило то, что произошло на самом деле: крах и перезапуск.

6. Полное до и после

Та же таблица, тот же вопрос: как растет выручка из месяца в месяц?

До

WITH monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         SUM(amount) AS revenue
  FROM orders
  GROUP BY strftime('%Y-%m', order_date)
)
SELECT month, revenue,
       (revenue - LAG(revenue) OVER (ORDER BY month))
         / LAG(revenue) OVER (ORDER BY month) AS growth
FROM monthly;

Три дефекта, ноль сообщений об ошибках. При целочисленном делении каждый темп роста выводится как 0. Отсутствие апреля сравнивает май с мартом. И ничто на экране не сообщает читателю, что что-то произошло.

после

-- ============================================================
-- Month-over-month revenue growth.
-- WHY the calendar CTE: LAG compares adjacent ROWS. April has
--      no orders, so without a calendar row May would compare
--      against March and report a fake one-month drop.
-- WHY 100.0: integer division would round every rate to 0.
-- WHY NULLIF: growth from a zero month is undefined, and NULL
--      is the honest way to report undefined.
-- ============================================================
WITH RECURSIVE calendar(month) AS (
  SELECT '2025-01'
  UNION ALL
  SELECT strftime('%Y-%m', date(month || '-01', '+1 month'))
  FROM calendar
  WHERE month < '2025-06'
),
monthly AS (
  SELECT strftime('%Y-%m', order_date) AS month,
         SUM(amount) AS revenue
  FROM orders
  GROUP BY strftime('%Y-%m', order_date)
)
SELECT c.month,
       COALESCE(m.revenue, 0) AS revenue,
       ROUND(
         100.0 * (COALESCE(m.revenue, 0)
                  - LAG(COALESCE(m.revenue, 0)) OVER (ORDER BY c.month))
         / NULLIF(LAG(COALESCE(m.revenue, 0)) OVER (ORDER BY c.month), 0),
       1) AS growth_pct
FROM calendar c
LEFT JOIN monthly m ON m.month = c.month
ORDER BY c.month;

Присутствует каждый охранник, и каждый излагает свою причину в комментарии, в стиле Как комментировать SQL, чтобы он обучал. Рецензент теперь может не согласиться с выбором вместо того, чтобы обнаружить его.

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

7. Крайние случаи: чтение негативов, форматирование, соотношение месячного и годового показателей.

Перед пояснением: в один месяц выручка упала на 30%, а в следующем выросла на 30%. Вернулся ли он туда, где все началось?

Нет, и это ловушка отрицательного роста показателей. Уменьшение на 30% от 100 — это 70. Увеличение на 30% от 70 — это 91. Процентные изменения складываются независимо от того, где они происходят, поэтому для того, чтобы отменить падение, требуется больший подъем. Когда вы представляете падение, назовите и уровень: «на 30,6%, с 360 до 250». Пара уровней удерживает читателей от мысленного изменения процентного значения.

Форматируем в последний момент. Сохраняйте рост в виде простого числа, например -30,6, на протяжении всего SQL и позволяйте уровню отчета добавлять знак процента. Строки типа «-30,6%» невозможно отсортировать, усреднить или составить диаграмму, и кто-то всегда пытается это сделать. ROUND(x, 1) достаточна точность для деловой аудитории; 20,03847% не сообщают ничего такого, чего не сообщает 20,0%.

Крошечная база дает глупый процент. Рост от 2 до 10 составляет 400%. Правда и бесполезно. Когда последний месяц близок к нулю, сообщите о необработанном изменении вместе со ставкой или подавите ставку ниже записанного вами минимального уровня.

Месяц месяца шумен там, где календарь сезонный. Продажи мороженого падают каждый сентябрь, и никто не должен получать тревожные электронные письма по этому поводу. Месяц за месяцем задается вопрос «что только что изменилось», что усиливает сезонность и разовые всплески. Из года в год июнь сравнивается с июнем прошлого года, поэтому сезон сводится на нет, и тенденция проявляется. Используйте Мама, чтобы быстро обнаружить повороты, г/г, чтобы спокойно констатировать тенденцию, и обе линии на одном графике, когда аудитория может это принять. Комплект прогнозирования корректно работает с сезонностью, в том числе когда «падение» — это просто дыхание календаря.

Почему это работает

Движение зерна во втором разделе не является выбором стиля. В реляционной модели результатом запроса является отношение, и каждая строка отношения содержит один факт в одном зерне (Кодд, 1970, Коммуникации ACM, 13(6), 377–387). «Рост» — это факт о паре месяцев, поэтому запрос должен сначала создать строки, зернистостью которых является месяц, а затем связать соседей. Весь контракт LAG, предыдущая строка в указанном порядке, имеет смысл только потому, что строки под ним означают один месяц каждая. Каждая ловушка на этой странице — это место, где порядок строк или зернистость строк молча перестали соответствовать календарю.

Эта страница также продолжала просить вас ответить, прежде чем она вам сказала. Это намеренно. Попытка ответить до того, как вы увидите решение, заметно улучшает память на материал, даже если попытка неверна (Бисра, Лю, Несбит, Салими и Винн, 2018, Обзор педагогической психологии, 30(3), 703–725). А практика извлечения информации лучше перечитывания для сохранения в течение любого интервала, важного для работы (Краснее & выбирать карпа, 2006, Психологическая наука, 17(3), 249–255). Шпаргалка внизу предназначена для поиска, а не для перечитывания.

Использование этого в вашем собственном проекте

Модернизация каждого запроса роста, который вы когда-либо отправляли, — это ужасно, и вы не закончите его. Вместо этого сделайте это по порядку.

  1. Запустите проверку количества месяцев в первую очередь в самом читаемом отчете. SELECT COUNT(DISTINCT strftime('%Y-%m', order_date)) FROM orders, а затем сравните с количеством календарных месяцев, которые должны охватывать ваши данные. Несоответствие означает, что по крайней мере одно сравнение LAG в данный момент неверно.
  2. добавлять prev_revenue как видимый столбец в этом отчете речь идет не только о процентах. Читатель, который может видеть 250 и 360 рядом друг с другом, уловит неправильную пару, которую скрывает одинокий -30,6.
  3. Помещать 100.0 или CAST в каждом подразделении, даже если сегодняшние типы столбцов являются десятичными. Типы столбцов меняются; охранник свободен.
  4. Создайте или одолжите один календарный стол и с этого момента присоединяйте к нему каждый временной ряд. Один CTE, написанный один раз, вставленный навсегда.
  5. Напишите причины охранников в комментариях. в том же проходе, пока еще помнишь почему.

Если у вас под рукой есть бумага, один рисунок стоит пяти минут и это необязательно. Нарисуйте свои собственные последние шесть месяцев в виде ячеек в столбце, стрелками от каждого поля к указанному выше, затем вычеркните любой месяц, который, как вы подозреваете, может быть пустым в данных, и посмотрите, какая стрелка только что стала ложью. Перерисовка фигуры по памяти – это повторение поиска и самопроверка в одном лице.

Подробнее об этом и еще что-нибудь подобное. Все инструкции находятся в одном месте на индекс направляющих: SQL, Excel, Tableau и связанные с ними рабочие привычки.

Все на одном экране

Это лист поиска. Закройте правую колонку, проработайте левую и произнесите каждый ответ вслух, прежде чем проверять его.

концепцияЧто это такое
Формула роста за месяцЭтот месяц минус прошлый месяц, разделенный на последний месяц.
Версия предложения«Месячный рост вырос на 20%, с 250 до 300». Оцените, затем оба уровня.
Шаг первый, всегдаGROUP BY до одной строки в месяц. Для роста сначала нужно месячное уровень детализации.
LAG(x) OVER (ORDER BY m)Отсортируйте строки, затем скопируйте значение предыдущей строки в эту строку.
Чего не знает ЛАГВсе о календарях. Он извлекает предыдущую строку.
Ловушка целочисленного деления50/250 равно 0, если оба являются целыми числами. Сначала умножьте на 100,0.
Рост за первый месяцNULL и оставьте его NULL. Рост по сравнению с предыдущими данными неизвестен.
Ловушка пропавшего месяцаГэп делает пару LAG несмежными месяцами. Май против марта, молча.
ИсправлениеСоздайте календарь, LEFT JOIN данных к нему, COALESCE до 0.
Инструменты календаряРекурсивный CTE в SQLite,генерировать_серию в PostgreSQL, измерение даты в хранилище.
Рост с нуляНеопределенный. NULLIF(предыдущий, 0) выводит значение NULL, а не ошибку.
Читая осеньСнижение на 30%, а затем повышение на 30% не является фиксированным. Всегда указывайте уровни со ставкой.
ФорматированиеСохраняйте числовое значение в SQL, ОКРУГЛИТЕ до одного десятичного знака, добавьте % на уровне отчета.
М/м против г/гМама быстро обнаруживает повороты, но усиливает сезонность. Год назад отменяет сезон.
Единственная привычка, которую следует сохранить. Если вы больше ничего не извлекаете из этой страницы, никогда не доверяйте LAG в отношении дат, пока не убедитесь, что в каждом календарном периоде есть строка. Неправильный показатель роста никогда не выглядит неправильным. Это выглядит как результат со знаком минус и одним десятичным знаком. Если запрос на рост дает сбой, который не описан на этой странице, существует общая шлейф диагностики на предмет застревания.

Еще одна мысль, и мне бы искренне хотелось услышать ответы других людей. Мои фальшивые -30,6% было легко уловить, потому что я специально создал разрыв. В реальных данных разрыв обычно представляет собой сбой загрузки, который никто не заметил, а это означает, что показатель роста был неверным в течение нескольких месяцев, прежде чем кто-либо его проверил. Как долго в отчете, который вы унаследовали, сохранялись неверные цифры тренда, и что в конечном итоге выявило их?

Ссылки

Число месяцев по месяцам верное, а месяц, с которым оно сравнивается, был случайностью.

SQL для аналитиков составляет 458 страниц, записаны сравнения периодов, включая то, как определить, как малый базовый месяц влияет на проценты.

SQL для аналитиков, 19 $ →
Попрактикуйтесь в ЛАГе, держа перед собой стол.

тот Курс SQL запускает SQLite в браузере, так что вы можете построить ежемесячную сводку, посмотреть, как LAG смещает столбец вниз, и намеренно разбить его с интервалом в месяц. Рядом с ним лежат рабочие примеры, карточки и пробный экзамен. Ничего устанавливать, учетная запись не требуется.

Откройте набор SQL →