Three valued logic, 혹은 3VL은 참(True), 거짓(False), 그리고 알 수 없음(Unknown) 세 가지 값 중 하나를 갖는 논리 체계를 나타낸다. SQL에서 three valued logic은 데이터베이스 쿼리에서 NULL 값을 다루는 데 사용된다.
참(True) - 조건이 참인 경우에 해당한다.
거짓(False) - 조건이 거짓인 경우에 해당한다.
알 수 없음(Unknown) - 조건에 따라 결과를 알 수 없는 경우에 해당한다. 주로 NULL 값을 나타낸다.
SQL에서 NULL의 의미
SQL에서 NULL은 데이터가 unknown(알 수 없음), unavailable or withheld(이용 불가능하거나 제공되지 않음), not applicable(해당 없음) 등의 상태일 때 사용된다. NULL은 특별한 값을 나타내며, 아래의 각 경우에 대한 의미를 가진다.
unknown(알 수 없음) - 데이터가 존재하지만 값이 알려지지 않았거나 알 수 없는 경우에 NULL을 사용한다. 예를 들어, 어떤 레코드의 특정 열이나 속성값이 아직 결정되지 않았을 때 NULL을 할당할 수 있다.
unavailable or withheld(이용 불가능하거나 제공되지 않음) - 데이터가 이용 불가능하거나 제공되지 않는 경우에 NULL을 사용한다. 예를 들어, 특정 조건을 충족하지 않아 데이터를 제공할 수 없는 경우 NULL을 사용할 수 있다.
not applicable(해당 없음) - 어떤 데이터나 속성이 특정 상황에 적용되지 않는 경우에 NULL을 사용한다. 예를 들어, 날짜 속성이 적용되지 않는 경우 해당 필드를 NULL로 표시할 수 있다.
id
name
birth_date
sex
position
salary
dept_id
1
...
...
...
...
...
...
2
...
...
...
...
...
...
14
...
NULL
...
...
...
...
15
...
NULL
...
...
...
...
위의 테이블에서 id가 14와 15인 임직원이 있을 때 두 명의 생일 데이터가 NULL로 돼있다고 해서 두 데이터가 절대 같다고는 할 수 없다. update가 안되거나 공개가 안되었을 뿐이지 NULL끼리만 저렇게 비교해서는 같은지 같지 않은지는 판단할 수 없다.
그렇다면 위의 쿼리를 실행했을 때 임직원의 아이디가 14와 15인 튜플이 조회가 될까?
SELECT id FROM employee WHERE birth_date = NULL;
정답은 Empty set이 출력될 것이다. NULL을 비교할 때는 '같다(=)'와 같은 비교 연산자를 사용할 수 없다, 왜냐면 앞서 언급했듯이 NULL은 unknown, unavailable 혹은 withheld와 같은 이유로 발생했을 수도 있기 때문에 = 연산자를 이용하여 비교할 수 없다.
그러면 NULL인 데이터는 어떻게 조회를 할까?
SELECT id FROM employee WHERE birth_date IS NULL;
NULL인지 여부를 판단할 때는 위의 쿼리처럼 IS 키워드를 이용하여 비교해야 한다. 그래야 birth_date가 NULL인 튜플을 조회할 수 있다.
똑같이 생일 정보가 NULL이 아닌 튜플을 가져오고 싶다면 IS NOT 키워드를 사용하면 된다.
SELECT * FROM employee WHERE birth_date = '1995-02-01';
또한, 위의 쿼리를 실행하면 테이블에서 1995년 2월 1일생의 튜플을 찾을 것이다. 하지만 임직원 번호 14,15번은 NULL이다. 위의 쿼리의 조건이 NULL과 비교하게 되면 NULL은 1995년 2월 1일이 아니니까 저 2개의 데이터에 대해선 비교 연산의 결과가 FALSE일거라고 생각할 수 있다. 보통 프로그래밍에선 FALSE로 나올 수 있지만 SQL에선 위에서 설명했듯이 NULL의 의미가 여러개를 가질 수 있다(생일 정보를 공개하지 않았거나, 아직 업데이트되지 않았거나, 혹은 아직 알려지지 않았거나). 그래서 실제로 어쩌면 저 2명의 임직원의 생일이 1995년 2월 1 일일수도 있다. 그래서 SQL에선 위와 같은 상황에선 TRUE나 FALSE대신 UNKNOWN(TRUE일 수도 있고 FALSE일 수도 있다.)으로 결과를 반환한다.
따라서 SQL에서 three-valued logic은 비교/논리 연산의 결과로 TRUE, FALSE, UNKNOWN을 가진다
비교 연산 예제
결과
1 = 1
TRUE
1 != 1
FALSE
1 = NULL 1 != NULL 1 > NULL 1 <= NULL
UNKNOWN
표에서 볼 수 있듯이 비교 연산자를 이용해 NULL과 비교를 하게 되면 UNKNOWN을 반환하게 된다.
WHERE절의 condition(s)
WHERE절은 조건을 지정하여 해당 조건을 만족하는 튜플(레코드)만을 결과로 반환하는 역할을 한다. 조건이 TRUE인 경우 해당 튜플이 선택되고, FALSE이거나 UNKNOWN인 경우 해당 튜플은선택되지 않는다. 이는 SQL에서의 three-valued logic에 따른 것이다.
SELECT * FROM employee WHERE birth_date = '1995-02-01';
만약 임원의 생일이 1995년 2월 1일생인 사람이 없다면 FALSE 반환, = NULL 연산자를 사용하면 비교할 수 없으므로 UNKNOWN을 반환한다. 다시 강조하자면 WHERE절에서 FALSE나 UNKNOWN이 반환되면 아무 튜플도 반환하지 않는다.
NOT IN 사용 시 주의 사항
v NOT IN (v1, v2, v3)
위의 NOT IN 연산자의 의미는 아래와 같다.
v != v1 AND v != v2 AND v != v3
v != v1 , v != v2, v != v3 조건 모두 TRUE가 되어야 WHERE절에서 튜플이 반환될 것이다.
하지만 여기서 만약에 v1, v2, v3 중에 하나가 NULL이라면?
아래의 표를 보고 v1, v2, v3중에 하나라고 NULL이라면 어떤 상황이 될지 한 번 예상해 보자.
NOT IN 예제
결과
3 NOT IN (1,2,4)
TRUE
3 NOT IN (1,2,3)
FALSE
3 NOT IN (1,3,NULL)
FALSE
3 NOT IN (1,2,NULL)
UNKNOWN
3번째의 3 NOT IN (1,3 NULL)을 한 번 살펴보자, 3은 괄호 안에 3이 있기 때문에 "3은 괄호 안에 3이 없다"의 결과로 FALSE를 반환하게 된다. 하지만 4번째를 보면 괄호 안에 3이 없다, 그래서 "3은 괄호 안에 3이 없다"라서 TRUE를 반환할 거 같지만 UNKNOWN을 반환하게 된다. 왜 그렇게 되는 걸까?
3 NOT IN (1,2, NULL) 표현은 아래와 같이 풀어서 쓸 수 있다고 하였다.
3 != 1 → TRUE 3 != 2 → TRUE 3 != NULL → UNKNOWN
위에서 NULL은 비교 연산자를 사용해서 비교할 경우 비교할 수 없기 때문에 UNKNOWN을 반환한다고 하였다. 결국에 위의 결과는 TRUE, TRUE, UNKNOWN이 되기 때문에 UNKNOWN을 반환하게 된다.
그럼 방금 배운 내용을 토대로 아래의 문제를 한 번 풀어보자.
문제: 2000년대생이 없는 부서의 ID와 이름을 조회하시오.
위의 문제를 아래와 같은 쿼리로 작성해서 간단하게 풀 수 있을 거 같았다.
SELECT D.id, D.name
FROM department AS D
WHERE D.id NOT IN(
SELECT E.dept_id
FROM employee E
WHERE E.birth_date >= '2000-01-01'
);
하지만 subquery 부분에서 만약에 아직 부서를 배치받지 못해서 E.dept_id 중 일부가 NULL이 있을 수도 있다. 그렇다면 위의 쿼리는 다음과 같을 것이다.
SELECT D.id, D.name
FROM department AS D
WHERE D.id NOT IN(
1,2, ... NULL, NULL, ...
);
따라서 방금 배운 내용을 토대로 위의 결과는 FALSE가 아니라 UNKNOWN이 반환된다는 걸 예측할 수 있다. WHERE절은 TRUE일 때만 튜플을 반환하므로 결국 위의 쿼리는 아무것도 반환을 하지 않을 것이다.
그렇다면 위의 문제를 어떻게 해결할 수 있을까? 아래의 쿼리를 한 번 살펴보자.
SELECT D.id, D.name
FROM department AS D
WHERE D.id NOT IN(
SELECT E.dept_id
FROM employee E
WHERE E.birth_date >= '2000-01-01'
AND E.dept_id IS NOT NULL
);
AND E.dept_id IS NOT NULL 구문을 추가하여 애초에 NULL인 데이터는 가져오지 않기 때문에 subquery의 결과에 NULL이 포함되지 않을 수 있게 하면 된다.