SQL Server의 UUID 칼럼 검색 성능 문제

요약

데이터베이스로 SQL Server를 사용하는 고객이 심각한 성능 문제를 경험하고 있습니다. 문제는 다음과 같습니다.

UUID 형식 문자열(VARCHAR)을 주키(Primary Key)로 사용하는 테이블을 주키 칼럼으로 조회했을 때 약 10% 비율로 풀스캔(Full Scan)합니다. 값에 따라 항상 색인을 이용하거나 풀스캔을 하거나 합니다.

주키로 조회하는 SELECT 쿼리 뿐만 아니라 UPDATEDELETE 쿼리에서 성능 저하 문제를 일으키고 있습니다.

단, 자바에서 PreparedStatement을 사용했을 때만 나타나는 현상입니다. 검색 조건을 결합하여 SQL을 만들 때는 성능 저하 문제가 없습니다.

현재까지 확인한 유일한 해결책은 PreparedStatement을 사용하지 않고 SQL에 검색 조건을 결합하는 겁니다. SQL 삽입(SQL Injection) 공격 등의 부작용도 처리해야 합니다.

SQL Server의 UNIQUEIDENTIFIERNEWID() 함수를 사용했을 때는 문제가 없습니다.

재현

테스트에서 사용한 SQL Server는 SQL Server 2019 Express Edition(15.0.2000.5, RTM)입니다. 2014 Express Edition에서도 마찬가지입니다.

다음 테이블이 있습니다.

CREATE TABLE MY_TABLE (
  ID VARCHAR(16) PRIMARY KEY
);

이 테이블에는 ID 칼럼만이 있습니다. 소문자 알파벳과 숫자로 구성된 16 글자 문자열로 주키 칼럼입니다. 8 바이트 데이터를 16진수로 표현한 겁니다.

다음은 ID를 만드는 자바 코드입니다.

import java.security.SecureRandom;

public class UUID {

    private static volatile SecureRandom numberGenerator = new SecureRandom();

    private final String id;

    public UUID() {
        byte[] randomBytes = new byte[8];
        numberGenerator.nextBytes(randomBytes);
        StringBuilder sb = new StringBuilder(2 * randomBytes.length);
        for (int i = 0; i < randomBytes.length; i++) {
            String hex = Integer.toHexString(randomBytes[i] & 0xFF);
            if (hex.length() == 1) {
                sb.append('0');
            }
            sb.append(hex);
        }
        id = sb.toString();
    }

    @Override
    public String toString() {
        return id;
    }

}

ID 칼럼을 VARCHAR(36)으로 바꾸고 java.util.UUID를 사용했을 때도 비슷한 문제가 있습니다.

백만건의 데이터를 입력했습니다. 앞 그림은 모두 '00'으로 시작하지만 전체적으로 앞 2자를 기준으로 균등합니다. 다음 쿼리로 확인할 수 있습니다.

SELECT C, COUNT(*)
  FROM (SELECT LEFT(ID, 2) AS C FROM MY_TABLE) T
GROUP BY C

다음은 자바에서 PreparedStatement로 각 레코드를 조회하는 코드입니다.

void load(String id) {
  try (Connection con = Sqls.getConnection()) {
    String sql = "SELECT * FROM MY_TABLE WHERE ID = ?";
    PreparedStatement pstmt = con.prepareStatement(sql);
    pstmt.setString(1, id);
    pstmt.executeQuery();
  }
}

임의의 만건을 실행했을 때 걸리는 시간은 다음과 같습니다.

기준

비율

평균 시간

3ms 이하

87.41%

0ms

10ms 이하

0.69%

6ms

50ms 이하

3.62%

30ms

100ms 이하

4.15%

73ms

100ms 이상

4.13%

124ms

값에 따라 항상 느리거나 항상 빠릅니다. 예를 들어 다음 값으로 조회하면 항상 느립니다.

03ce3717dee82df0
1d3936b2ddd718a9
24f7de006b3fa8c0
57cabb4fb61787e9

주키로 검색을 했음에도 약 10%의 성능 저하 현상이 나타납니다. PreparedStatement을 사용하지 않은 코드입니다.

void load(String id) {
  try (Connection con = Sqls.getConnection()) {
    String sql = String.format("SELECT * FROM MY_TABLE WHERE ID = '%s'", id);
    Statement stmt = con.createStatement();
    stmt.executeQuery(sql);
  }
}

결과를 보면 만건 중 한번을 제외하면 문제가 없습니다. PreparedStatement 사용 여부에 따라 결과가 크게 다릅니다.

기준

비율

평균 시간

3ms 이하

99.98%

0ms

10ms 이하

0.01%

3ms

50ms 이하

0%


100ms 이하

0%


100ms 이상

0.01%

406ms

오라클(11g Express Edition Release 11.2.0.2.0)에서 테스트했을 때는 이런 문제가 없습니다. 오라클에서는 한두건이 느리게 나타나는 현상도 없습니다.

해결책

현재까지는 PreparedStatement를 사용하지 않는다가 유일한 해결책입니다. 몇 가지 다른 시도는 해결책이 아니었습니다.

JDBC 드라이버 교체

다음 드라이버에서 테스트했으며 차이가 없습니다.

파라미터 스니핑(Parameter Sniffing)

확인 중입니다.

힌트 사용

다음과 같이 색인 힌트를 주었지만 효과가 없었습니다.

SELECT * FROM MY_TABLE WITH(INDEX(MY_TABLE_ID_PK)) WHERE ID = ?