Top N 件をとる効率的なHive / Prestoクエリ


遅いクエリを眺めてたら、Prestoでrow_numberを使ってナンバリングをした後に、rank<=10といったことをしているクエリが多々あった。
例えばPrestoだと、row_numberは全レコードを保持して処理するので、件数が多ければ多いほど遅いし、メモリ消費量もあれなことになる。例えば数億件でrow_numberをすると2~300GBピーク時に使ってそうだ。
https://github.com/prestodb/presto/issues/5298

なので、効率的なPrestoとHive0.13のクエリを書いておく。

Presto

ダメな例(row_number)

データ件数数億件で、30分以上かかっても終わらなさそう。

SELECT checksum(rnk)
FROM (
  SELECT row_number() OVER (PARTITION BY l_orderkey, l_partkey ORDER BY l_shipdate DESC) AS rnk
  FROM lineitem
) t
WHERE rnk = 1

良い例(rank)

数億件で1~2分程度で終わる。メモリ消費も数GB程度。

SELECT checksum(rnk)
FROM (
  SELECT rank() OVER (PARTITION BY l_orderkey, l_partkey ORDER BY l_shipdate DESC) AS rnk
  FROM lineitem
) t
WHERE rnk = 1

Hive

悪いとまでは言わないけど良くない例(row_number, rank)

Hiveのrankやrow_numberはあんまりパフォーマンス的には変わらなそう。(コードまでは見てない。)
数億件で4~6分くらい。

SELECT count(rnk)
FROM (
  SELECT rank() OVER (PARTITION BY l_orderkey, l_partkey ORDER BY l_shipdate DESC) AS rnk
  FROM lineitem
) t
WHERE rnk = 1
SELECT count(rnk)
FROM (
  SELECT row_number() OVER (PARTITION BY l_orderkey, l_partkey ORDER BY l_shipdate DESC) AS rnk
  FROM lineitem
) t
WHERE rnk = 1

良い例 (each_top_k)

HiveというよりはHivemallの関数にeach_top_kがある。これはまさにTop N件を取得するための関数。
これを使うと3分くらいで処理が終わるようになる。下記の例では、COUNT用にMRの段数を1つ増やして3分なので、それを消せばもう少し早く終わるのではないかなと。

SELECT COUNT(rnk) FROM (
  SELECT each_top_k(
      1, concat(l_orderkey, '+', l_partkey), l_shipdate,
       l_orderkey, l_partkey
    ) as (rnk, l_shipdate,  l_orderkey, l_partkey)
  FROM (
    SELECT l_orderkey, l_partkey, l_shipdate
    FROM lineitem
    CLUSTER BY l_orderkey, l_partkey
  ) t0
) t1

ちゃんちゃん。