1. 程式人生 > >mysql統計實戰-查詢每個季度新增支付使用者數及其在後續季度的留存

mysql統計實戰-查詢每個季度新增支付使用者數及其在後續季度的留存

題設如下:

A使用者第一次支付是在季度B,則這個A使用者視為B季度的新增支付使用者,A使用者在後續的C季度再次支付了訂單,則這筆訂單的gmv就是B季度新增支付使用者在C季度的留存

需求如下:

計算每個月度新增支付使用者數,新增支付使用者的訂單數,新增支付使用者的gmv,以及這個這三個資料在後續季度的留存情況

首先建立簡化的訂單表

CREATE TABLE `order` (
  `id` int(11) NOT NULL DEFAULT '0' COMMENT '訂單ID',
  `uid` int(11) NOT NULL DEFAULT '0' COMMENT '使用者id',
  `pay_date` date NOT NULL DEFAULT '0000-00-00' COMMENT '支付時間',
  `gmv` float(11,2) NOT NULL DEFAULT '0.00' COMMENT 'gmv',
  PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='訂單表';
SET @last_uid=-1;
SET @last_q=-100;
#生成表頭
SELECT CONCAT('期間,當期,',GROUP_CONCAT(q ORDER BY q ASC)) FROM (SELECT DISTINCT IF(o.pay_date>='2015-01-01',CONCAT(YEAR(o.pay_date),'Q',CEILING(MONTH(o.pay_date) / 3)),'2015Q0') AS q  FROM tb_order AS o WHERE o.pay_date >='2014-01-01' AND o.pay_date <'2017-09-01') AS t
UNION ALL 
#表頭結束
#生產表格主體
#第五層,增加當期列
SELECT CONCAT(q1,'	',SUM(IF(q1=q2,cnt,0)),'	',GROUP_CONCAT(cnt ORDER BY q2 ASC)) FROM(
#第四層,按照首個季度q1聚合
SELECT q1,q2,SUM(cnt) AS cnt FROM (
#第三層,按照第一個支付的季度,和實時的支付聚合
SELECT q1,q2,SUM(cnt) AS cnt FROM (
#第二層,利用兩個使用者變數,向下傳遞上一行的uid和季度,下一行比較兩個使用者變數,如果上一行的uid和本行不同,則本行就是這個uid的第一次支付的季度為q1,q2為本條目當前支付的季度,f忽略
SELECT uid,t.q AS q2,IF(@last_uid!=t.uid,(@last_q:=t.q),@last_q) AS q1,(@last_uid:=t.uid) AS f,cnt FROM
 (
#第一層,將訂單資料按uid和季度聚合
SELECT IF(o.pay_date>='2015-01-01',CONCAT(YEAR(o.pay_date),'Q',CEILING(MONTH(o.pay_date) / 3)),'2015Q0') AS q,
uid,
#人數
#COUNT(DISTINCT o.uid) AS cnt
#訂單數
#COUNT(DISTINCT o.order_id) AS cnt
#gmv
SUM(o.gmv) AS cnt
FROM tb_order AS o 
WHERE  o.pay_date < '2017-09-01'
GROUP BY uid ,q  
ORDER BY uid ASC,q ASC
#第一層結束
)t 
#第二層結束
) t 
WHERE q1 >='2015Q0' AND q1 < '2017Q4' AND q2 >='2015Q0' AND q2 < '2017Q4'
GROUP BY q1,q2
第三層結束
UNION ALL 
#生成完整的佔位網格
SELECT ql.q AS q1,qr.q AS q2,0 AS cnt FROM 
(SELECT DISTINCT IF(o.pay_date>='2015-01-01',CONCAT(YEAR(o.pay_date),'Q',CEILING(MONTH(o.pay_date) / 3)),'2015Q0') AS q  FROM tb_order AS o WHERE o.pay_date >='2014-01-01' AND o.pay_date <'2017-09-01') AS ql
INNER JOIN (SELECT DISTINCT IF(o.pay_date>='2015-01-01',CONCAT(YEAR(o.pay_date),'Q',CEILING(MONTH(o.pay_date) / 3)),'2015Q0') AS q  FROM tb_order AS o WHERE o.pay_date >='2014-01-01' AND o.pay_date <'2017-09-01') AS qr
GROUP BY q1,q2
) t GROUP BY q1,q2
#第四層結束
) t GROUP BY q1
#第五層結束
#表格主體結束

結果截圖如下


在上面截圖中每行的列之間用tab製表符分割,貼上到notepad並顯示所有字元如下



其中橙色的箭頭就代表了製表符,將以上字元直接貼上到excel中為


其中每行的第一個單元格是第一次支付的季度,第一行表頭的是隨後的每個季度,表格主體的意思是

這樣我們就用sql直接生成了一個可以直接貼上到excel的字元文字。

下面我們開始分析整個過程

在這個需求中主要有以下難點

1.列數不固定,因為從統計的時間區間內每個季度都會佔用一列,而mysql不能像progress sql那樣構造行

2.需要確定每個使用者的第一個季度和以後的其他季度。

3.並不是每個單元格都有資料,上面截圖的值為0的單元格其實並沒有值需要填充。

下面開始逐一分析每層的邏輯

第一層,將訂單資訊按照uid和支付的月份聚合,如果超出統計日誌的範圍就設定為2015Q0,產生一個由uid,q(季度),和cnt組成的結果集,並按照uid和q排序。

第二層,利用兩個使用者變數向下傳遞上一條的uid和q,如果本條的uid和上一條的uid不同那麼重置,將本條的q賦值給使用者變數並向下傳遞,一直到下一個uid再次終止為止,這樣q1就是每個uid的的第一個q,也就是這個uid的首次支付季度。將q直接賦值給q2。

第三層,按照首次支付的季度和後續的季度,也就是q1,q2聚合,產生一個欄位為q1,q2,cnt的結果集。在這個過程中,不是每個季度的首次支付使用者在後續的每個季度都有留存,這樣的話表格上機會有空缺,為了彌補空缺的位置,需要union 一個全量的,但是cnt都為0的一個表格,這樣的話就實現了用0佔空位。

第四層,按照q1聚合,後面的每一列都用tab隔開,這一行就都是這個q1在後續每個q2的留存資料。

第五層,增加當期,在第二列增加當期,q1=q2時的資料,也就是使用者在自己所屬的q1的訂單資料。

最後需要說明的是在第一層的select中有被註釋的幾行,分別為人數,訂單數,gmv,當統計人數的時候就將訂單數和gmv註釋掉,其他的兩個也一次類推。