NULL セマンティクス

テーブルは行のセットで構成され、各行は列のセットを含みます。列はデータ型に関連付けられており、エンティティの特定の属性を表します(例:ageperson というエンティティの列です)。場合によっては、行の特定の列の値は、行が存在する時点では不明です。SQL では、このような値は NULL として表されます。このセクションでは、さまざまな演算子、式、およびその他の SQL 構造における NULL 値の処理セマンティクスについて詳しく説明します。

以下は、person という名前のテーブルのスキーマレイアウトとデータを示しています。データには age 列に NULL 値が含まれており、このテーブルは以下のセクションのさまざまな例で使用されます。

TABLE: person

Id 名前 Age
100 Joe 30
200 Marry NULL
300 Mike 18
400 Fred 50
500 Albert NULL
600 Michelle 30
700 Dan 50

比較演算子

Apache Spark は、>>==<<= などの標準的な比較演算子をサポートしています。これらの演算子の結果は、オペランドのいずれかまたは両方が不明または NULL の場合、不明または NULL になります。等価性のために NULL 値を比較するために、Spark は NULL セーフ等価演算子(<=>)を提供します。これは、オペランドのいずれかが NULL の場合は False を返し、両方のオペランドが NULL の場合は True を返します。次の表は、1 つまたは両方のオペランドが NULL の場合の比較演算子の動作を示しています。

左オペランド 右オペランド > >= = < <= <=>
NULL 任意の値 NULL NULL NULL NULL NULL False
任意の値 NULL NULL NULL NULL NULL NULL False
NULL NULL NULL NULL NULL NULL NULL True

-- Normal comparison operators return `NULL` when one of the operand is `NULL`.
SELECT 5 > null AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

-- Normal comparison operators return `NULL` when both the operands are `NULL`.
SELECT null = null AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

-- Null-safe equal operator return `False` when one of the operand is `NULL`
SELECT 5 <=> null AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|            false|
+-----------------+

-- Null-safe equal operator return `True` when one of the operand is `NULL`
SELECT NULL <=> NULL;
+-----------------+
|expression_output|
+-----------------+
|             true|
+-----------------+

論理演算子

Spark は、ANDORNOT などの標準的な論理演算子をサポートしています。これらの演算子は Boolean 式を引数として受け取り、Boolean 値を返します。

次の表は、1 つまたは両方のオペランドが NULL の場合の論理演算子の動作を示しています。

左オペランド 右オペランド OR AND
True NULL True NULL
False NULL NULL False
NULL True True NULL
NULL False NULL False
NULL NULL NULL NULL
オペランド NOT
NULL NULL

-- Normal comparison operators return `NULL` when one of the operands is `NULL`.
SELECT (true OR null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             true|
+-----------------+

-- Normal comparison operators return `NULL` when both the operands are `NULL`.
SELECT (null OR false) AS expression_output
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

-- Null-safe equal operator returns `False` when one of the operands is `NULL`
SELECT NOT(null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

比較演算子と論理演算子は、Spark では式として扱われます。これら 2 種類の式以外に、Spark は関数式、キャスト式などの他の形式の式もサポートしています。Spark の式は、大きく次のように分類できます。

NULL 不許容式

NULL 不許容式は、式に 1 つ以上の引数が NULL の場合に NULL を返します。ほとんどの式はこのカテゴリに属します。

SELECT concat('John', null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

SELECT positive(null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

SELECT to_date(null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

NULL 値オペランドを処理できる式

このクラスの式は、NULL 値を処理するように設計されています。式の結果は、式自体に依存します。たとえば、関数式 isnull は、NULL 入力に対して true を返し、非 NULL 入力に対して false を返しますが、関数 coalesce は、オペランドのリストで最初の非 NULL 値を返します。ただし、coalesce は、すべてのオペランドが NULL の場合に NULL を返します。以下は、このカテゴリの式の不完全なリストです。

SELECT isnull(null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             true|
+-----------------+

-- Returns the first occurrence of non `NULL` value.
SELECT coalesce(null, null, 3, null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|                3|
+-----------------+

-- Returns `NULL` as all its operands are `NULL`. 
SELECT coalesce(null, null, null, null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

SELECT isnan(null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|            false|
+-----------------+

組み込み集計式

集計関数は、入力行のセットを処理することで単一の結果を計算します。以下は、集計関数が NULL 値をどのように処理するかについてのルールです。

-- `count(*)` does not skip `NULL` values.
SELECT count(*) FROM person;
+--------+
|count(1)|
+--------+
|       7|
+--------+

-- `NULL` values in column `age` are skipped from processing.
SELECT count(age) FROM person;
+----------+
|count(age)|
+----------+
|         5|
+----------+

-- `count(*)` on an empty input set returns 0. This is unlike the other
-- aggregate functions, such as `max`, which return `NULL`.
SELECT count(*) FROM person where 1 = 0;
+--------+
|count(1)|
+--------+
|       0|
+--------+

-- `NULL` values are excluded from computation of maximum value.
SELECT max(age) FROM person;
+--------+
|max(age)|
+--------+
|      50|
+--------+

-- `max` returns `NULL` on an empty input set.
SELECT max(age) FROM person where 1 = 0;
+--------+
|max(age)|
+--------+
|    null|
+--------+

WHERE、HAVING、JOIN 句の条件式

WHEREHAVING 演算子は、ユーザー指定の条件に基づいて行をフィルタリングします。JOIN 演算子は、結合条件に基づいて 2 つのテーブルから行を結合するために使用されます。これら 3 つの演算子すべてについて、条件式はブール式であり、TrueFalse、または Unknown (NULL) を返す可能性があります。条件の結果が True の場合、「満たされる」と見なされます。

-- Persons whose age is unknown (`NULL`) are filtered out from the result set.
SELECT * FROM person WHERE age > 0;
+--------+---+
|    name|age|
+--------+---+
|Michelle| 30|
|    Fred| 50|
|    Mike| 18|
|     Dan| 50|
|     Joe| 30|
+--------+---+

-- `IS NULL` expression is used in disjunction to select the persons
-- with unknown (`NULL`) records.
SELECT * FROM person WHERE age > 0 OR age IS NULL;
+--------+----+
|    name| age|
+--------+----+
|  Albert|null|
|Michelle|  30|
|    Fred|  50|
|    Mike|  18|
|     Dan|  50|
|   Marry|null|
|     Joe|  30|
+--------+----+

-- Person with unknown(`NULL`) ages are skipped from processing.
SELECT age, count(*) FROM person GROUP BY age HAVING max(age) > 18;
+---+--------+
|age|count(1)|
+---+--------+
| 50|       2|
| 30|       2|
+---+--------+

-- A self join case with a join condition `p1.age = p2.age AND p1.name = p2.name`.
-- The persons with unknown age (`NULL`) are filtered out by the join operator.
SELECT * FROM person p1, person p2
    WHERE p1.age = p2.age
    AND p1.name = p2.name;
+--------+---+--------+---+
|    name|age|    name|age|
+--------+---+--------+---+
|Michelle| 30|Michelle| 30|
|    Fred| 50|    Fred| 50|
|    Mike| 18|    Mike| 18|
|     Dan| 50|     Dan| 50|
|     Joe| 30|     Joe| 30|
+--------+---+--------+---+

-- The age column from both legs of join are compared using null-safe equal which
-- is why the persons with unknown age (`NULL`) are qualified by the join.
SELECT * FROM person p1, person p2
    WHERE p1.age <=> p2.age
    AND p1.name = p2.name;
+--------+----+--------+----+
|    name| age|    name| age|
+--------+----+--------+----+
|  Albert|null|  Albert|null|
|Michelle|  30|Michelle|  30|
|    Fred|  50|    Fred|  50|
|    Mike|  18|    Mike|  18|
|     Dan|  50|     Dan|  50|
|   Marry|null|   Marry|null|
|     Joe|  30|     Joe|  30|
+--------+----+--------+----+

集計演算子 (GROUP BY、DISTINCT)

前のセクションの 比較演算子 で説明したように、2 つの NULL 値は等しくありません。ただし、グループ化と一意の処理の目的で、NULL データを持つ 2 つ以上の値は同じバケットにグループ化されます。この動作は、SQL 標準およびその他のエンタープライズデータベース管理システムと互換性があります。

-- `NULL` values are put in one bucket in `GROUP BY` processing.
SELECT age, count(*) FROM person GROUP BY age;
+----+--------+
| age|count(1)|
+----+--------+
|null|       2|
|  50|       2|
|  30|       2|
|  18|       1|
+----+--------+

-- All `NULL` ages are considered one distinct value in `DISTINCT` processing.
SELECT DISTINCT age FROM person;
+----+
| age|
+----+
|null|
|  50|
|  30|
|  18|
+----+

ソート演算子 (ORDER BY 句)

Spark SQL は、ORDER BY 句での NULL オーダリング指定をサポートしています。Spark は、NULL オーダリング指定に応じて、すべての NULL 値を最初に配置するか最後に配置するかによって ORDER BY 句を処理します。デフォルトでは、すべての NULL 値が最初に配置されます。

-- `NULL` values are shown at first and other values
-- are sorted in ascending way.
SELECT age, name FROM person ORDER BY age;
+----+--------+
| age|    name|
+----+--------+
|null|   Marry|
|null|  Albert|
|  18|    Mike|
|  30|Michelle|
|  30|     Joe|
|  50|    Fred|
|  50|     Dan|
+----+--------+

-- Column values other than `NULL` are sorted in ascending
-- way and `NULL` values are shown at the last.
SELECT age, name FROM person ORDER BY age NULLS LAST;
+----+--------+
| age|    name|
+----+--------+
|  18|    Mike|
|  30|Michelle|
|  30|     Joe|
|  50|     Dan|
|  50|    Fred|
|null|   Marry|
|null|  Albert|
+----+--------+

-- Columns other than `NULL` values are sorted in descending
-- and `NULL` values are shown at the last.
SELECT age, name FROM person ORDER BY age DESC NULLS LAST;
+----+--------+
| age|    name|
+----+--------+
|  50|    Fred|
|  50|     Dan|
|  30|Michelle|
|  30|     Joe|
|  18|    Mike|
|null|   Marry|
|null|  Albert|
+----+--------+

セット演算子 (UNION、INTERSECT、EXCEPT)

NULL 値は、セット演算のコンテキストでの等価性のために NULL セーフな方法で比較されます。これは、行を比較するとき、2 つの NULL 値は、通常の EqualTo(=) 演算子とは異なり、等しいと見なされることを意味します。

CREATE VIEW unknown_age SELECT * FROM person WHERE age IS NULL;

-- Only common rows between two legs of `INTERSECT` are in the 
-- result set. The comparison between columns of the row are done
-- in a null-safe manner.
SELECT name, age FROM person
    INTERSECT
    SELECT name, age from unknown_age;
+------+----+
|  name| age|
+------+----+
|Albert|null|
| Marry|null|
+------+----+

-- `NULL` values from two legs of the `EXCEPT` are not in output. 
-- This basically shows that the comparison happens in a null-safe manner.
SELECT age, name FROM person
    EXCEPT
    SELECT age FROM unknown_age;
+---+--------+
|age|    name|
+---+--------+
| 30|     Joe|
| 50|    Fred|
| 30|Michelle|
| 18|    Mike|
| 50|     Dan|
+---+--------+

-- Performs `UNION` operation between two sets of data. 
-- The comparison between columns of the row ae done in
-- null-safe manner.
SELECT name, age FROM person
    UNION 
    SELECT name, age FROM unknown_age;
+--------+----+
|    name| age|
+--------+----+
|  Albert|null|
|     Joe|  30|
|Michelle|  30|
|   Marry|null|
|    Fred|  50|
|    Mike|  18|
|     Dan|  50|
+--------+----+

EXISTS/NOT EXISTS サブクエリ

Spark では、EXISTS および NOT EXISTS 式は WHERE 句内で許可されます。これらは、TRUE または FALSE のいずれかを返すブール式です。言い換えると、EXISTS はメンバーシップ条件であり、参照するサブクエリが 1 つ以上の行を返すと TRUE を返します。同様に、NOT EXISTS は非メンバーシップ条件であり、サブクエリから行が返されないかゼロ行が返されると TRUE を返します。

これらの 2 つの式は、サブクエリの結果に NULL が存在することによって影響を受けません。NULL 認識のための特別なプロビジョニングなしでセミジョイン/アンチセミジョインに変換できるため、通常は高速です。

-- Even if subquery produces rows with `NULL` values, the `EXISTS` expression
-- evaluates to `TRUE` as the subquery produces 1 row.
SELECT * FROM person WHERE EXISTS (SELECT null);
+--------+----+
|    name| age|
+--------+----+
|  Albert|null|
|Michelle|  30|
|    Fred|  50|
|    Mike|  18|
|     Dan|  50|
|   Marry|null|
|     Joe|  30|
+--------+----+

-- `NOT EXISTS` expression returns `FALSE`. It returns `TRUE` only when
-- subquery produces no rows. In this case, it returns 1 row.
SELECT * FROM person WHERE NOT EXISTS (SELECT null);
+----+---+
|name|age|
+----+---+
+----+---+

-- `NOT EXISTS` expression returns `TRUE`.
SELECT * FROM person WHERE NOT EXISTS (SELECT 1 WHERE 1 = 0);
+--------+----+
|    name| age|
+--------+----+
|  Albert|null|
|Michelle|  30|
|    Fred|  50|
|    Mike|  18|
|     Dan|  50|
|   Marry|null|
|     Joe|  30|
+--------+----+

IN/NOT IN サブクエリ

Spark では、IN および NOT IN 式は、クエリの WHERE 句内で許可されます。EXISTS 式とは異なり、IN 式は TRUEFALSE、または UNKNOWN (NULL) の値を返すことができます。概念的には、IN 式は、否定演算子 (OR) で区切られた一連の等価条件と意味的に同等です。たとえば、c1 IN (1, 2, 3) は (C1 = 1 OR c1 = 2 OR c1 = 3) と意味的に同等です。

NULL 値の処理に関しては、セマンティクスは比較演算子 (=) および論理演算子 (OR) の NULL 値の処理から推測できます。要約すると、IN 式の結果を計算するためのルールは次のとおりです。

NOT IN は、リストに NULL が含まれている場合、入力値に関係なく常に UNKNOWN を返します。これは、IN が NULL を含むリストに値がない場合に UNKNOWN を返し、NOT UNKNOWN は再び UNKNOWN であるためです。

-- The subquery has only `NULL` value in its result set. Therefore,
-- the result of `IN` predicate is UNKNOWN.
SELECT * FROM person WHERE age IN (SELECT null);
+----+---+
|name|age|
+----+---+
+----+---+

-- The subquery has `NULL` value in the result set as well as a valid 
-- value `50`. Rows with age = 50 are returned. 
SELECT * FROM person
    WHERE age IN (SELECT age FROM VALUES (50), (null) sub(age));
+----+---+
|name|age|
+----+---+
|Fred| 50|
| Dan| 50|
+----+---+

-- Since subquery has `NULL` value in the result set, the `NOT IN`
-- predicate would return UNKNOWN. Hence, no rows are
-- qualified for this query.
SELECT * FROM person
    WHERE age NOT IN (SELECT age FROM VALUES (50), (null) sub(age));
+----+---+
|name|age|
+----+---+
+----+---+