Home MySQL의 시간 정밀도
Post
Cancel

MySQL의 시간 정밀도


정산하다 보면 날짜 범위 조회 를 정말 많이 하게 되는데요. 날짜 단위 조회에서 BETWEEN>= AND < 중 어떤 방식이 더 명확한지 고민하게 되었고, 이 과정에서 알게된 내용을 정리하기 위해 글을 작성하게 되었습니다.

1
2
3
SELECT *
FROM time_test
WHERE created_at BETWEEN '2026-02-07 00:00:00.000000' AND '2026-02-07 23:59:59.999999';
1
2
3
SELECT *
FROM time_test
WHERE created_at >= '2026-02-07 00:00:00.000000' AND created_at < '2026-02-08 00:00:00';





1. MySQL의 시간 정밀도


MySQL에서 날짜·시간 칼럼이 표현할 수 있는 범위는 칼럼 타입과 정밀도에 의해 제한됩니다. DATETIME과 TIMESTAMP는 초 단위 뒤에 소수점 정밀도를 가질 수 있으며, 이 정밀도는 최대 6자리 입니다.

MySQL permits fractional seconds for TIME, DATETIME, and TIMESTAMP values, with up to microseconds (6 digits) precision. To define a column that includes a fractional seconds part, use the syntax type_name, where type_name is TIME, DATETIME, or TIMESTAMP, and fsp is the fractional seconds precision.




즉, MySQL이 저장하고 비교할 수 있는 가장 작은 시간 단위는 마이크로초이며, 나노초 단위의 시각은 데이터베이스에서 표현할 수 없습니다. 예를 들어 DATETIME(6)YYYY-MM-DD HH:MM:SS.ffffff 형태로 값을 저장하므로, 하루 중 표현 가능한 마지막 시각은 23:59:59.999999 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
+----+-------------------------------------+
| id | created_at                          |
+----+-------------------------------------+
|  1 | 2026-02-07 23:59:59.999999          |
|  2 | 2026-02-08 00:00:00.000000          |
|  3 | 2026-02-07 15:00:00.000000          |
|  4 | 2026-02-07 14:59:59.123457          |
|  5 | 2026-02-07 14:59:59.999999          |
|  6 | 2026-02-07 14:59:59.000000          |
|  7 | 2026-02-08 00:00:00.000000          |
|  8 | 2026-02-07 23:59:59.000000          |
+----+-------------------------------------+




따라서 애플리케이션에서 23:59:59.999999999 처럼 나노초 단위로 계산된 값을 사용하더라도, 해당 시각은 MySQL에서 그대로 비교되지 않고 마이크로초 단위로 정규화됩니다.

1
2
3
4
// 애플리케이션 start, end 
val start = LocalDate.of(2026, 2, 7).atStartOfDay()
val end   = LocalDate.of(2026, 2, 7)
    .atTime(23, 59, 59, 999_999_999)
1
2
3
4
5
6
-- 실제 검색 결과
SELECT *
FROM time_test
WHERE created_at BETWEEN
      '2026-02-07 00:00:00.000000'
  AND '2026-02-07 23:59:59.999999';




이는 데이터베이스 드라이버와 자바 타입 매핑 과정에서 발생합니다. 즉, 나노초 단윗값이 마이크로초 단위로 절삭되어 23:59:59.999999 로 전달됩니다. 애플리케이션이 생성한 값과 데이터베이스가 실제로 비교하는 값은 처음부터 동일하지 않죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class TimeUtil {
    
    ......
    
    public static Timestamp adjustNanosPrecision(Timestamp ts, int fsp, boolean serverRoundFracSecs) {
        if (fsp < 0 || fsp > 6) {
            throw ExceptionFactory.createException(WrongArgumentException.class, "fsp value must be in 0 to 6 range.");
        }
        Timestamp res = (Timestamp) ts.clone();
        double tail = Math.pow(10, 9 - fsp);
        int nanos = serverRoundFracSecs ? (int) Math.round(res.getNanos() / tail) * (int) tail : (int) (res.getNanos() / tail) * (int) tail;
        if (nanos > 999999999) { // if rounded up to the second then increment seconds
            nanos %= 1000000000; // get last 9 digits
            res.setTime(res.getTime() + 1000); // increment seconds
        }
        res.setNanos(nanos);
        return res;
    }

    ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
1. ClientPreparedStatement.setTimestamp(:1797)
   └─ com.mysql.cj.j데이터베이스c

2. AbstractQueryBindings.setTimestamp(:246)
   └─ com.mysql.cj

3. AbstractQueryBindings.setTimestamp(:272)
   └─ com.mysql.cj

4. ClientPreparedQueryBindings.bindTimestamp(:901)
   └─ com.mysql.cj

5. TimeUtil.adjustNanosPrecision(:195)  ← ★ 여기서 나노초 절삭/반올림
   └─ com.mysql.cj.util





2. 범위 검색에서 누락되는 케이스는 없을까?


범위 검색에서 데이터가 실제로 누락되는 케이스는 발생하지 않습니다. MySQL은 나노초 단윗값을 저장하지 못하므로, 어떤 경로로 값이 전달되든 최종적으로는 마이크로초 단위로 정규화된 값만 저장·비교하기 때문입니다. 즉, 애초에 저장할 때 정밀도가 DATETIME(6)이기 때문에 어떤 값이 입력되더라도 6자리만 비교하니까요.

1
2
3
4
5
SELECT *
FROM time_test
WHERE created_at BETWEEN
          '2026-02-07 00:00:00.000000'
          AND '2026-02-07 23:59:59.999999';




따라서 이 경우, 다음과 같이 >= AND < 를 사용하면 명확하게 경계를 지을 수 있습니다. 간단(?)하죠?

1
2
3
4
5
SELECT *
FROM time_test
WHERE created_at >= '2026-02-07 00:00:00'
  AND created_at < '2026-02-08 00:00:00'
ORDER BY created_at;





3. 범위 검색 외에 다른 문제는 없을까?


애플리케이션과 데이터베이스는 서로 다른 시간 표현 모델과 정밀도를 사용하기 때문에, 같은 시각을 기준으로 작성된 코드라도 실제 비교되는 시간의 의미는 달라질 수 있습니다. 이 차이는 시간 기반 정렬, 증분 조회, 쿠폰·포인트 지급, 배치 분할, 시간 기반 ID 생성처럼 서로 다른 시각은 구분된다 라는 전제 위에 쌓인 로직에서 순서 보장이나 처리 기준을 애매하게 만들 수 있습니다.

  • 시간 표현 모델의 차이
  • 정밀 시간 계산 불가



3-1. 시간 표현 모델의 차이

애플리케이션에서는 하루의 끝을 이렇게 정의합니다.

1
2
3
val start = LocalDate.of(2026, 2, 7).atStartOfDay()
val end   = LocalDate.of(2026, 2, 7)
    .atTime(23, 59, 59, 999_999_999)



그리고 다음 날은 이렇게 시작합니다.

1
val nextStart = LocalDate.of(2026, 2, 8).atStartOfDay()



코드만 보면 2월 7일의 끝과 2월 8일의 시작이 정확히 이어지는 것처럼 보입니다. 하지만 DB에서는 end 값이 다음과 같이 해석됩니다. 경계가 코드 기준이 아니라 데이터베이스 변환 규칙 기준 이 됩니다. 이 시점부터 날짜 구간이 정확히 이어진다고 말하기 어려워집니다.

1
'2026-02-07 23:59:59.999999'




3-2. 정밀 시간 계산 불가

쿠폰, 포인트, 이벤트 로그처럼 짧은 시간에 여러 건이 생성되는 경우입니다. 애플리케이션에서는 이렇게 생성된다고 가정 해보겠습니다.

1
2
3
쿠폰 A: 23:59:59.999999100
쿠폰 B: 23:59:59.999999200
쿠폰 C: 23:59:59.999999300



하지만 MySQL DATETIME(6)에는 모두 아래와 같이 저장되며, 쿠폰 지급 순서, 이벤트 처리 순서가 비결정적이 됩니다.

1
2
3
쿠폰 A: 23:59:59.999999
쿠폰 B: 23:59:59.999999
쿠폰 C: 23:59:59.999999



시간을 기준으로 유니크한 ID를 만들거나 순서를 보장하려는 로직은, 같은 시각에 생성되는 값이 서로 다른 시각으로 구분될 수 있다는 전제를 깔고 있습니다. 예를 들어, 현재 시각 + 시퀀스 혹은 “타임스탬프만으로도 충분히 유니크하다”라는 가정이 그렇습니다. 하지만 데이터베이스가 표현할 수 있는 시간 정밀도가 애플리케이션보다 낮은 경우, 서로 다른 시각으로 생성된 값들이 DB에서는 동일한 시각으로 취급되면서 이 전제가 무너질 수 있습니다.

1
2
3
4
# 애플리케이션에서 생성된 시각
ID A: 23:59:59.999999100
ID B: 23:59:59.999999200
ID C: 23:59:59.999999300
1
2
3
4
# DB에 저장된 시각 (DATETIME(6))
23:59:59.999999
23:59:59.999999
23:59:59.999999



이 경우 데이터는 정상적으로 저장되지만, ID 충돌 방지, 정렬 순서, 이후 재처리 기준과 같은 로직이 의도와 다르게 동작할 여지가 생깁니다. 특히 시간만으로는 충분히 구분된다. 는 암묵적인 가정 위에 쌓인 로직일수록, 문제를 추적하기가 더 어렵습니다. 물론 단순히 시간만을 유니크한 아이디 값으로는 잘 사용하지 않겠지만요.

1
2
# snowflake
[ seconds ][ node id ][ sequence ]
1
2
# ULID / UUIDv7 (시간 + 랜덤)
[ milliseconds ][ random bits ]





4. 정리


MySQL은 마이크로초까지만 시간을 표현하므로, 나노초로 계산한 하루의 끝 을 상한으로 쓰면 코드에 적힌 시간과 DB가 실제로 비교하는 시간이 달라질 수 있습니다. 결과는 같더라도 경계의 의미가 직관적이지 않으므로, 날짜 범위 조회는 >= start AND < end 형태가 깔끔합니다.


This post is licensed under CC BY 4.0 by the author.

STRAIGHT_JOIN은 왜 마지막에 사용해야 할까?

오케스트레이션의 역할과 책임은 어디까지 일까?