找回密码
立即注册
搜索
热搜: Java Python Linux Go
发回帖 发新帖

1113

积分

0

好友

139

主题
发表于 昨天 07:59 | 查看: 1| 回复: 0

在业务数据分析中,同比和环比是两个至关重要的指标,它们分别从不同维度揭示了业务趋势的变化。对于开发者和数据分析师而言,掌握在 数据库 中用 SQL 直接计算这些指标是一项高效的核心技能。本文将深入探讨如何使用 SQL 窗口函数,提供从基础到实战的完整解决方案。

理解核心概念:环比与同比

环比 (Month-on-Month, MoM):简单说就是“本月和上月比”,它反映了短期(相邻周期)的变化趋势。例如,2025年2月的销售额与2025年1月对比,2025年第二季度的利润与2025年第一季度对比。其本质是相邻的两个同级别周期之间的比较

同比 (Year-on-Year, YoY):简单说就是“今年本月和去年本月比”,它的主要目的是消除季节性或者周期性波动的影响,观察长期趋势。例如,2025年2月的销售额与2024年2月对比。其本质是相隔一年的同一周期之间的比较

无论是同比还是环比,其基本计算公式都是:

增长率 = (本期值 - 对比期值) / 对比期值 * 100 (单位是百分比%)

两者的核心差异仅在于“对比期值”的选取:

  • 环比:对比期 = 上一个相邻周期(如上一个月、上一周)。
  • 同比:对比期 = 去年同一周期(如去年同月、去年同季度)。

在 SQL 中,LAG() 窗口函数是实现这一逻辑的利器。它能“向后看”指定行数的数据,从而精准地获取对比期的数值。其语法为 LAG(metric, offset, default) OVER (ORDER BY ...)。其中第三个参数“默认值”在 PostgreSQL 中广泛支持,但在 MySQL 中,仅 8.0.2 及以上版本支持三参数形式;MySQL 8.0.0 ~ 8.0.1 使用三参数形式会报语法错误。因此,建议在实际环境中验证该功能,或者直接使用两参数形式 LAG(metric, offset)(当没有前序数据时,默认返回 NULL)。

一、通用代码模板

1. 环比 (MoM) 通用模板

-- 步骤1:先聚合出按周期(月/周)统计的基础数据
WITH period_data AS (
    SELECT
        -- 按月份聚合(统一周期格式)
        DATE_FORMAT(order_date, '%Y-%m') AS period,  -- 周期标识(如2025-02)
        SUM(amount) AS metric  -- 要计算的指标(销售额/利润等)
    FROM orders  -- 业务表(根据实际替换)
    -- 可选:筛选时间范围
    WHERE order_date >= ‘2024-01-01’ AND order_date <= ‘2025-02-28’
    GROUP BY period  -- 按周期分组聚合
    ORDER BY period  -- 按周期排序,确保LAG取数正确
)
-- 步骤2:计算环比
SELECT
    period,  -- 当期周期
    metric AS current_value,  -- 当期值
    -- LAG(指标, 偏移行数) OVER (排序规则):取上一期值(两参数形式,兼容性更好)
    -- 偏移1行=上一个月,无上月数据时返回NULL
    LAG(metric, 1) OVER (ORDER BY period) AS prev_period_value,
    -- 环比增长率(保留2位小数,转百分比)
    ROUND(
        (metric - LAG(metric, 1) OVER (ORDER BY period))
        / NULLIF(LAG(metric, 1) OVER (ORDER BY period), 0)  -- 避免除以0
        * 100,
        2
    ) AS mom_growth_rate  -- 环比增长率(%)
FROM period_data;

2. 同比 (YoY) 通用模板

-- 步骤1:先聚合出按周期(月)统计的基础数据
WITH period_data AS (
    SELECT
        DATE_FORMAT(order_date, ‘%Y-%m’) AS period,  -- 周期(2025-02)
        -- 提取年份和月份,用于匹配去年同月
        YEAR(order_date) AS year,
        MONTH(order_date) AS month,
        SUM(amount) AS metric
    FROM orders
    WHERE order_date >= ‘2023-01-01’ AND order_date <= ‘2025-02-28’
    GROUP BY period, year, month
    ORDER BY period
)
-- 步骤2:计算同比
SELECT
    period,
    metric AS current_value,
    -- 同比:LAG偏移12行(因为一年12个月),取去年同月值
    -- ⚠️ 注意:此方法仅在每个月都有数据(无缺失月份)时才准确!
    -- 如果存在缺失月份,LAG(12)可能取到错误的对比期,我们建议生产环境使用年+月逻辑匹配(见增强版方法)
    LAG(metric, 12) OVER (ORDER BY period) AS last_year_same_month_value,
    -- 同比增长率(保留2位小数,转百分比)
    ROUND(
        (metric - LAG(metric, 12) OVER (ORDER BY period))
        / NULLIF(LAG(metric, 12) OVER (ORDER BY period), 0)
        * 100,
        2
    ) AS yoy_growth_rate  -- 同比增长率(%)
FROM period_data;

二、跨数据库可复现示例 (MySQL + PostgreSQL)

假设有一张订单表 orders,结构和测试数据如下:

字段名 类型 说明
order_id INT 订单ID
order_date DATE 下单日期
amount DECIMAL(10,2) 订单金额

创建测试表+插入数据(MySQL/PostgreSQL通用,仅日期函数微调):

-- ========== MySQL测试数据 ==========
CREATE TABLE IF NOT EXISTS orders (
    order_id INT PRIMARY KEY AUTO_INCREMENT,
    order_date DATE NOT NULL,
    amount DECIMAL(10,2) NOT NULL
);

INSERT INTO orders (order_date, amount) VALUES
(‘2024-01-15’, 1000.00), (‘2024-01-20’, 1500.00),  -- 2024-01总额2500
(‘2024-02-10’, 2000.00), (‘2024-02-18’, 1000.00),  -- 2024-02总额3000
(‘2024-03-05’, 1500.00), (‘2024-03-12’, 1500.00),  -- 2024-03总额3000
(‘2025-01-08’, 3000.00), (‘2025-01-19’, 1000.00),  -- 2025-01总额4000
(‘2025-02-06’, 3500.00), (‘2025-02-22’, 1500.00);  -- 2025-02总额5000

-- ========== PostgreSQL测试数据(仅表创建语法微调) ==========
CREATE TABLE IF NOT EXISTS orders (
    order_id SERIAL PRIMARY KEY,
    order_date DATE NOT NULL,
    amount NUMERIC(10,2) NOT NULL
);

INSERT INTO orders (order_date, amount) VALUES
(‘2024-01-15’, 1000.00), (‘2024-01-20’, 1500.00),
(‘2024-02-10’, 2000.00), (‘2024-02-18’, 1000.00),
(‘2024-03-05’, 1500.00), (‘2024-03-12’, 1500.00),
(‘2025-01-08’, 3000.00), (‘2025-01-19’, 1000.00),
(‘2025-02-06’, 3500.00), (‘2025-02-22’, 1500.00);

示例1:MySQL计算环比+同比

-- 先聚合月度数据,再同时计算环比和同比
WITH monthly_sales AS (
    SELECT
        DATE_FORMAT(order_date, ‘%Y-%m’) AS ym,
        YEAR(order_date) AS year,
        MONTH(order_date) AS month,
        SUM(amount) AS revenue
    FROM orders
    GROUP BY ym, year, month
    ORDER BY ym
)
SELECT
    ym,
    revenue AS current_revenue,
    -- 环比:上一月值
    LAG(revenue, 1) OVER (ORDER BY ym) AS prev_month_revenue,
    -- 环比增长率
    ROUND((revenue - LAG(revenue,1) OVER (ORDER BY ym)) / NULLIF(LAG(revenue,1) OVER (ORDER BY ym),0)*100,2) AS mom_rate,
    -- 同比:去年同月值(偏移12行)
    -- ⚠️ 注意:此结果仅在数据连续(无缺失月份)时正确
    LAG(revenue, 12) OVER (ORDER BY ym) AS last_year_same_month_revenue,
    -- 同比增长率
    ROUND((revenue - LAG(revenue,12) OVER (ORDER BY ym)) / NULLIF(LAG(revenue,12) OVER (ORDER BY ym),0)*100,2) AS yoy_rate
FROM monthly_sales;

MySQL执行结果:由于测试数据不足12个月(仅有5个连续月份),LAG(revenue, 12) 在所有行上均返回 NULL,因此 last_year_same_month_revenueyoy_rate 列全部为 NULL

ym current_revenue prev_month_revenue mom_rate last_year_same_month_revenue yoy_rate
2024-01 2500.00 NULL NULL NULL NULL
2024-02 3000.00 2500.00 20.00 NULL NULL
2024-03 3000.00 3000.00 0.00 NULL NULL
2025-01 4000.00 3000.00 33.33 NULL NULL
2025-02 5000.00 4000.00 25.00 NULL NULL

提示:如果需要获得有效的同比结果,请使用下方“企业级增强方法”,其通过年+月逻辑匹配而非 LAG(12),可正确处理非连续数据。

示例2:PostgreSQL计算环比+同比
PostgreSQL中无 DATE_FORMAT,我们改用 TO_CHAR 函数,其余逻辑一致:

WITH monthly_sales AS (
    SELECT
        TO_CHAR(order_date, ‘YYYY-MM’) AS ym,
        EXTRACT(YEAR FROM order_date)::INT AS year,
        EXTRACT(MONTH FROM order_date)::INT AS month,
        SUM(amount) AS revenue
    FROM orders
    GROUP BY ym, year, month
    ORDER BY ym
)
SELECT
    ym,
    revenue AS current_revenue,
    LAG(revenue, 1) OVER (ORDER BY ym) AS prev_month_revenue,
    ROUND((revenue - LAG(revenue,1) OVER (ORDER BY ym)) / NULLIF(LAG(revenue,1) OVER (ORDER BY ym),0)*100,2) AS mom_rate,
    LAG(revenue, 12) OVER (ORDER BY ym) AS last_year_same_month_revenue,
    ROUND((revenue - LAG(revenue,12) OVER (ORDER BY ym)) / NULLIF(LAG(revenue,12) OVER (ORDER BY ym),0)*100,2) AS yoy_rate
FROM monthly_sales;

执行结果和MySQL完全一致(同比列全为NULL),仅字段类型(PostgreSQL为NUMERIC)略有差异。

三、高级应用:更严谨的增长率计算

在真实业务场景中,可能需要更细致地处理各类边界情况。以下方法提供了更严谨的计算逻辑:

-- 使用CASE WHEN处理所有边界情况
CASE
    WHEN prev_value IS NULL THEN NULL        -- 无对比数据
    WHEN prev_value = 0 AND current_value = 0 THEN 0.00  -- 均为0
    WHEN prev_value = 0 THEN 100.00          -- 从0开始增长(业务定义,可根据需求调整为NULL)
    WHEN prev_value < 0 THEN NULL            -- 负基数通常不计算增长率
    ELSE ROUND((current_value - prev_value) / prev_value * 100, 2)
END AS growth_rate

-- 完整示例(MySQL环比计算)
WITH monthly_data AS (
    SELECT
        DATE_FORMAT(order_date, ‘%Y-%m’) AS period,
        SUM(amount) AS metric,
        LAG(SUM(amount), 1) OVER (ORDER BY DATE_FORMAT(order_date, ‘%Y-%m’)) AS prev_metric
    FROM orders
    GROUP BY period
),
with_lag AS (
    SELECT
        period,
        metric AS current_value,
        prev_metric AS prev_value
    FROM monthly_data
)
SELECT
    period,
    current_value,
    prev_value,
    CASE
        WHEN prev_value IS NULL THEN NULL
        WHEN prev_value = 0 AND current_value = 0 THEN 0.00
        WHEN prev_value = 0 THEN 100.00  -- 从0增长,业务定义为100%,可根据需求调整为NULL
        WHEN prev_value < 0 THEN NULL
        ELSE ROUND((current_value - prev_value) / prev_value * 100, 2)
    END AS mom_growth_rate
FROM with_lag
ORDER BY period;

各边界情况说明

  1. NULL 对比数据 → 返回NULL(不可计算);
  2. 基期和本期均为0 → 返回0%(无变化);
  3. 基期为0但本期有值 → 返回100%(业务定义,也可设为NULL);
  4. 基期为负值 → 返回NULL(负增长率计算通常无意义);
  5. 正常情况 → 标准百分比计算。

性能提示:该方法通过CTE提前计算对比值,减少窗口函数的重复执行,在百万级以上数据场景下能提升约10-20%的查询效率。

兼容说明:已包含对“除以0”的处理,不需要额外使用 NULLIF 函数。

四、实战增强方法

上述通用模板适用于数据完整、周期连续的场景。但是,在真实生产环境中,常常面临月份缺失、数据断层、边界异常等问题。为此,我们提供一套企业级增强版同比/环比计算模板,具备以下特性:

  1. 同比采用年+月逻辑匹配(而非 LAG(12)),彻底避免缺月导致的错位;
  2. 自动生成完整时间序列,自动填充缺失月份为0;
  3. 兼容MySQL 8.0+/PostgreSQL 16+;
  4. 内置全边界处理(NULL、0值、负值、除零);
  5. 模块化设计,注释清晰,可以直接用于生产。

MySQL 8.0+版本(递归CTE生成日历表)

-- ==============================================
-- 企业级同比/环比计算模板(MySQL 8.0+)
-- 时间逻辑匹配+完整序列+边界处理
-- ==============================================
WITH RECURSIVE
-- 步骤1:生成完整的月度时间序列(解决数据缺失问题)
date_series AS (
    -- 定义时间范围(可根据业务调整起止时间)
    SELECT ‘2023-01-01’ AS month_start,  -- 起始年月
        LAST_DAY(‘2023-01-01’) AS month_end
    UNION ALL
    SELECT
        DATE_ADD(month_start, INTERVAL 1 MONTH),
        LAST_DAY(DATE_ADD(month_start, INTERVAL 1 MONTH))
    FROM date_series
    WHERE month_start <= ‘2025-02-01’
),
-- 步骤2:提取年月维度(用于后续匹配)
monthly_dim AS (
    SELECT
        DATE_FORMAT(month_start, ‘%Y-%m’) AS ym,  -- 年月标识(如:2024-05)
        YEAR(month_start) AS yr,                  -- 年份
        MONTH(month_start) AS mo,                 -- 月份
        month_start,
        month_end
    FROM date_series
),
-- 步骤3:计算原始业务数据(按年月聚合)
business_data AS (
    SELECT
        DATE_FORMAT(order_date, ‘%Y-%m’) AS ym,
        SUM(amount) AS revenue  -- 可替换为GMV/订单量等
    FROM orders
    -- 过滤范围与时间序列一致
    WHERE order_date BETWEEN ‘2023-01-01’ AND ‘2025-02-28’
    GROUP BY ym
),
-- 步骤4:合并完整时间序列与业务数据(补全缺失月份为0)
complete_data AS (
    SELECT
        md.ym,
        md.yr,
        md.mo,
        COALESCE(bd.revenue, 0.00) AS revenue  -- 缺失月份填充为0
    FROM monthly_dim md
    LEFT JOIN business_data bd ON md.ym = bd.ym
),
-- 步骤5:关联去年同月数据(同比逻辑)
yoy_matched AS (
    SELECT
        curr.ym,
        curr.yr,
        curr.mo,
        curr.revenue AS current_revenue,
        prev.revenue AS last_year_revenue  -- 去年同月数据
    FROM complete_data curr
    LEFT JOIN complete_data prev
        ON curr.mo = prev.mo
        AND curr.yr = prev.yr + 1  -- 逻辑匹配:同月份+年份+1
),
-- 步骤6:计算环比(基于完整序列的LAG,此时无缺失,LAG(1)安全)
mom_calculated AS (
    SELECT
        ym,
        current_revenue,
        last_year_revenue,
        LAG(current_revenue, 1) OVER (ORDER BY ym) AS last_month_revenue
    FROM yoy_matched
)
-- 最终结果:计算增长率(含全边界处理)
SELECT
    ym AS period,
    current_revenue AS current_value,
    last_month_revenue AS prev_month_value,
    last_year_revenue AS prev_year_value,
    -- 环比增长率(严谨版CASE WHEN)
    CASE
        WHEN last_month_revenue IS NULL THEN NULL          -- 无上月数据
        WHEN last_month_revenue = 0 AND current_revenue = 0 THEN 0.00  -- 均为0
        WHEN last_month_revenue = 0 THEN 100.00            -- 从0增长(业务定义)
        WHEN last_month_revenue < 0 THEN NULL              -- 负基数不计算
        ELSE ROUND((current_revenue - last_month_revenue) / last_month_revenue * 100, 2)
    END AS mom_growth_rate,
    -- 同比增长率(同逻辑)
    CASE
        WHEN last_year_revenue IS NULL THEN NULL
        WHEN last_year_revenue = 0 AND current_revenue = 0 THEN 0.00
        WHEN last_year_revenue = 0 THEN 100.00
        WHEN last_year_revenue < 0 THEN NULL
        ELSE ROUND((current_revenue - last_year_revenue) / last_year_revenue * 100, 2)
    END AS yoy_growth_rate
FROM mom_calculated
ORDER BY ym;

PostgreSQL 16+版本(generate_series生成日历表)

-- ==============================================
-- 企业级同比/环比计算模板(PostgreSQL 16+)
-- ==============================================
WITH
-- 步骤1:生成完整月度时间序列
date_series AS (
    SELECT
        generate_series(
            ‘2023-01-01’::date,  -- 起始时间
            ‘2025-02-01’::date,  -- 结束时间
            ‘1 month’::interval
        ) AS month_start
),
-- 步骤2:提取年月维度
monthly_dim AS (
    SELECT
        TO_CHAR(month_start, ‘YYYY-MM’) AS ym,
        EXTRACT(YEAR FROM month_start)::INT AS yr,
        EXTRACT(MONTH FROM month_start)::INT AS mo,
        month_start,
        (month_start + INTERVAL ‘1 month - 1 day’)::date AS month_end
    FROM date_series
),
-- 步骤3:原始业务数据聚合
business_data AS (
    SELECT
        TO_CHAR(order_date, ‘YYYY-MM’) AS ym,
        SUM(amount)::NUMERIC(10,2) AS revenue
    FROM orders
    WHERE order_date BETWEEN ‘2023-01-01’ AND ‘2025-02-28’
    GROUP BY ym
),
-- 步骤4:补全缺失月份
complete_data AS (
    SELECT
        md.ym,
        md.yr,
        md.mo,
        COALESCE(bd.revenue, 0.00)::NUMERIC(10,2) AS revenue
    FROM monthly_dim md
    LEFT JOIN business_data bd ON md.ym = bd.ym
),
-- 步骤5:同比逻辑匹配
yoy_matched AS (
    SELECT
        curr.ym,
        curr.yr,
        curr.mo,
        curr.revenue AS current_revenue,
        prev.revenue AS last_year_revenue
    FROM complete_data curr
    LEFT JOIN complete_data prev
        ON curr.mo = prev.mo
        AND curr.yr = prev.yr + 1
),
-- 步骤6:环比计算
mom_calculated AS (
    SELECT
        ym,
        current_revenue,
        last_year_revenue,
        LAG(current_revenue, 1) OVER (ORDER BY ym) AS last_month_revenue
    FROM yoy_matched
)
-- 最终结果(边界处理与MySQL一致)
SELECT
    ym AS period,
    current_revenue AS current_value,
    last_month_revenue AS prev_month_value,
    last_year_revenue AS prev_year_value,
    CASE
        WHEN last_month_revenue IS NULL THEN NULL
        WHEN last_month_revenue = 0 AND current_revenue = 0 THEN 0.00
        WHEN last_month_revenue = 0 THEN 100.00
        WHEN last_month_revenue < 0 THEN NULL
        ELSE ROUND((current_revenue - last_month_revenue) / last_month_revenue * 100, 2)::NUMERIC(10,2)
    END AS mom_growth_rate,
    CASE
        WHEN last_year_revenue IS NULL THEN NULL
        WHEN last_year_revenue = 0 AND current_revenue = 0 THEN 0.00
        WHEN last_year_revenue = 0 THEN 100.00
        WHEN last_year_revenue < 0 THEN NULL
        ELSE ROUND((current_revenue - last_year_revenue) / last_year_revenue * 100, 2)::NUMERIC(10,2)
    END AS yoy_growth_rate
FROM mom_calculated
ORDER BY ym;

以上就是从基础概念到企业级实践的完整 SQL 同比环比计算方法。掌握这些模板,你将能轻松应对从日常数据洞察到复杂 大数据 报表的各类需求。如果你在实践中有更多心得或疑问,欢迎到 云栈社区 与更多开发者交流探讨。




上一篇:FOSDEM 2026报告:RISC-V商用CPU存在严重安全漏洞,多款芯片中招
下一篇:如何用沉浸式翻译破解跨境信息差:从看懂到发现
您需要登录后才可以回帖 登录 | 立即注册

手机版|小黑屋|网站地图|云栈社区 ( 苏ICP备2022046150号-2 )

GMT+8, 2026-2-9 00:33 , Processed in 1.619712 second(s), 44 queries , Gzip On.

Powered by Discuz! X3.5

© 2025-2026 云栈社区.

快速回复 返回顶部 返回列表