결측 데이터 (Missing Data)

목표

  • 데이터베이스가 어떻게 결측 정보를 표현하는지 설명한다.
  • 결측 정보를 다룰 때, 3개 값을 가진 로직(three-valued logic) 데이터베이스 사용을 설명한다.
  • 결측 정보를 올바르게 처리하는 쿼리를 작성한다.

현실 세계 데이터는 결코 완전하지 않고 구멍은 항상 있다. null로 불리는 특별한 값을 사용하여 데이터베이스는 구멍을 표현한다. null는 0, False, 혹은 빈 문자열도 아니다.“아무것도 없음(nothing here)”을 의미하는 특별한 값이다. null을 다루는 것은 약간 특별한 기교와 신중한 생각을 요구한다.

시작으로 Visited 테이블을 살펴보자. 레코드가 8개 있지만 #752은 날짜가 없다. 혹은 더 정확히 말하면 날짜가 null이다.

%load_ext sqlitemagic
%%sqlite survey.db
select * from Visited;
619 DR-1 1927-02-08
622 DR-1 1927-02-10
734 DR-3 1939-01-07
735 DR-3 1930-01-12
751 DR-3 1930-02-26
752 DR-3 None
837 MSK-4 1932-01-14
844 DR-1 1932-03-22

Null 다른 값과는 다르게 동작한다. 만약 1930년 이전 레코드를 선택한다면,

%%sqlite survey.db
select * from Visited where dated<'1930-00-00';
619 DR-1 1927-02-08
622 DR-1 1927-02-10

결과 2개를 얻게 되고, 만약 1930년 동안 혹은 이후 레코드를 선택한다면,

%%sqlite survey.db
select * from Visited where dated>='1930-00-00';
734 DR-3 1939-01-07
735 DR-3 1930-01-12
751 DR-3 1930-02-26
837 MSK-4 1932-01-14
844 DR-1 1932-03-22

결과를 5개 얻게되지만, 레코드 #752은 결과값 어디에도 존재하지 않는다. 이유는 null<'1930-00-00' 평가결과가 참도 거짓도 아니기 때문이다. null 이 의미하는 것은 “알수가 없다”는 것이다. 그리고 만약 비교 평가식의 왼쪽편 값을 알지 못한다면, 비교도 참인지 거짓인지 알수가 없다. 데이터베이스는 “알 수 없음”을 null로 표현하기 때문에, null<'1930-00-00'의 값도 사실 null이다. null>='1930-00-00'도 또한 null인데 왜냐하면 질문에 답을 할 수 없기 때문이다. 그리고, where절에 레코드는 테스트가 참인 것만 있기 때문에 레코드 #752은 어느 결과값에도 포함되지 않게 된다.

평가식만 null값을 이와 같은 방식으로 다루는 연산자는 아니다. 1+nullnull이고, 5*nullnull이고, log(null)null이 된다. 특히, 무언가를 = 과 != 으로 null과 비교하는 것도 null이 된다.

%%sqlite survey.db
select * from Visited where dated=NULL;
%%sqlite survey.db
select * from Visited where dated!=NULL;

null 인지 아닌지를 점검하기 위해서, 특별한 테스트 is null을 사용해야 한다.

%%sqlite survey.db
select * from Visited where dated is NULL;
752 DR-3 None

혹은, 역으로는 is not null을 사용한다.

%%sqlite survey.db
select * from Visited where dated is not NULL;
619 DR-1 1927-02-08
622 DR-1 1927-02-10
734 DR-3 1939-01-07
735 DR-3 1930-01-12
751 DR-3 1930-02-26
837 MSK-4 1932-01-14
844 DR-1 1932-03-22

null 값은 나타나는 곳마다 두통을 일으킨다. 예를 들어, Dyer가 측정하지 않은 모든 염분 정보를 찾는다고 가정하자. 다음과 같이 쿼리를 작성하는 것은 당연하다.

%%sqlite survey.db
select * from Survey where quant='sal' and person!='lake';
619 dyer sal 0.13
622 dyer sal 0.09
752 roe sal 41.6
837 roe sal 22.5

하지만, 상기 쿼리 필터는 누가 측정을 했는지 모르는 레코드는 빠뜨린다. 다시 한번, 이유는 personnull일 때, !=비교는 null값을 만들어서 레코드가 결과값에 있지 않게 된다. 만약 이런 레코드도 유지하려고 한다면, 명시적으로 검사를 추가할 필요가 있다.

%%sqlite survey.db
select * from Survey where quant='sal' and (person!='lake' or person is null);
619 dyer sal 0.13
622 dyer sal 0.09
735 None sal 0.06
752 roe sal 41.6
837 roe sal 22.5

여전히 이러한 접근법이 맞는 것인지 아닌 것인지 판단할 필요가 있다. 만약 절대적으로 결과에 Lake가 측정한 어떠한 값도 포함하지 않는다고 확신한다면, 누가 작업을 한 것인지 모르는 모든 레코드를 제외할 필요가 있다.

도전 과제

  1. 날짜가 알려지지 않은 (즉 null) 항목은 빼고, 날짜 순으로 Visited 테이블에 있는 레코드를 정렬한 쿼리를 작성하세요.

  2. 다음 쿼리가 무슨 결과를 할까요?

    select * from Visited where dated in ('1927-02-08', null);

    상기 쿼리가 실질적으로 무엇을 생기게 할까요?

  3. 몇몇 데이터베이스 디자이너는 null 보다 결측 데이터를 표기하기 위해서 보초값(sentinel value)를 사용한다. 예를 들어, 결측 날짜를 표기하기 위해서 “0000-00-00” 날짜를 사용하거나 결측 염분치 혹은 결측 방사선 측정값을 표기하기 위해서 -1.0을 사용한다. (왜냐하면 실제 측정값이 음수가 될 수 없기 때문이다.) 이러한 접근법은 무엇을 단순화할까요? 이러한 접근법이 어떤 부담과 위험을 가져올까요?

주요점

  • 데이터베이스는 결측 정보를 표현하기 위해서 null을 사용한다.
  • null이 관계되는 산술 혹은 불 연산 결과도 null이다.
  • null과 함께 안전하세 사용될 수 있는 유일한 연산자는 is nullis not null이다.