PigでNULLを扱う場合の挙動まとめ
最近、象とか豚と戯れるサファリパークな日々が続いていますが、豚と戯れる時に一番気を付けないといけないのは NULL の扱い方だと思います。
そんなわけで NULL を扱った場合の挙動についてまとめました。
準備
NULLを含むデータとして次のようなデータを使います。
product_id, user_id ってとこですかね。
null_data_sample.tsv
101 100001
100003
101
100001
102 100002
まぁ、こういうデータになんで NULL が含まれてるのかってツッコミたくなりますが!
手元でサクッと試したいのでローカルモードで Grunt を立ち上げてデータをロードします
$ pig -x local
grunt> a = LOAD 'null_data_sample.tsv' AS (product_id:int, user_id:int);
Filter
NULL との比較は IS [NOT] NULL を使用しないとどのような比較を行っても NULL を含むレコードは取り除かれます。
これは SQL と一緒ですね。
product_id が 101 のものだけ抽出される
grunt> b = FILTER a BY product_id == 101;
grunt> DUMP b;
(101,100001)
(101,)
product_id が 101 じゃないものが抽出されるが NULL のものは抽出されない
grunt> c = FILTER a BY product_id != 101;
grunt> DUMP c;
(102,100002)
product_id が NULL のものが抽出される
grunt> d = FILTER a BY product_id IS NULL;
grunt> DUMP d;
(,100003)
(,100001)
product_id が NULL じゃないものが抽出される
grunt> e = FILTER a BY product_id IS NOT NULL;
grunt> DUMP e;
(101,100001)
(101,)
(102,100002)
OR 条件だと次の条件に NULL 判定を入れれば product_id が NULL のものも抽出される
grunt> f = FILTER a BY product_id == 101 OR product_id is null;
(101,100001)
(,100003)
(101,)
(,100001)
Join
Inner Join
Inner Join は結合するキーに NULL が含まれるとそのレコードは取り除かれます。
product_id が NULL のものは取り除かれる
grunt> b = LOAD 'null_data_sample.tsv' AS (product_id:int, user_id:int);
grunt> c = JOIN a BY product_id, b BY product_id;
grunt> DUMP c;
(101,100001,101,100001)
(101,100001,101,)
(101,,101,100001)
(101,,101,)
(102,100002,102,100002)
複合キーの場合、1つでも NULL が含まれると取り除かれる
grunt> d = JOIN a BY (product_id, user_id), b BY (product_id, user_id);
grunt> DUMP d;
(101,100001,101,100001)
(102,100002,102,100002)
Outer Join
Outer Join は結合するキーに NULL が含まれると、NULL 同士であっても別の値として扱われます。
Full Join だと Self Join でも NULL を含むレコードは別のレコードができる
grunt> e = JOIN a BY product_id FULL, b BY product_id;
grunt> DUMP e;
(101,100001,101,100001)
(101,100001,101,)
(101,,101,100001)
(101,,101,)
(102,100002,102,100002)
(,100003,,)
(,100001,,)
(,,,100003)
(,,,100001)
Left Join だと案の定 Self Join でも NULL を含むものは結合されない
grunt> f = JOIN a BY product_id LEFT, b BY product_id;
grunt> DUMP f;
(101,100001,101,100001)
(101,100001,101,)
(101,,101,100001)
(101,,101,)
(102,100002,102,100002)
(,100003,,)
(,100001,,)
複合キーでも同様に NULL は NULL 同士であっても結合されない
grunt> g = JOIN a BY (product_id, user_id) FULL, b BY (product_id, user_id);
grunt> DUMP g;
(101,100001,101,100001)
(101,,,)
(,,101,)
(102,100002,102,100002)
(,100001,,)
(,,,100001)
(,100003,,)
(,,,100003)
Group
結合する場合は NULL 同士が結合されないのに Group だと NULL 同士でグルーピングされます。
grunt> b = GROUP a BY product_id;
grunt> DUMP b;
(101,{(101,100001),(101,)})
(102,{(102,100002)})
(,{(,100003),(,100001)})
Order
ソートする場合は数値でも文字列でも昇順だと NULL が最初に来ます
数値の昇順だと NULL は最初に位置する
grunt> b = ORDER a BY product_id;
grunt> DUMP b;
(,100003)
(,100001)
(101,100001)
(101,)
(102,100002)
当然降順だと NULL は最後に位置する
grunt> c = ORDER a BY product_id DESC;
grunt> DUMP c;
(102,100002)
(101,100001)
(101,)
(,100003)
(,100001)
文字列の昇順でも NULL は最初に位置する
grunt> d = LOAD 'null_data_sample.tsv' AS (product_id:chararray, user_id:int);
grunt> e = ORDER d BY product_id;
(,100003)
(,100001)
(101,100001)
(101,)
(102,100002)
当然文字列でも降順だと NULL は最後に位置する
grunt> f = ORDER d BY product_id DESC;
(102,100002)
(101,100001)
(101,)
(,100003)
(,100001)
Distinct
Distinct は NULL を1種類の値として扱うみたいです。
grunt> b = FOREACH a GENERATE product_id;
grunt> c = DISTINCT b;
grunt> DUMP c;
(101)
(102)
()
四則演算
NULL に対する四則演算は NULL になります。
grunt> b = FOREACH a GENERATE product_id + user_id, product_id - user_id, product_id * user_id, product_id / user_id;
grunt> DUMP b;
(100102,-99900,10100101,0)
(,,,)
(,,,)
(,,,)
(100104,-99900,10200204,0)
三項演算子
三項演算子の場合、NULL に対して NULL 判定以外の比較を行うと結果が NULL になります。
なので、NULL を取り得る値に対して三項演算子を適用する場合は必ず最初に NULL 判定を行わないとネストして三項演算子を適用しても所望の結果が得られないことになります。
NULL 判定をしないと値が NULL のものは返り値も NULL になる。
grunt> b = FOREACH a GENERATE (product_id == 101 ? 'product_id is 101' : 'product_id is not 101');
(product_id is 101)
()
(product_id is 101)
()
(product_id is not 101)
NULL 判定を最初に持ってこないと NULL になる
grunt> c = FOREACH a GENERATE (product_id == 101 ? 'product_id is 101' :
>> (product_id IS NULL ? 'product_id is NULL' : 'product_id is neither 101 nor NULL'));
grunt> DUMP c;
(product_id is 101)
()
(product_id is 101)
()
(product_id is neither 101 nor NULL)
NULL の可能性があれば NULL 判定を最初に行うことで所望の結果が得られる
grunt> d = FOREACH a GENERATE (product_id IS NULL ? 'product_id is NULL' :
>> (product_id == 101 ? 'product_id is 101' : 'product_id is neither 101 nor NULL'));
grunt> DUMP d;
(product_id is 101)
(product_id is NULL)
(product_id is 101)
(product_id is NULL)
(product_id is neither 101 nor NULL)
Filter 同様 OR 条件に NULL 判定を用いてもOK
grunt> e = FOREACH a GENERATE (product_id == 101 OR product_id IS NULL ? 'product_id is 101 or NULL' : 'product_id is neither 101 nor NULL');
grunt> DUMP e;
(product_id is 101 or NULL)
(product_id is 101 or NULL)
(product_id is 101 or NULL)
(product_id is 101 or NULL)
(product_id is neither 101 nor NULL)
というわけで、NULL の扱いには気を付けましょう!