DACエンジニアブログ:アドテクゑびす界

DACのエンジニアやマーケター、アナリストが執筆するアドテクの技術系ブログです。

Treasure Dataで大規模なマスタデータを扱う際にはtimeカラムインデックスを活用しよう

DACではTreasure Dataを利用して各種データの蓄積や集計を行っています。Treasure Dataは時系列のデータを扱うのに特にすぐれたアーキテクチャなのですが、セグメントIDとユーザーIDの組み合わせといった大量のマスタデータを利用した計算にも利用することもできます。そのような場合にtimeカラムインデックスを活用してマスタデータを高速に抽出する方法について解説します。

Treasure Dataでは時系列のデータを効率よくあつかうため、timeカラムが固定インデックスとなっており、3600秒(1時間)ごとのパーティショニングに分けてインポートされます。この性質を利用して、時系列で格納する必要のないデータについては「セグメントID * 3600」「カテゴリID * 3600」のようにマスタのキー値を元に作成した時間を設定することで値が高速に取得できるようになります。

検証のためにtimeカラムをセグメントIDとして設定したテーブルを作成します。テーブル作成の元ネタはセグメントとユーザーのM:N対応を縦持ちで持つテストデータで、総行数は約8億行あります。

-- timeカラムをセグメントID * 3600の設定
INSERT INTO TABLE tmp_segment_time_test
SELECT
segment_id * 3600 time,
segment_id,
tuuid
FROM
segment_data

指定したふたつのセグメント同士の重複ユーザー数を抽出してみましょう。

-- segment_idでセグメントを指定
SELECT
COUNT(distinct t1.tuuid)
FROM
(SELECT tuuid FROM tmp_segment_time_test t1 WHERE segment_id = 61306) t1
INNER JOIN (SELECT tuuid FROM tmp_segment_time_test t1 WHERE segment_id = 51486) t2
ON t1.tuuid = t2.tuuid

Presto計算ログ(1分36秒)

-- memory:1.02GB, peak memory:10.22GB, queued time:1.24ms
20160816_145600_04229_uspws                                                                                                        1.60m           rows  bytes bytes/sec done   total             
[0] output <- aggregation <- [1]                                                                                                   FINISHED          10    90B  15.0KB/s    1 /     1             
 [1] aggregation <- aggregation <- [2]                                                                                             FINISHED      36,657  1.8MB  23.5MB/s   60 /    60             
  [2] aggregation <- project <- innerjoin <- left:project <- filter <- aone_odessa_dev.tmp_segment_time_test <- right:remoteSource FINISHED 951,752,677 24.6GB  39.4MB/s  110 /   111 [*] FullScan
   [3] project <- filter <- aone_odessa_dev.tmp_segment_time_test                                                                  FINISHED 810,615,799 17.5GB  21.2MB/s   61 /    61 [*] FullScan
finished at 2016-08-16T14:57:36Z

指定のセグメントだけを取りたいのにテーブルに対するフルスキャンが走っており、ピークメモリ使用量も大きくなっています。これに対してtimeカラムでセグメントIDを指定してみます。

-- timeでセグメントを指定
SELECT
COUNT(distinct t1.tuuid)
FROM
(SELECT tuuid FROM tmp_segment_time_test t1 WHERE time = 61306 * 3600) t1
INNER JOIN (SELECT tuuid FROM tmp_segment_time_test t1 WHERE time = 51486 * 3600) t2
ON t1.tuuid = t2.tuuid

Presto計算ログ(20秒)

-- memory:1.03GB, peak memory:1.03GB, queued time:446.46us
20160816_145219_04225_uspws                                                                                                        19.54s         rows  bytes bytes/sec done   total                                                   
[0] output <- aggregation <- [1]                                                                                                   FINISHED          0     0B      0B/s    0 /     1                                                   
 [1] aggregation <- aggregation <- [2]                                                                                             FINISHED          0     0B      0B/s    0 /    60                                                   
  [2] aggregation <- project <- innerjoin <- left:project <- filter <- aone_odessa_dev.tmp_segment_time_test <- right:remoteSource FINISHED 14,391,787  712MB   121MB/s    5 /     6 [1976-12-29 10:00:00 UTC, 1976-12-29 10:00:00 UTC)
   [3] project <- filter <- aone_odessa_dev.tmp_segment_time_test                                                                  FINISHED 14,127,595  306MB  21.4MB/s    1 /     1 [1975-11-16 06:00:00 UTC, 1975-11-16 06:00:00 UTC)
finished at 2016-08-16T14:52:39Z

実行時間が20%程度になり、ピークメモリ使用量も10%程度に削減されています。timeカラムインデックスを利用しているため、セグメントIDが「1975-11-16 06:00:00 UTC」という扱いになっています。timeカラムインデックスを利用した格納・取得方法はHiveでもPrestoでも効きますので、時系列に格納する必要性のないデータについては、マスターデータのキーをtimeとして指定しながら格納することで高速な抽出ができるようになります。もちろん結果値は同等です。

注意点としてはtimeはBigInteger型であり、日付型としても扱われることから1億年と2000年前から検索するといった事はできません。このような値をtime値を格納すると正常にパーティショニングされず、timeを利用していないクエリについても正常に取得できなくなる可能性があります。このため大きなID番号を取り扱う際には「time * 3600」ではなく「time * 360」としたうえでセグメントIDとの複合キーにするなど、適切な範囲で散らばるようにIDをグルーピングすべきです。

以上、Treasure Dataで大規模なマスタデータを扱う際にはtimeカラムインデックスが利用できるというTIPSでした。