Amazon Athena/PrestoのLambda式・Lambda関数を触ってみた

2018.01.21

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

サーバーレスのインタラクティブなクエリーサービスAmazon Athenaはバックエンドのクエリーエンジンに分散クエリーエンジンのPrestoを採用しています。 この度、Athena の利用する Presto のバージョンが 0.172 にアップデートされ、Lambda式・Lambda関数に対応したため、Prestoのオフィシャルドキュメントを頼りに実際に動かしてみました。

なお、最新の EMR である 5.10.0 の Presto のバージョンは 0.187 です。

Lambda 式について

Presto 公式ドキュメントからの転載となりますが、以下のように、ほとんどの SQL 式を使えます。

x -> x + 1
(x, y) -> x + y
x -> regexp_like(x, 'a+')
x -> x[1] / x[2]
x -> IF(x > 0, x, -x)
x -> COALESCE(x, 0)
x -> CAST(x AS JSON)

一方で、以下には対応していません。

  • サブクエリー x -> 2 + (SELECT 3)
  • 集約関数 x -> max(y)
  • TRY 関数

なお、最新の Presto 0.192 は TRY 関数に対応しています

x -> x + TRY(1 / 0)

Lambda 関数を使ってみる

Lambda 式を関数として渡してみましょう。

Presto 公式ドキュメントにLambda関数の定義とその実行例があるため、各関数に対して例をひとつだけ選び、動かしてみます。

また、関数の理解を深めるため、Presto の Lambda 関数を Python で書き換えます。

filter(array<T>, function<T, boolean>) → ARRAY<T>

array 型を引数に受け取り、 function が true となるエレメントだけの array 型を返します。

SELECT filter(ARRAY [5, -6, NULL, 7], x -> x > 0)
-- [5, 7]

Python 版

l = [5, -6, None, 7]
filter(lambda x : x > 0, l) # 例1
[x for x in l if x > 0] # 例2

map_filter(map<K, V>, function<K, V, boolean>) → MAP<K,V>

map 型を引数に受け取り、 function が true となるエレメントだけの map 型を返します。

SELECT map_filter(MAP(ARRAY[10, 20, 30], ARRAY['a', NULL, 'c']), (k, v) -> v IS NOT NULL)
-- {10=a, 30=c}

Python 版

d = dict(zip([10,20,30], ['a',None,'c']))
dict(filter(lambda (k,v):v is not None, d.iteritems())) # 例1
{k:v for k,v in d.iteritems() if v is not None} # 例2

reduce(array<T>, initialState S, inputFunction<S, T, S>, outputFunction<S, R>) → R

array 型を引数に受け取り、reduce した結果を返します。

SELECT reduce(ARRAY [5, 20, 50], 0, (s, x) -> s + x, s -> s)
-- 75

Python 版

reduce(lambda x, y: x+y, [5, 20, 50]) # 例1
((5+20)+50) # 例2

loop を使うなら

result = 0
list = [5, 20, 50]
for num in list:
    result = result + num

Python3 では "put in functools, a loop is more readable most of the times" という理由により reduce は組み込み関数から削除され、functools モジュールの1関数となっています。

transform(array<T>, function<T, U>) → ARRAY<U>

array 型を引数に受け取り、 各エレメントに function を実行した array 型を返します。

SELECT transform(ARRAY [5, 6], x -> x + 1)
-- [6, 7]

Python 版

map(lambda x:x+1, [5, 6]) # 例1
[x+1 for x in [5, 6]]     # 例2

transform_keys(map<K1, V>, function<K1, V, K2>) → MAP<K2,V>

map 型を引数に受け取り、 各エントリーに function を実行した結果でキーを更新した map 型を返します。

SELECT transform_keys(MAP(ARRAY [1, 2, 3], ARRAY ['a', 'b', 'c']), (k, v) -> k + 1)
-- {2=a, 3=b, 4=c}

Python 版

d = dict(zip([1,2,3], ['a','b','c']))
dict(map(lambda (k,v): (k+1,v), d.iteritems()))  # 例1
{k+1:v for k,v in d.iteritems()}                 # 例2

transform_values(map<K, V1>, function<K, V1, V2>) → MAP<K, V2>

map 型を引数に受け取り、 各エントリーに function を実行した結果でバリューを更新した map 型を返します。

SELECT transform_values(MAP(ARRAY [1, 2, 3], ARRAY [10, 20, 30]), (k, v) -> v + 1)
-- {1 -> 11, 2 -> 22, 3 -> 33}

Python 版

d = dict(zip([1,2,3], [10,20,30]))
dict(map(lambda (k,v): (k,v+1), d.iteritems())) # 例1
{k:v+1 for k,v in d.iteritems()}                # 例2

zip_with(array<T>, array<U>, function<T, U, R>) → array<R>

2組の Array 型を引数に受け取り、両 Array のエレメントが引数の function を実行した結果の Array 型を返します

SELECT zip_with(ARRAY[1, 2], ARRAY[3, 4], (x, y) -> x + y)
-- [4, 6]

Python 版

map(lambda (x,y):x+y, zip([1,2], [3,4])) # 例1
[x+y for x,y in zip([1,2], [3,4])]       # 例2

最後に

今回のLambda式・関数を利用すると ELB のアクセスログに対して Athena を使ってクエリー部分をゴニョゴニョすることも瞬殺です。

SELECT transform(split(url_extract_query('http://example.com/?a=1&b=2&c=3'), '&'), x -> split(x, '='))
-- [[a, 1], [b, 2], [c, 3]]

Amazon Athena の適用範囲が一段と広がりました。

参照