SQLで分析を始めた人に贈る、中級者に上がるための10のTips -後編-

こんにちは!PairsのAnalyzeチームでエンジニアをしている鉄本です。
 
Analyzeチームでは、施策検討時のデータ抽出やサービス上の問題検知のために、分析用途のSQLクエリ (MySQL) を作成しています。
このシリーズでは、日々の業務を通して学んだTipsやよく使うクエリの一部を簡単な活用例と一緒に、2回に分けて紹介しています。今回はその後編です。
※前編はこちら
 
前回に引き続き、クエリの紹介の中で出てくる関数の詳細は、公式リファレンスを参考にしてください。
 


目次

– 前編 –

1. クエリ内の複数箇所で使われる固定値を「ユーザ定義変数」でまとめる
2. 誕生日(DATE)から現在の年齢を計算する
3. データのない日を補完して日別レポートを作成する
4. 7日移動平均でデータの増減傾向を割り出す

前編でも、分析でよく使う便利なクエリを紹介しているので、ぜひこちらも参考にしてみてくださいね!

– 後編 –

5. メールアドレスのドメインを集計する
6. 乱数で検証データを生成する
7. 複数行の結果をカンマ区切りで1行に集約する
8. グルーピングした1番古い(新しい)レコードの情報を取り出す


 

5. メールアドレスのドメインを集計する

メールアドレスのドメインの分布を分析したいことってありますよね?
そんな時に役に立つのが SUBSTRING_INDEX() という関数です。

SET @EMAIL = 'hoge@hoge.com';
SELECT SUBSTRING_INDEX(@EMAIL, '@', -1); // hoge.com

SUBSTRING_INDEX()は区切り文字に対して、N個目までの文字列を切り出すための関数です。
www.Pairs.lv” のURLのドメインを例にクエリを実行してみます。
正の値は先頭から、負の値は末尾から個数を指定できることがわかります。

SET @URL_DOMAIN = 'www.Pairs.lv';
SELECT
  num,
  SUBSTRING_INDEX(@URL_DOMAIN, '.', num) AS plus,
  SUBSTRING_INDEX(@URL_DOMAIN, '.', -1 * num) AS minus
FROM
 ( -- 0〜3までの連続する数字を生成。詳しくは、前編を参照ください。
    SELECT @num := 0 AS num
    UNION ALL
    SELECT @num := @num+1 FROM information_schema.COLUMNS
    LIMIT 4
 ) AS n
;
num plus minus
0
1 www lv
2 www.Pairs Pairs.lv
3 www.Pairs.lv www.Pairs.lv

 

さて、本題に戻ります。
例えば以下のようなデータを集計する場合、メールアドレスのドメインでグルーピングして集計することができます。

user

user_id email
1 hoge@hoge.com
2 hogehoge@hoge.com
3 fuga@fuga.com
SELECT
 SUBSTRING_INDEX(email, '@', -1) AS domain,
 COUNT(user_id) AS cnt
FROM user
GROUP BY domain
;
domain cnt
hoge.com 2
fuga.com 1

メールアドレス以外にも、URLのサブドメインを知りたい、小数の小数部だけを抜き出したい…など様々なシーンで利用できます。

 

6. 乱数で検証データを生成する

検証用に自前でデータを用意するとき、本番データをマスキングして使うことがあります。
そのときに役に立つのが、「ランダムな文字列を生成する方法」です。
例えば、先ほどのドメインを取得するクエリを例にマスキングしたメールアドレスを作ってみます。
 
ランダムな文字列は、MD5()UUID(), ENCRYPT()..などの暗号化関数で生成することができます。

SET @EMAIL = 'hoge@hoge.com';
SELECT CONCAT(MD5(@EMAIL), '@', SUBSTRING_INDEX(@EMAIL, '@', -1));
// => 4712f9b0e63f56ad952ad387eaa23b9c@hoge.com

さらに、SUBSTRING()関数で必要な長さに調整することもできるので、
emailの重複を許容している場合は以下のクエリでも大丈夫です。
 
以下の例では、生成した文字列の1文字目から6文字分を切り取っています。

SET @EMAIL = 'hoge@hoge.com';
SELECT CONCAT(SUBSTRING(MD5(@EMAIL), 1, 6), '@', SUBSTRING_INDEX(@EMAIL, '@', -1));
// => 4712f9@hoge.com

また、数字を適当に用意したいときにはRAND()関数を使います。
RAND()関数自体は0以上1未満の小数の値を返すので、必要に応じて四捨五入や桁の調整が必要になります。
 
例えば、 1〜31の数字が欲しい場合は以下で取得することができます。

SET @FROM = 1, @TO = 31;
SELECT FLOOR(@FROM  + RAND() * (@TO - @FROM + 1));
// => 23

これらを応用すると、特定の日から1年以内の日付をランダムで必要な件数分生成する、といったことも可能です。

SET @START_DATE = '2000-01-01', @FROM = 1, @TO = 365;
SELECT
    @START_DATE + INTERVAL FLOOR(@FROM  + RAND() * (@TO - @FROM + 1)) DAY AS random_date
FROM
 (
    SELECT @num := 0 AS num
    UNION ALL
    SELECT @num := @num+1 FROM information_schema.COLUMNS
    LIMIT 100
 ) AS n
;
random_date
2000-06-28
2000-08-10
2000-07-27

 

7. 複数行の結果をカンマ区切りで1行に集約する

以下のような、ユーザーの趣味のテーブルがあるとします。

user_hobby

uid hobby_type hobby
1 インドア 読書
1 インドア 料理
2 アウトドア 旅行

ここで、ユーザー毎に横並びで趣味を表示したい場合、GROUP_CONCAT()という便利な関数があります。

SELECT
 user_id AS uid,
 GROUP_CONCAT(hobby) AS hobby_list
FROM user_hobby
GROUP BY uid
;
uid hobby_list
1 読書,料理
2 旅行

さらに、並びや区切り文字の指定も可能です。
仮にデータが重複していた場合、DISTINCTをつけて弾くこともできます。便利ですね。

SELECT
 user_id AS uid,
 GROUP_CONCAT(hobby
    ORDER BY hobby DESC -- 降順に並べる
    SEPARATOR "\t" -- 区切り文字をタブに
 ) AS hobby_list,
 GROUP_CONCAT(DISTINCT hobby_type) AS type_list  -- データの重複を弾く
FROM user_hobby
GROUP BY uid
;
uid hobby_list type_list
1 料理 読書 インドア
2 旅行 アウトドア

また、テーブル結合をしてデータがない場合は”NULL”になります。
先ほどの例のuserテーブルと結合して集計してみます。

SELECT u.id AS uid, GROUP_CONCAT(hobby)
FROM user AS u
 LEFT JOIN user_hobby AS h
  ON u.id = h.user_id
GROUP BY uid
;
uid hobby_list
1 読書,料理
2 旅行
3 NULL

余談ですが、この結果を用いてExcelで区切り位置を「,」に指定すれば、セルを分割することもできます。

Excelのアウトプット例

uid hobby_list
1 読書 料理
2 旅行
3

 

8. グルーピングした1番古い(新しい)レコードの情報を取り出す

データをグルーピングするとき、1番古いレコードの情報をとりたいことってありますよね。
例えば以下のようなポイント取得テーブルがあるとします。
ここで、日別でユーザー毎に、最初のレコード日時と獲得したポイント数や、そのとき利用していたデバイス情報を取得したいとき、どうしますか?

user_point

id user_id created_at point device
1 1 2016-10-01 00:00:00 100 pc
2 1 2016-10-01 13:00:00 200 sp
3 1 2016-10-02 01:00:00 300 sp
4 2 2016-10-01 12:00:00 200 ios
5 2 2016-10-01 14:00:00 100 sp
6 2 2016-10-02 02:00:00 500 sp
7 2 2016-10-02 15:00:00 50 ios

よく例にあがる方法としては、
目的のグルーピングをしたSELECT文でレコードを特定し、クエリを入れ子にすることで目的のデータを取得できます。

SELECT
 user_id,
 DATE(created_at) AS dt,
 point AS first_point,
 device AS first_device
FROM user_login
WHERE
 id IN  -- 入れ子にしたSELECT文でグルーピングした最古のレコードのidを取得する
  (
   SELECT
    MIN(id)
   FROM user_login
   GROUP BY user_id, DATE(created_at)
  )
GROUP BY user_id, dt
;
user_id dt first_point first_device
1 2016-10-01 100 pc
1 2016-10-02 300 sp
2 2016-10-01 200 ios
2 2016-10-02 500 sp

ただこのIN句とサブクエリの組み合わせは、データ量が増えたり、複雑なクエリになると結果が返ってこない可能性も考えられます。
そこで、普段分析クエリを作る時に工夫している方法を紹介したいと思います。

SELECT
 user_id,
 DATE(created_at) AS dt,
 MIN(id * 1000 + point) % 1000 AS first_point,
 (CASE
     MIN(id * 10 +
         CASE device
             WHEN 'sp' THEN 1
             WHEN 'pc' THEN 2
             WHEN 'ios' THEN 3
             WHEN 'android' THEN 4
             ELSE 5
         END
     ) % 10
     WHEN 1 THEN 'sp'
     WHEN 2 THEN 'pc'
     WHEN 3 THEN 'ios'
     WHEN 4 THEN 'android'
     ELSE 5 THEN 'other'
 END) AS first_device
FROM user_login
GROUP BY user_id, dt
; 

一見複雑そうに見えますが、これでサブクエリを使ったり重いクエリになることなく集計が可能になりました。
 
ここでは、MIN()関数を使って1番古いレコードを集計する際に、桁を上げて必要な情報を付与しています。
今回の例ではポイントは最大3桁なので、idに1000をかけてポイントを加算する (id*10000+point) ことで、idと同じ大小関係のデータを用意しています。

id point id*1000+point
1 100 1100
2 200 2200
3 300 3300
4 200 4200
5 100 5100
6 500 6500
7 50 7050

クエリの複雑度が増すので今回は説明を割愛しますが、仮に必要な桁数がわからない場合は、小数点やLENGTH()関数を使ってidに桁の情報も含めることで対応可能です。
 
 
また、pointのように数字ではない場合でも、種類が決まっていればCASE文を使って一時的にidを振り分けてあげることで対応が可能になります。
deviceの例を参考に見てみると、CASE文を2度使って工夫しています。
① 1度目のCASE文:device情報をidに置き換える
② idの桁を1つ上げて、1の位にidに変換したデバイス情報を埋め込む
③ デバイス情報を埋め込んだidが1番若いレコードを取得する
④ 2度目のCASE文:device情報を復元する

 (CASE
     -- ③ デバイス情報を埋め込んだidが1番若いレコードを取得する
     MIN(
         -- ② idの桁を1つ上げて、1の位にidに変換したデバイス情報を埋め込む
         id * 10 +
         -- ① 1度目のCASE文:device情報をidに置き換える
         CASE device
             WHEN 'sp' THEN 1
             WHEN 'pc' THEN 2
             WHEN 'ios' THEN 3
             WHEN 'android' THEN 4
             ELSE 5
         END
     )
     -- ④ 2度目のCASE文:device情報を復元する
     % 10
     WHEN 1 THEN 'sp'
     WHEN 2 THEN 'pc'
     WHEN 3 THEN 'ios'
     WHEN 4 THEN 'android'
     ELSE 5 THEN 'other'
 END) AS first_device

もしidのような連番で特定できるカラムがない場合でも、例えばUNIX_TIMESTAMP(created_at)で作成順に置き換えることが可能です。
テーブルの構成や目的に合わせて、工夫してみてください!

最後に

本記事を最後までご覧いただきありがとうございます。
今回も、前編に続いて分析や検証で役に立つTipsを4つ紹介しました。
前回に比べて「使わなくてもなんとかなるけど使ってみたら結構便利!」な内容を紹介していますので、是非使ってみてください。

番外編について

実は10のTipsがまだ8つしかないことにお気付きの方もいると思います。
本当は「私がはまったMySQLの罠」についてもお話したかったのですが、なかなかのボリュームになってしまったため、次回 番外編にてお話したいと思います。
どうぞお楽しみに!!

  • このエントリーをはてなブックマークに追加

エウレカでは、一緒に働いていただける方を絶賛募集中です。募集中の職種はこちらからご確認ください!皆様のエントリーをお待ちしております!

Recommend

脱Go言語初心者への道のり #1 〜オフィシャルガイドラインの作法【前編】〜

Go Conference 2016 Springで発表してきました