NULL 의미론

NULL 의미론

테이블은 행 집합으로 구성되고, 각 행은 컬럼 집합을 포함해요. 컬럼은 데이터 타입과 연결되며 엔티티의 특정 속성을 나타내요(예: ageperson이라는 엔티티의 컬럼). 때때로 특정 행의 컬럼 값은 그 행이 생겨날 때 알려지지 않을 수 있어요. SQL에서 그런 값은 NULL로 표현돼요. 이 섹션은 다양한 연산자, 표현식, 기타 SQL 구조에서 NULL 값을 처리하는 의미론을 자세히 설명해요.

다음은 person이라는 테이블의 스키마 레이아웃과 데이터를 보여줘요. 데이터는 age 컬럼에 NULL 값을 포함하며, 이 테이블은 아래 섹션의 다양한 예시에서 사용될 거예요.

TABLE: person

Id Name 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-safe 동등 연산자(<=>)를 제공하는데, 이는 피연산자 중 하나가 NULL이면 False를 반환하고 둘 다 NULL이면 True를 반환해요. 다음 표는 하나 또는 둘 다 NULL일 때 비교 연산자의 동작을 보여줘요.

왼쪽 피연산자 오른쪽 피연산자 > >= = < <= <=>
NULL 아무 값 NULL NULL NULL NULL NULL False
아무 값 NULL NULL NULL NULL NULL NULL False
NULL NULL NULL NULL NULL NULL NULL True

예시

-- 일반 비교 연산자는 피연산자 중 하나가 `NULL`이면 `NULL`을 반환해요.
SELECT 5 > null AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

-- 일반 비교 연산자는 두 피연산자가 모두 `NULL`이면 `NULL`을 반환해요.
SELECT null = null AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

-- null-safe 동등 연산자는 피연산자 중 하나가 `NULL`이면 `False`를 반환해요
SELECT 5 <=> null AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|            false|
+-----------------+

-- null-safe 동등 연산자는 피연산자 중 하나가 `NULL`이면 `True`를 반환해요
SELECT NULL <=> NULL;
+-----------------+
|expression_output|
+-----------------+
|             true|
+-----------------+

논리 연산자

Spark는 AND, OR, NOT 같은 표준 논리 연산자를 지원해요. 이 연산자들은 Boolean 표현식을 인자로 받고 Boolean 값을 반환해요.

다음 표는 하나 또는 둘 다 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

예시

-- 일반 비교 연산자는 피연산자 중 하나가 `NULL`이면 `NULL`을 반환해요.
SELECT (true OR null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             true|
+-----------------+

-- 일반 비교 연산자는 두 피연산자가 모두 `NULL`이면 `NULL`을 반환해요.
SELECT (null OR false) AS expression_output
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

-- null-safe 동등 연산자는 피연산자 중 하나가 `NULL`이면 `False`를 반환해요
SELECT NOT(null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

표현식

비교 연산자와 논리 연산자는 Spark에서 표현식으로 취급돼요. 이 두 종류 외에도 Spark는 함수 표현식, 형변환(cast) 표현식 등 다른 형태의 표현식을 지원해요. Spark의 표현식은 크게 분류하면:

  • NULL 비허용 표현식

  • NULL 값 피연산자를 처리할 수 있는 표현식

  • 이 표현식들의 결과는 표현식 자체에 따라 달라져요.

NULL 비허용 표현식

NULL 비허용 표현식은 표현식의 인자 중 하나 이상이 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을 반환해요. 아래는 이 범주에 속하는 표현식의 불완전한 목록이에요.

  • COALESCE
  • NULLIF
  • IFNULL
  • NVL
  • NVL2
  • ISNAN
  • NANVL
  • ISNULL
  • ISNOTNULL
  • ATLEASTNNONNULLS
  • IN

예시

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

-- `NULL`이 아닌 값의 첫 번째 발생을 반환해요.
SELECT coalesce(null, null, 3, null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|                3|
+-----------------+

-- 모든 피연산자가 `NULL`이므로 `NULL`을 반환해요.
SELECT coalesce(null, null, null, null) AS expression_output;
+-----------------+
|expression_output|
+-----------------+
|             null|
+-----------------+

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

내장 집계 표현식

집계 함수는 입력 행 집합을 처리해 단일 결과를 계산해요. 아래는 NULL 값이 집계 함수에서 처리되는 규칙이에요.

  • 모든 집계 함수는 NULL 값을 처리에서 제외해요.

  • 이 규칙의 유일한 예외는 COUNT(*) 함수예요.

  • 일부 집계 함수는 모든 입력 값이 NULL이거나 입력 데이터 집합이 비어 있으면 NULL을 반환해요. 이 함수들의 목록은:

  • MAX

  • MIN

  • SUM

  • AVG

  • EVERY

  • ANY

  • SOME

예시

-- `count(*)`는 `NULL` 값을 건너뛰지 않아요.
SELECT count(*) FROM person;
+--------+
|count(1)|
+--------+
|       7|
+--------+

-- `age` 컬럼의 `NULL` 값은 처리에서 제외돼요.
SELECT count(age) FROM person;
+----------+
|count(age)|
+----------+
|         5|
+----------+

-- 빈 입력 집합에 대한 `count(*)`는 0을 반환해요. 이는 `max` 같은
-- 다른 집계 함수가 `NULL`을 반환하는 것과는 달라요.
SELECT count(*) FROM person where 1 = 0;
+--------+
|count(1)|
+--------+
|       0|
+--------+

-- `NULL` 값은 최댓값 계산에서 제외돼요.
SELECT max(age) FROM person;
+--------+
|max(age)|
+--------+
|      50|
+--------+

-- `max`는 빈 입력 집합에서 `NULL`을 반환해요.
SELECT max(age) FROM person where 1 = 0;
+--------+
|max(age)|
+--------+
|    null|
+--------+

WHERE, HAVING, JOIN 절의 조건 표현식

WHERE, HAVING 연산자는 사용자가 지정한 조건에 따라 행을 필터링해요. JOIN 연산자는 조인 조건에 따라 두 테이블의 행을 결합하는 데 사용돼요. 세 연산자 모두에서 조건 표현식은 불리언 표현식이며 True, False, 또는 알 수 없음(NULL)을 반환할 수 있어요. 조건 결과가 True이면 "충족됨(satisfied)"으로 간주돼요.

예시

-- 나이가 알 수 없는(`NULL`) 사람은 결과 집합에서 걸러져요.
SELECT * FROM person WHERE age > 0;
+--------+---+
|    name|age|
+--------+---+
|Michelle| 30|
|    Fred| 50|
|    Mike| 18|
|     Dan| 50|
|     Joe| 30|
+--------+---+

-- `IS NULL` 표현식을 논리합으로 사용해 알 수 없는(`NULL`) 기록을 가진
-- 사람을 선택해요.
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|
+--------+----+

-- 나이가 알 수 없는(`NULL`) 사람은 처리에서 제외돼요.
SELECT age, count(*) FROM person GROUP BY age HAVING max(age) > 18;
+---+--------+
|age|count(1)|
+---+--------+
| 50|       2|
| 30|       2|
+---+--------+

-- 조인 조건 `p1.age = p2.age AND p1.name = p2.name`인 셀프 조인 사례.
-- 나이가 알 수 없는(`NULL`) 사람은 조인 연산자에 의해 걸러져요.
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|
+--------+---+--------+---+

-- 조인의 양쪽 다리의 age 컬럼이 null-safe 동등으로 비교되므로
-- 나이가 알 수 없는(`NULL`) 사람이 조인에 합격해요.
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)

이전 섹션 비교 연산자에서 논의했듯이, 두 NULL 값은 같지 않아요. 그러나 그룹화와 distinct 처리의 목적에서는 NULL 데이터를 가진 두 개 이상의 값이 같은 버킷으로 그룹화돼요. 이 동작은 SQL 표준 및 다른 엔터프라이즈 데이터베이스 관리 시스템과 일치해요.

예시

-- `NULL` 값은 `GROUP BY` 처리에서 한 버킷에 들어가요.
SELECT age, count(*) FROM person GROUP BY age;
+----+--------+
| age|count(1)|
+----+--------+
|null|       2|
|  50|       2|
|  30|       2|
|  18|       1|
+----+--------+

-- 모든 `NULL` 나이는 `DISTINCT` 처리에서 하나의 distinct 값으로 간주돼요.
SELECT DISTINCT age FROM person;
+----+
| age|
+----+
|null|
|  50|
|  30|
|  18|
+----+

정렬 연산자(ORDER BY 절)

Spark SQL은 ORDER BY 절에서 null 정렬 지정을 지원해요. Spark는 ORDER BY 절을 처리할 때 null 정렬 지정에 따라 모든 NULL 값을 처음 또는 마지막에 배치해요. 기본적으로 모든 NULL 값은 처음에 배치돼요.

예시

-- `NULL` 값이 처음에 표시되고 다른 값들은
-- 오름차순으로 정렬돼요.
SELECT age, name FROM person ORDER BY age;
+----+--------+
| age|    name|
+----+--------+
|null|   Marry|
|null|  Albert|
|  18|    Mike|
|  30|Michelle|
|  30|     Joe|
|  50|    Fred|
|  50|     Dan|
+----+--------+

-- `NULL`이 아닌 컬럼 값들은 오름차순으로 정렬되고
-- `NULL` 값은 마지막에 표시돼요.
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|
+----+--------+

-- `NULL`이 아닌 컬럼 값들은 내림차순으로 정렬되고
-- `NULL` 값은 마지막에 표시돼요.
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-safe 방식으로 동등성을 비교해요. 즉, 행을 비교할 때 일반 EqualTo(=) 연산자와 달리 두 NULL 값은 같다고 간주돼요.

예시

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

-- `INTERSECT`의 두 다리 사이의 공통 행만 결과 집합에 있어요.
-- 행의 컬럼 사이 비교는 null-safe 방식으로 수행돼요.
SELECT name, age FROM person
    INTERSECT
    SELECT name, age from unknown_age;
+------+----+
|  name| age|
+------+----+
|Albert|null|
| Marry|null|
+------+----+

-- `EXCEPT`의 두 다리에서 온 `NULL` 값은 출력에 없어요.
-- 이는 비교가 null-safe 방식으로 일어난다는 것을 보여줘요.
SELECT age, name FROM person
    EXCEPT
    SELECT age FROM unknown_age;
+---+--------+
|age|    name|
+---+--------+
| 30|     Joe|
| 50|    Fred|
| 30|Michelle|
| 18|    Mike|
| 50|     Dan|
+---+--------+

-- 두 데이터 집합 사이에 `UNION` 연산을 수행해요.
-- 행의 컬럼 사이 비교는 null-safe 방식으로 수행돼요.
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는 멤버십 조건으로, 참조하는 서브쿼리가 하나 이상의 행을 반환하면 TRUE를 반환해요. 마찬가지로 NOT EXISTS는 비멤버십 조건으로, 서브쿼리에서 행이 반환되지 않거나 0개 행이 반환되면 TRUE를 반환해요.

이 두 표현식은 서브쿼리 결과에 NULL이 있어도 영향을 받지 않아요. 일반적으로 null 인식을 위한 특별한 처리 없이 세미조인(semijoin)/안티세미조인(anti-semijoin)으로 변환될 수 있기 때문에 더 빠르게 동작해요.

예시

-- 서브쿼리가 `NULL` 값을 가진 행을 만들더라도, 서브쿼리가 1행을 만들므로
-- `EXISTS` 표현식은 `TRUE`로 평가돼요.
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` 표현식은 `FALSE`를 반환해요. 서브쿼리가 행을 만들지 않을 때만
-- `TRUE`를 반환해요. 이 경우에는 1행을 반환해요.
SELECT * FROM person WHERE NOT EXISTS (SELECT null);
+----+---+
|name|age|
+----+---+
+----+---+

-- `NOT EXISTS` 표현식은 `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에서 INNOT IN 표현식은 쿼리의 WHERE 절 안에서 허용돼요. EXISTS 표현식과 달리 IN 표현식은 TRUE, FALSE, 또는 UNKNOWN (NULL) 값을 반환할 수 있어요. 개념적으로 IN 표현식은 논리합 연산자(OR)로 구분된 일련의 동등 조건과 의미적으로 동일해요. 예를 들어 c1 IN (1, 2, 3)은 의미적으로 (C1 = 1 OR c1 = 2 OR c1 = 3)과 동일해요.

NULL 값 처리를 다루는 한, 의미론은 비교 연산자(=)와 논리 연산자(OR)의 NULL 값 처리에서 유추할 수 있어요. 요약하면, IN 표현식의 결과를 계산하는 규칙은 다음과 같아요.

  • 문제의 NULL이 아닌 값이 목록에서 발견되면 TRUE가 반환돼요
  • NULL이 아닌 값이 목록에 없고 목록에 NULL 값도 없으면 FALSE가 반환돼요
  • 값이 NULL이거나, NULL이 아닌 값이 목록에 없고 목록에 NULL 값이 하나 이상 있으면 UNKNOWN이 반환돼요

NOT IN은 입력 값에 관계없이 목록에 NULL이 있으면 항상 UNKNOWN을 반환해요. 이는 값이 NULL을 포함한 목록에 없으면 IN이 UNKNOWN을 반환하고, NOT UNKNOWN이 다시 UNKNOWN이기 때문이에요.

예시

-- 서브쿼리는 결과 집합에 `NULL` 값만 있어요. 따라서
-- `IN` 술어의 결과는 UNKNOWN이에요.
SELECT * FROM person WHERE age IN (SELECT null);
+----+---+
|name|age|
+----+---+
+----+---+

-- 서브쿼리는 결과 집합에 `NULL` 값과 유효한 값 `50`을 함께 가지고 있어요.
-- age = 50인 행이 반환돼요.
SELECT * FROM person
    WHERE age IN (SELECT age FROM VALUES (50), (null) sub(age));
+----+---+
|name|age|
+----+---+
|Fred| 50|
| Dan| 50|
+----+---+

-- 서브쿼리가 결과 집합에 `NULL` 값을 가지므로 `NOT IN`
-- 술어는 UNKNOWN을 반환해요. 따라서 이 쿼리에 합격하는 행은 없어요.
SELECT * FROM person
    WHERE age NOT IN (SELECT age FROM VALUES (50), (null) sub(age));
+----+---+
|name|age|
+----+---+
+----+---+

출처: 문서

더 알아보기 (Learn more)