TD_SESSIONIZEを使うときはサブクエリを使え


Webアクセスログ解析に超絶便利なTreasureDataのHive UDFのTD_SESSIONZEですが、使用する際には以下の前提があるので注意すること。
HiveQLがエラーになるわけではないが、狙ったようにセッションIDが付与されないので誤った集計結果になってしまいます。

  • 引数に渡すカラムについてDISTRIBUTE BYSORT BYするサブクエリの直後に使用する

TD_SESSIONIZEとは

同一アクセスと判断できるログに一意なセッションIDを付与してくれる関数。

string TD_SESSIONIZE(int/long unix_timestamp, int timeout, string sessionize_by)
  • 第一引数:UNIX時刻
  • 第二引数:同一セッションと判断する時間の範囲(単位は秒)
  • 第三引数:同一セッションと判断するフィールド

公式ドキュメント

具体例

サンプルで用意されているsample_datasetsのwww_accessテーブルを使って、
同じホスト(格納されているのはIPアドレス)からの1時間以内のアクセスを同じセッションとみなしたい場合、以下のように使います。

正しい使い方

SELECT 
  TD_SESSIONIZE(time,3600,host) AS session_id,
  TD_TIME_FORMAT(time,'yyyy-MM-dd HH:mm:ss','JST') AS access_time,
  host,
  path
FROM (
    SELECT 
      time,
      host,
      path
    FROM
      www_access
      DISTRIBUTE BY host
      SORT BY host,time
  ) t

結果には以下のようにセッションIDが付与されます。

session_id access_time host path
bb368f01-9198-4718-9df3-51baeff9fce1 2014-10-04 05:39:00 100.111.144.44 /item/computers/4323
bb368f01-9198-4718-9df3-51baeff9fce1 2014-10-04 06:11:08 100.111.144.44 /item/software/759

ここで、重要なのが、
* TD_SESSIONIZEの第三引数に使いたいフィールドでDISTRIBUTE BY
* TD_SESSIONIZEの第三引数、第一引数に使いたいフィールドでSORT BY
をサブクエリで実行した後にTD_SESSIONIZEを計算させること。
これは必須条件です。

誤った使い方

以下のようにTD_SESSIONIZEDISTRIBUTE BYSORT BYと同じレベルで実行するのはダメです。

SELECT 
  TD_SESSIONIZE(
    time,
    3600,
    host
  ) AS session_id,
  TD_TIME_FORMAT(time,
    'yyyy-MM-dd HH:mm:ss',
    'JST') AS access_time,
  host,
  path,
  time
FROM www_access DISTRIBUTE BY host SORT BY host,time

HiveQLはエラーを出さずに正常終了するのですが、結果として

session_id access_time host path
0f760fbe-4e7b-411a-b82d-4e44c0fd8cd8 2014-10-04 05:39:00 100.111.144.44 /item/computers/4323
c2700830-f6e7-40af-9c78-4bc72eebba64 2014-10-04 06:11:08 100.111.144.44 /item/software/759

のように、同じセッションとみなしてほしい(=同じホストで1時間以内のアクセス)レコードに異なるセッションIDが付与されてしまいます。

考察

落ち着いて考えると、それはそうだよなぁという感じ。
DISTRIBUTE BYSORT BYもMapReduceにおいてReducerに集めるカラムのキー範囲や順番を制御するものです。
TD_SESSIONIZEを実装するなら、事前にセッションを判定するキーが一箇所に集約されていて、しかも時間の順番にソートされている前提で、上から順番にレコードを読んでIDを付与していくのが一番楽です。
なので、一度MapReduceでその条件でレコードをグループ分けかつソートし、2段目のMapReduceのMapperでそれぞれTD_SESSIONIZEしてもらうのが自然。

辛いのは、ドキュメントの説明が

Similiar to TD_X_RANK, you need to ‘CLUSTER BY’ or ‘DISTRIBUTE BY’ the sessionize_by field, and sort by the unix_timestamp field.

となっていて、一見するとこの前提がわかりづらいこと。
Similar to TD_X_RANKとあるのでそちらの説明を見ると、

You need to ‘CLUSTER BY’ or ‘ORDER BY’ within a sub query to use this feature.

とあるので、霊感で気づくのが要求されます。。。

2016/10/17 追記

TreasureDataのサポート経由で報告したところ、すぐにドキュメントを修正してくれました!

Similiar to TD_X_RANK, you need to CLUSTER BY or DISTRIBUTE BY the sessionize_by field, and SORT BY the unix_timestamp field within a sub query to use this feature.

まとめ

超絶便利な関数なので上記の点に気をつけて使いまくりましょう。