包含所有需統計的月份 (如 201001~201109),保證無數據月份也能輸出 0。
-- 每筆消費記錄對應的流失判定月份
-- 消費日期 CostValue (如 201001) + 6個月 = NeedCostDate (201007)
-- 會員必須在 NeedCostDate 所在月份之前 (含) 有後續消費,否則在 NeedCostDate 月份流失
SELECT
UserId,
CostValue,
DATE_FORMAT(DATE_ADD(STR_TO_DATE(CONCAT(CostValue,'01'),'%Y%m%d'), INTERVAL 6 MONTH), '%Y%m') AS NeedCostDate
FROM PDetail
WITH Consumption AS (
-- 每筆消費及其流失判定月
SELECT
UserId,
CostValue,
DATE_FORMAT(DATE_ADD(STR_TO_DATE(CONCAT(CostValue,'01'),'%Y%m%d'), INTERVAL 6 MONTH), '%Y%m') AS NeedCostDate
FROM PDetail
),
NextConsumption AS (
-- 關鍵:每筆消費後,同會員是否在 NeedCostDate 之前有下一筆消費
SELECT
c.*,
EXISTS (
SELECT 1 FROM PDetail p2
WHERE p2.UserId = c.UserId
AND p2.CostValue > c.CostValue
AND p2.CostValue <= c.NeedCostDate
) AS has_next_consumption
FROM Consumption c
),
LostMembers AS (
-- 無後續消費的記錄,標記為在 NeedCostDate 月份流失
SELECT NeedCostDate AS LostMonth, UserId
FROM NextConsumption
WHERE has_next_consumption = FALSE
)
-- 最終彙總:左連接輔助表,保證無流失月份顯示 0
SELECT
ym.DateValue AS 年月,
COUNT(lm.UserId) AS 該月會員流失數量
FROM YearMonth ym
LEFT JOIN LostMembers lm ON ym.DateValue = lm.LostMonth
GROUP BY ym.DateValue
ORDER BY 年月;
原答案提供的長 SQL 即為此邏輯的 5.7 兼容實現,核心步驟一致:
| 要點 | 說明 |
|---|---|
| 日期運算 | 字符串 `YYYYMM` → `STR_TO_DATE(CONCAT(...,'01'),'%Y%m%d')` 轉日期 → `DATE_ADD(..., INTERVAL 6 MONTH)` → `DATE_FORMAT(..., '%Y%m')` 回字符串。 |
| 邊界條件 | `CostValue > 當前消費` 且 `<= NeedCostDate`,嚴格大於當前消費月,小於等於截止月。 |
| 重複流失 | 會員可能多次流失 (流失→回歸→再流失),邏輯基於每筆消費獨立判定,符合業務。 |
| 性能優化 | `PDetail(UserId, CostValue)` 建聯合索引;`YearMonth` 小表可全表掃描;大數據量考慮分區表或預計算物化視圖。 |
| 數據質量 | 假設 `CostValue` 真實反映消費月份,無回補/修正導致邏輯誤判。 |
會員流失統計核心是 **「以每次消費為基準,向前看 6 月無消費則流失」**。使用輔助日期表保證月份完整,通過日期運算得出流失判定月,再用 `EXISTS` 子查詢檢查後續消費存在性,最後聚合統計。MySQL 8.0+ 推薦 CTE 寫法清晰可維護;5.7 用嵌套子查詢/臨時表實現同樣邏輯。