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註釋掉,其他的兩個也一次類推。