原題:Q: Exceeded max (local) memory xxGB error.

Presto が Hive と比べて高速化を実現している1つの要因に,メモリを積極的に活用している事が挙げられます。逆に言えば,Presto と用意されているメモリ(プランに依存します)の許容量を超えるクエリを実行してしまうと,例えクエリが正しくとも,"Exceeded max (local) memory xxGB error." によってエラーが返されます。もしこのエラーが実行した時にすぐ出現すれば対処はできますが,ある日バッチ処理などで起こってしまうと,そのエラーをしばらく見過ごしてしまうことになり,深刻な問題を引き起こす可能性があります。故に日頃からメモリを喰わないクエリを心がけることは大変重要な事です。

心得その1. メモリの使用量の高いオペレーションを理解せよ。

以下に挙げるオペレーションはメモリの使用量が高くなっているものです。JOIN, GROUP BY, UNION など,概ね想像通りのラインナップですが,ORDER BY や DISTINCT にも注意が必要です。

  • DISTINCT

  • UNION

  • ORDER BY

  • GROUP BY (of many columns)

  • JOIN

これらのオペレーションのパフォーマンス改善の観点での具体的な代替方法については,「クエリのパフォーマンス改善のための Tips」をご参照下さい。ここでも一つ一つ具体的に見ていきます。

  1. DISTINCT オペレーターの使用を避ける。

COUNT 内における DISTINCT の使用上の注意は既出(後で引用します)ですが,まずは DISTINCT オペレーター自身の挙動について理解しておくべきです。

上記のような裸の DISTINCT を用いたクエリでは,以下の様な作用をします:

  • DISTINCT はカラム c1, c2, c3 全てに作用する

  • (c1, c2, c3) のユニークな組合せを計算し(重複を1つにまとめ)ようとする

  • (c1, c2, c3) の組合せを全てメモリ上に保存しようとする

  • しかしこの処理はシングルノードのメモリのみしか使用できない

しかしながら,後述しますが以下の様に DISTINCT を GROUP BY で代用しても改善にはならない事に注意してください。以下の例では 1億レコードのデータセットから3つのカラムの500万通りの組合せを列挙しようとしています:

a. DISTINCT(1分04秒)

b. GROUP BY(1分15秒)

  1. ユニークユーザ取得のCOUNT(DISTINCT)ではなくAPPROX_DISTINCTの利用。

「クエリのパフォーマンス改善のための Tips」の同見出しを参照ください。

  1. UNION に替えて UNION ALL を使用せよ。

UNION は指定されたカラムの全ての組合せ(tuples)の中でユニークなものをテーブルの垣根を越えて抽出します。こちらも DISCINTCT 同様,シングルノード処理となり,全てのカラムの組合せを保持するので大量のメモリリソースを必要とします。一方,UNION ALL オペレーションは組合せを考慮せず,純粋に2つのテーブルを統合するだけなのでメモリリソースは多くありません。UNION ALL の後,より効率的な方法で UNION でやりたかったことを実現するのが賢明です。

↑ UNION は2つのテーブルのカラム (c1, c2, c3) の重複を取り除いた組合せを列挙します。想像するだけでも大変な処理です。しかもシングルノード処理です…

↑ UNION ALL は単純に2つのテーブルの全レコードの c1, c2, c3 の値を列挙します。

  1. 巨大なクエリ結果へのORDER BYの除去

「クエリのパフォーマンス改善のための Tips」の同見出しを参照ください。

  1. GROUP BY キーのカラム数を減らす

  1. の DISTINCT と同様,GROUP BY でキー指定されたカラム c1, c2, c3 の組合せを計算し,かつメモリ上に組合せを保持するためにコストの高い処理となります。1. の DISTINCT との比較部分もご参照ください。

  1. GROUP BY のキーとして smart_digest() UDF を用いる。

Treasure Data UDF である smart_digest は,引数に文字列カラムしか持って来れませんが,効率の良いアルゴリズムで組合せを計算してくれます。使える場合は積極的に活用します。ただしこの場合,smart_digest(key) の値はハッシュ値に変換されるため,元の key の値とはならないことに注意してください。つまり COUNT と併用するときのみに有効なのです。以下の比較は引き続き1億レコードのデータセットを使用しています。

a. GROUP BY key(59秒)

b. GROUP BY smart_digest(key)(33秒)

c. GROUP BY smart_digest(key)(2分21秒)※ 1.の続き,3カラムバージョン

  1. 大きいテーブル「から」小さいテーブルを JOIN する

以下の様に,小さなテーブルを先に記述して,大きなテーブルを JOIN するような意図となるクエリを書いてしまうと,JOIN しようとする大きなテーブルがメモリの容量をオーバーしてエラーが生じてしまう可能性があります:

--Bad Case: JOIN FROM small_table to large_table
  1. SELECT 
    * 
    FROM
     small_table 
    ,
     large_table
  2. WHERE
     small_table.id 
    =
     large_table.id

一方,以下の様に大きなテーブルを先に記述すると,「Broadcast Join」という処理に最適化され,左側のテーブルはいくつかのノードにパーティションニングされて処理が進められます。一方,右側のテーブルは全てのノードにコピーされるため,こちらが大きいとメモリエラーが出ます。

--Good Case: JOIN FROM large_table to small_table
  1. SELECT 
    * 
    FROM
     large_table 
    ,
     small_table
  2. WHERE
     large_table.id 
    =
     small_table.id
  1. Distribute Join を活用する

それでもどうしてもうまく行かない場合は,JOIN を分散実行するためのマジックコメントを記述して実行してみます。:

Distribute Join は左/右側 両方テーブルの JOIN Key をハッシュ化し,これを Partition Key としてパーティショニングします。両方のテーブルに対してパーティショニングを行うため,いくらハッシュ化した値と言えど,全てのノードへのネットワーク通信コストがかかり,必ずしも最適なパフォーマンスとならない事に注意してください。

お使いの Presto バージョンが 0.205 の場合は

-- set session distributed_join = 'true'

を記述し、バージョン 317 の場合は

-- set session join_distribution_type = 'PARTITIONED'

 を記述して実行してください。

なお、Presto のバージョンはは2020年1月14日以後順次 0.205 から 317 へのアップグレードを進めており、1月30日現在は両方のバージョンが利用可能ですが、将来的に 0.205 は廃止予定です。

アップグレードの詳細およびPrestoバージョンの指定方法についてはこちらのドキュメントをご覧ください。

  • No labels