You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

在SQL中实现qcut进行RFM分析:解决ORA-00978分组函数嵌套问题

实现RFM分析的简洁SQL方案(Oracle 12c/PostgreSQL)

问题梳理

你已经用Python完成了RFM分析的模型实现,现在因为生产环境以PHP为主,需要在Oracle 12c或PostgreSQL中用SQL落地分析逻辑——核心需求是实现自定义分位数分箱(比如指定recency的分箱边界生成5个分位数),最初用percentile_disc的写法过于冗长,后来尝试CTE写法又碰到了ORA-00978错误,接下来我们一步步解决这些问题。

先解决ORA-00978错误

你写的CTE里MAX_VALUES部分用了sum(max(recency) + 0.0000000001)这种嵌套聚合函数,Oracle不允许在没有GROUP BY的情况下嵌套使用聚合函数(而且这里sum完全是多余的),直接用max()获取最大值即可修正这个错误。

修正后的基础RFM分箱代码(Oracle)

WITH RFM AS (
    SELECT 
        SRC_USER_ID,
        COUNT(DISTINCT PICKUP_DATE) - 1 AS frequency,
        (MAX(PICKUP_DATE) - MIN(PICKUP_DATE)) AS recency,
        (TO_DATE('2018/05/12', 'yyyy/mm/dd') - MIN(PICKUP_DATE)) AS T,
        SUM(PRICE_TOTAL) AS monetary_value
    FROM TRANSACTIONS
    GROUP BY SRC_USER_ID
),
MAX_VALUES AS (
    SELECT 
        MAX(recency) + 0.0000000001 AS max_recency,
        MAX(frequency) + 0.00000001 AS max_frequency,
        MAX(monetary_value) + 0.000000001 AS max_monetary
    FROM RFM
)
SELECT 
    SRC_USER_ID,
    recency,
    frequency,
    monetary_value,
    -- 等宽分箱:将指标分成5个区间
    WIDTH_BUCKET(recency, 0, max_recency, 5) AS recency_quantile,
    WIDTH_BUCKET(frequency, 0, max_frequency, 5) AS frequency_quantile,
    WIDTH_BUCKET(monetary_value, 0, max_monetary, 5) AS monetary_quantile
FROM RFM, MAX_VALUES;

自定义分位数边界的实现方案

如果需要像你提到的,给recency指定固定分箱边界(比如0,0,74,321生成5个分位数),可以用更灵活的方式实现:

方案1:CASE WHEN 自定义分箱(通用Oracle/PostgreSQL)

这种方式最直观,完全可控分箱逻辑:

WITH RFM AS (
    SELECT 
        SRC_USER_ID,
        COUNT(DISTINCT PICKUP_DATE) - 1 AS frequency,
        (MAX(PICKUP_DATE) - MIN(PICKUP_DATE)) AS recency,
        (TO_DATE('2018/05/12', 'yyyy/mm/dd') - MIN(PICKUP_DATE)) AS T,
        SUM(PRICE_TOTAL) AS monetary_value
    FROM TRANSACTIONS
    GROUP BY SRC_USER_ID
)
SELECT 
    SRC_USER_ID,
    recency,
    frequency,
    monetary_value,
    -- 自定义recency分箱规则
    CASE
        WHEN recency <= 0 THEN 1
        WHEN recency <=74 THEN 2
        WHEN recency <=321 THEN 3
        WHEN recency <= (SELECT PERCENTILE_DISC(0.8) WITHIN GROUP (ORDER BY recency) FROM RFM) THEN 4
        ELSE 5
    END AS recency_quantile,
    -- 其他指标用等频分箱的话,直接用NTILE
    NTILE(5) OVER (ORDER BY frequency) AS frequency_quantile,
    NTILE(5) OVER (ORDER BY monetary_value) AS monetary_quantile
FROM RFM;

方案2:PostgreSQL专属简化写法

PostgreSQL的WIDTH_BUCKET支持传入分箱边界数组,自定义分箱更简洁:

WITH RFM AS (
    SELECT 
        SRC_USER_ID,
        COUNT(DISTINCT PICKUP_DATE) - 1 AS frequency,
        (MAX(PICKUP_DATE) - MIN(PICKUP_DATE)) AS recency,
        (DATE '2018-05-12' - MIN(PICKUP_DATE)) AS T,
        SUM(PRICE_TOTAL) AS monetary_value
    FROM TRANSACTIONS
    GROUP BY SRC_USER_ID
)
SELECT 
    SRC_USER_ID,
    recency,
    frequency,
    monetary_value,
    -- 直接传入自定义边界数组生成5个分位数
    WIDTH_BUCKET(recency, ARRAY[0, 0, 74, 321, (SELECT MAX(recency) FROM RFM)]) AS recency_quantile,
    NTILE(5) OVER (ORDER BY frequency) AS frequency_quantile,
    NTILE(5) OVER (ORDER BY monetary_value) AS monetary_quantile
FROM RFM;

简化最初的percentile_disc写法

如果还是想用分位数函数生成动态边界,可以把分位数计算放到CTE里,避免重复代码:

WITH RFM AS (
    SELECT 
        SRC_USER_ID,
        COUNT(DISTINCT PICKUP_DATE) - 1 AS frequency,
        (MAX(PICKUP_DATE) - MIN(PICKUP_DATE)) AS recency,
        (TO_DATE('2018/05/12', 'yyyy/mm/dd') - MIN(PICKUP_DATE)) AS T,
        SUM(PRICE_TOTAL) AS monetary_value
    FROM TRANSACTIONS
    GROUP BY SRC_USER_ID
),
QUANTILE_THRESHOLDS AS (
    SELECT
        PERCENTILE_DISC(0.2) WITHIN GROUP (ORDER BY recency) AS r_q20,
        PERCENTILE_DISC(0.4) WITHIN GROUP (ORDER BY recency) AS r_q40,
        PERCENTILE_DISC(0.6) WITHIN GROUP (ORDER BY recency) AS r_q60,
        PERCENTILE_DISC(0.8) WITHIN GROUP (ORDER BY recency) AS r_q80,
        PERCENTILE_DISC(0.2) WITHIN GROUP (ORDER BY frequency) AS f_q20,
        PERCENTILE_DISC(0.4) WITHIN GROUP (ORDER BY frequency) AS f_q40,
        PERCENTILE_DISC(0.6) WITHIN GROUP (ORDER BY frequency) AS f_q60,
        PERCENTILE_DISC(0.8) WITHIN GROUP (ORDER BY frequency) AS f_q80,
        PERCENTILE_DISC(0.2) WITHIN GROUP (ORDER BY monetary_value) AS m_q20,
        PERCENTILE_DISC(0.4) WITHIN GROUP (ORDER BY monetary_value) AS m_q40,
        PERCENTILE_DISC(0.6) WITHIN GROUP (ORDER BY monetary_value) AS m_q60,
        PERCENTILE_DISC(0.8) WITHIN GROUP (ORDER BY monetary_value) AS m_q80
    FROM RFM
)
SELECT
    r.SRC_USER_ID,
    r.recency,
    r.frequency,
    r.monetary_value,
    CASE
        WHEN r.recency <= q.r_q20 THEN 1
        WHEN r.recency <= q.r_q40 THEN 2
        WHEN r.recency <= q.r_q60 THEN 3
        WHEN r.recency <= q.r_q80 THEN 4
        ELSE 5
    END AS recency_quantile,
    CASE
        WHEN r.frequency <= q.f_q20 THEN 1
        WHEN r.frequency <= q.f_q40 THEN 2
        WHEN r.frequency <= q.f_q60 THEN 3
        WHEN r.frequency <= q.f_q80 THEN 4
        ELSE 5
    END AS frequency_quantile,
    CASE
        WHEN r.monetary_value <= q.m_q20 THEN 1
        WHEN r.monetary_value <= q.m_q40 THEN 2
        WHEN r.monetary_value <= q.m_q60 THEN 3
        WHEN r.monetary_value <= q.m_q80 THEN 4
        ELSE 5
    END AS monetary_quantile
FROM RFM r, QUANTILE_THRESHOLDS q;

关键注意事项

  • 分位数函数选择:PERCENTILE_DISC返回数据集中的实际值(离散分位数),PERCENTILE_CONT返回插值后的连续值,根据业务需求选择即可。
  • 等频vs等宽分箱:NTILE()是等频分箱(保证每个分箱的行数大致相等),WIDTH_BUCKET是等宽分箱(每个分箱的数值范围相等),按需选用。

内容的提问来源于stack exchange,提问作者eamon1234

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.28 06:30:37