포스트

2024-08-19-TIL

2024-08-19-TIL

Today I Learned

이력 유형

이력은 시간에 따라 발생하는 데이터다. 같은 형식의 데이터가 시간 순서로 계속 쌓이고, 시점을 기준으로 조회된다는 특징이 있다. DB Guide의 글은 실무에서 쓰는 이력 유형을 데이터가 발생하는 방식에 따라 세 가지로 나눈다.

  • 발생이력: 어떤 사건이 일어날 때마다 쌓는 이력이다. 로그인 기록, 결제 기록처럼 사건 자체가 데이터다.
  • 변경이력: 어떤 속성 값이 바뀔 때마다 이전 값이나 바뀐 값을 남기는 이력이다. 회원 등급 변경, 주소 변경 같은 것이다.
  • 진행이력: 상태가 일정 기간 유지되다가 다른 상태로 넘어가는 이력이다. 계약 상태, 주문 처리 단계처럼 특정 시점에 어떤 상태였는지를 묻는 조회가 많다.

같은 글은 성능 관점의 해법도 함께 제시한다. 변경이력과 발생이력은 최신 값을 조회하는 빈도가 높으므로 최신여부 같은 기능성 컬럼을 두는 것이 좋고, 진행이력은 각 행에 자신의 종료일자를 함께 두어 특정 시점의 상태를 바로 찾을 수 있게 하는 것이 좋다고 한다. 이 종료일자를 두느냐가 아래의 점 이력과 선분 이력의 차이다.

  • https://blog.naver.com/db_guide/150122285707
  • https://rrhh234cm.tistory.com/32

점 이력, 선분 이력

  • 점 이력: 변경이 일어난 시점(시작일시)만 기록한다. 저장이 단순하고 새 이력을 넣을 때 기존 행을 고칠 필요가 없다. 대신 “2024-03-01 시점의 등급”을 구하려면 그 시점 이전의 가장 최근 행을 찾아야 해서 MAX(시작일시) 서브쿼리 같은 비싼 조회가 필요하다.
  • 선분 이력: 시작일시와 종료일시를 함께 기록해 각 행이 유효 기간을 갖는다. 특정 시점 조회가 WHERE :기준일 BETWEEN 시작일시 AND 종료일시 한 줄로 끝난다. 대신 새 이력을 넣을 때마다 직전 행의 종료일시를 갱신해야 하고, 현재 유효한 행의 종료일시는 9999-12-31 같은 최대값으로 둔다. 기간이 겹치거나 비지 않도록 관리하는 것이 핵심이다.
1
2
3
4
5
-- 선분 이력에서 특정 시점의 등급 조회
SELECT grade
FROM member_grade_history
WHERE member_id = :memberId
  AND :baseDate BETWEEN start_at AND end_at;

조회가 많고 시점 조회가 핵심이면 선분 이력, 쓰기가 많고 최근 값 위주로 조회하면 점 이력이 맞다.

  • https://blog.naver.com/l1523/221795451017

점 조건, 선분 조건

  • 점 조건 : IN, = 연산자를 이용한 조건을 의미하며 해당 연산자는하나의 점만을 의미하게 된다.
  • 선분 조건 : LIKE, BETWEEN, <, > 등과 같이 점 조건을 제외한연산자를 사용한 조건을 의미한다. 선분 조건은 하나의 점만을 의미하는 것이 아니면 해당 조건을 만족하는 모든 실수를 의미하게 된다.

이 구분이 중요한 이유는 결합 인덱스(복합 인덱스)의 컬럼 순서 때문이다. 인덱스는 앞 컬럼부터 정렬되어 있으므로, 앞 컬럼이 점 조건이면 그 값 안에서 다음 컬럼도 정렬되어 있어 범위를 계속 좁힐 수 있다. 반면 앞 컬럼이 선분 조건이면 그 범위 안에서는 다음 컬럼 값이 흩어져 있으므로, 뒤 컬럼 조건은 범위를 좁히지 못하고 읽은 행을 걸러내는 데만 쓰인다.

gurubee 강좌의 예로 정리하면 거래일자 > '200803' AND 사용_구분 = '정상'이라는 조건에서

  • 인덱스가 거래일자 + 사용_구분이면 선분 조건인 거래일자만 처리 범위를 줄인다.
  • 인덱스가 사용_구분 + 거래일자이면 점 조건 다음에 선분 조건이 오므로 두 조건 모두 처리 범위를 줄인다.

그래서 결합 인덱스를 설계할 때는 등호 조건으로 자주 쓰이는 컬럼을 앞에, 범위 조건 컬럼을 뒤에 두는 것이 기본 원칙이다. 앞의 선분 이력 조회도 member_id(점 조건)를 앞에 두고 start_at, end_at을 뒤에 두는 인덱스가 맞다.

  • http://www.gurubee.net/lecture/2228
  • https://dev-kwon.tistory.com/22
  • https://dev-splin.github.io/cs(computer%20science)/database/Database-Index/
  • https://tangoblog.tistory.com/7
  • https://hochoon-dev.tistory.com/entry/RDBMS-%EC%9D%B8%EB%8D%B1%EC%8A%A4-%EC%83%9D%EC%84%B1%EC%A0%84%EB%9E%B5
  • https://durai.tistory.com/m/entry/%ED%8A%9C%EB%8B%9D-%EC%A0%95%EB%A6%AC
  • https://likenow.tistory.com/31

Compound Index in MongoDB

MongoDB의 compound index도 같은 원리를 따른다. 여러 필드의 값을 지정한 순서대로 B-tree에 정렬해 저장하므로 필드 순서가 성능을 좌우한다. 인덱스의 앞부분(prefix)으로 시작하는 쿼리만 효율적으로 쓸 수 있다. 예를 들어 { item: 1, location: 1, stock: 1 } 인덱스는 item, item + location, item + location + stock 조회를 지원하지만, location이나 stock만으로 조회하는 쿼리에는 쓸 수 없다.

필드 순서는 MongoDB 문서가 권하는 ESR 가이드라인을 따른다. 등호 조건(Equality) 필드를 먼저, 정렬(Sort) 필드를 다음에, 범위 조건(Range) 필드를 마지막에 둔다. 앞의 점 조건, 선분 조건 원칙에 정렬까지 고려한 버전이다. compound index가 있으면 그 prefix와 같은 단일 인덱스는 대부분 중복이므로 제거할 수 있다.

1
db.orders.createIndex({ status: 1, createdAt: -1, amount: 1 })
  • https://www.mongodb.com/ko-kr/docs/manual/core/indexes/index-types/index-compound/
  • https://www.mongodb.com/ko-kr/docs/v5.0/core/index-compound/
  • https://blog.ull.im/engineering/2019/04/05/mongodb-indexing-strategy.html
  • https://dexie.org/docs/Compound-Index

Query By Example

Query By Example(QBE)은 원래 1970년대 IBM에서 나온, 예시 값을 채워 넣어 질의하는 방식이다. Spring Data의 QBE는 같은 아이디어로, 조회 조건을 담은 도메인 객체(probe)를 넘기면 값이 채워진 필드로 쿼리를 만든다.

1
2
3
4
5
6
7
8
Person probe = new Person();
probe.setFirstname("Dave");

ExampleMatcher matcher = ExampleMatcher.matching()
    .withIgnorePaths("lastname")
    .withStringMatcher(ExampleMatcher.StringMatcher.STARTING);

List<Person> result = personRepository.findAll(Example.of(probe, matcher));
  • 구성 요소: probe(조건 값을 채운 객체), ExampleMatcher(문자열 매칭 방식, 무시할 경로, null 처리), Example(둘을 묶은 것)
  • 사용: 리포지토리가 QueryByExampleExecutor를 상속하면 findAll(Example) 같은 메서드를 쓸 수 있다. JpaRepository는 이미 이를 상속한다.
  • 한계: firstname = ? or (firstname = ? and lastname = ?) 같은 중첩되거나 묶인 조건을 표현할 수 없고, 컬렉션이나 맵 속성은 지원하지 않는다. 문자열이 아닌 속성은 정확히 일치하는 조건만 가능하다.

검색 화면처럼 입력된 필드만 조건으로 쓰는 단순한 동적 쿼리에 편하지만, 범위 조건이나 OR 조건이 필요하면 Specification이나 QueryDSL을 쓴다.

  • https://en.wikipedia.org/wiki/Query_by_Example
  • https://docs.spring.io/spring-data/jpa/reference/repositories/query-by-example.html
이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다