이력데이터의 문제점

이력을 나타내는 데이터는 매우 자주 쓰이며 현장에서도 빈번히 관계형 데이터베이스를 이용해 다룬다. 이력 데이터가 없는 응용 프로그램은 존재하지 않는다고 본다.

이력 데이터는 본질적으로 관계형 데이터베이스와 궁합이 좋지 않다.

이력과 관계형 모델의 상성 문제

릴레이션은 집합이다. 따라서 각 요소 사이에 순서가 없다. 그러나 이력은 오래된 것인지 새로운 것인지에 대한 순서가 있다. 이는 관계형 모델로 다룰 수 없다.

또한 이력 데이터는 쉽게 테이블이 커진다. 테이블이 커지면 성능이 나오지 않는 문제가 발생한다.

릴레이션과 시간축의 직교성

이력 데이터는 시간축과 릴레이션이 직교하지 않는다는 점이 문제다. 요점은 시간에 따라 쿼리의 실행 결과가 변한다는 것이다.

시간축과 직교하지 않는 것은 릴레이션이라고 할 수 없다.

NULL의 가능성

어떤 이력의 시작일과 종료일이 기록되고 가장 새로운 가격 항목에는 end_date에 9999-12-31이라고 설정되어 있다. 이는 실질적으로 기한이 설정 돼 있지 않은 것과 같고 설계자의 선택에 따라서는 이 값 대신 NULL이 될 수도 있을 것이다. end_date가 NULL이 되면 검색 조건은 IS NULL을 사용하도록 변경해야한다.

SELECT price
FROM price_list
WHERE item = '턱걸이 기계'
AND NOW() BETWEEN start_date AND end_date
OR (start_date >= NOW() AND end_date IS NULL)

쿼리 조건이 복잡해지고 3VL을 다루게 되었다.

특정 행만 의미가 다르다

설계에 따라서 start_date만 있는 경우도 있을 것이다. 이러한 테이블에서 턱걸이 기계의 가격을 구해본다

SELECT price
FROM price_list
WHERE item = '턱걸이 기계'
AND start_date = (
	SELECT max(start_date)
    FROM price_list
    WHERE item = '턱걸이 기계'
)

이 쿼리는 관계형 모델에서 벗어난다.

MAX()라는 집계함수를 사용하고 있지만, 집계는 관계형 연산이 아니다. 어째서 리포팅이 아닌데 집계 처리가 필요한 것인지 잘 생각해야한다.

또 각행의 의미가 균일하지 않다는 문제가 있다. 릴레이션은 참이 되는 명제이며 동시에 해당하는 술어가 있다. 릴레이션의 각 튜플의 의미는 명제함수만으로 결정되며 그 이상도 이하가 될 수 없다.

그러나 위의 예시에서는 가장 최신의 가격이 현재의 가격이라는 의미를 나타내게 된다.

이력데이터 해결책

절대적인 해답은 없다. 그렇지만 조금이라도 관계형 모델에 적합하게 만들어본다. 위의 가격테이블과 가격이력테이블을 가지고 예시를 들어서 진행한다.

릴레이션을 나눈다

관계형 모델적으로 의미가 다른 튜플. 명제함수로 평가할 수 없는 튜플은 같은 릴레이션에 포함시키지 않는다.

가장 간단한 분할 방법

어떤 상품의 가격 이력의 경우, 현재의 가격과 과거의 가격을 포함하는 두 개의 릴레이션으로 나눈다. 그러나 이러한 방법은 현재부터 과거까지 모든 가격이력을 조회할 때 UNION을 써야할 수가 있다.

외부키를 사용할 수 없음

테이블을 두 개로 분해할 때 가장 큰 문제는 외부키 제약을 사용할 수 없다는 것이다. 가격 테이블과 가격이력 테이블 중 하나의 테이블에 해당하는 행을 포함하는 것을 보장한다와 같은 제약을 외부키로 표현할 수 없다.

이와같이 제약을 표현하려면 트리거를 사용해야한다.

두 개의 테이블의 정합성

같은 행이 양쪽 테이블에 포함될 수 있다는 상황은 데이터의 부정합이라고 할 수 있다. 이를 방지하려면 응용프로그램에서 확실하게 행의 이동을 한 개의 트랜잭션으로 실행되게 하거나, 트리거를 사용해 제약을 표현해야 한다.

중복행을 허용

외부키를 사용하지 못하는 설계는 불편할 수 있지만 현재의 가격만 다른 테이블에 저장하고 이력 테이블은 중복되게 저장하는 설계를 할 수 있다. 그러면 이 두 릴레이션은 직교하지 않을텐데 이 문제는 제한적이므로 대안이 있다. 현재 가격 테이블에 있는 행은 이력 테이블에 같은 행이 없으면 안되므로 외부키 제약조건을 걸어야한다. 이런 경우 먼저 이력 테이블에 행을 삽입해야한다. 또한 UPDATE하지 않는다.

대리키

외부키 제약을 사용하고 싶다면 대리키를 사용하는 설계를 검토해본다. id만 저장하는 테이블을 따로 만들고 가격테이블과 이력테이블에 id 외부키로 지정해두어서 사용할 수 있다. 그러나 이것은 join이 증가한다는 점이 있다.

미래의 가격

가격 개정은 갑자기 일어나는 것이 아니라 어느 정도 사전에 알고 있는 경우가 많다. 따라서 언제부터 가격 개정을 할 것인지 계획을 세우고 그 데이터를 DB에 저장하게 된다. 미래의 가격 같은 다른 의미가 있다면 다른 테이블에 저장을 해야한다. 미래의 가격은 변경되는 시간이 되었을 때 누군가가 가격 테이블로 옮겨야 한다. 즉 응용프로그램에서 구현되어야 한다.

이력데이터의 안티 패턴

다음은 이력 데이터를 다룰 때 피해야 할 패턴이다.

플래그 사용

플래그가 있는 칼럼을 만들어 행을 숨기는 표시를 하는 값을 추가하는 것이다. 그러나 이것의 문제점은 flag 칼럼이 카디널리티가 낮아서 효율이 높지 않다는 것이다. 단순히 작은 정보에 많은 공간을 할당하는 것은 효율적이지도 않다.

정말로 문제가 되는 것은 이 릴레이션이 3NF가 되지 않는다는 것이다. 그때 그때 상황에 따라 다르다.

또한 한 개의 상품에 대해서 flag=1 이 돼야 하는 행은 항상 한 개 뿐이다. 이 제약을 트리거로 표현할 수 있지만 count()같은 것을 사용하면 부하가 높아지게 된다.

절차형으로 구현하자

어떻게 해도 관계형 모델로 대응할 수 없는 알고리즘은 선언적 스타일을 버리고 절차형으로 로직을 구현하는 것이 가장 좋은 방법일 것이다. 그러나 이는 도저히 대응할 수 없는 경우에 한해서다.