결정적 함수

결정적 함수 (Deterministic 함수)

SQLite 에서 SQL 함수는 "결정적(deterministic)" 이거나 "비결정적(non-deterministic)" 일 수 있어요. 결정적 함수는 같은 입력이 주어지면 항상 같은 결과를 주는 함수예요.

출처: SQLite Deterministic Functions

본문

SQLite 의 SQL 함수는 두 가지 종류로 나눌 수 있어요. 입력이 같으면 항상 같은 결과를 주는 결정적 함수와, 같은 인자라도 호출할 때마다 다른 결과를 줄 수 있는 비결정적 함수예요.

1. 개요 (Overview)

결정적 함수는 같은 입력이면 항상 같은 답을 줘요. SQLite 의 대부분 내장 SQL 함수는 결정적이에요. 예를 들어 abs(X) 함수는 입력 X 가 같으면 항상 같은 답을 반환해요.

비결정적 함수는 인자가 항상 같더라도 호출할 때마다 다른 답을 줄 수 있어요. 비결정적 함수의 예는 다음과 같아요:

  • random()
  • changes()
  • last_insert_rowid()
  • sqlite3_version()

random() 함수는 호출할 때마다 다른 답을 주므로 당연히 비결정적이에요. changes()last_insert_rowid() 의 답은 이전 SQL 문장에 의존하므로 역시 비결정적이에요. sqlite3_version() 함수는 거의 상수지만 SQLite 가 업그레이드되면 바뀔 수 있어요. 특정 세션에서는 항상 같은 답을 주지만, 세션을 넘어서 답이 바뀔 수 있으므로 여전히 비결정적 함수로 간주돼요.

2. 비결정적 함수 사용 제한

SQLite 에는 비결정적 함수를 사용할 수 없는 몇 가지 문맥이 있어요:

  • CHECK 제약 조건의 표현식에서 (아래 2.1 참고)
  • 부분 인덱스(partial index)의 WHERE 절에서
  • 표현식 인덱스의 일부로 사용되는 표현식에서
  • 생성된 컬럼(generated column)의 표현식에서

위 경우에 함수가 반환하는 값은 데이터베이스 파일에 저장되는 정보에 영향을 줘요. CHECK 제약 조건의 함수 값은 어떤 항목이 테이블에 유효한지를 결정하고, 부분 인덱스의 WHERE 절이나 표현식 인덱스의 함수 값은 인덱스 b-tree 에 저장되는 값을 계산해요. 나중에 이 중 어떤 함수라도 다른 값을 반환하면, 데이터베이스가 더 이상 올바른 형태가 아닐 수 있어요. 따라서 데이터베이스 손상을 막기 위해 위 문맥에서는 결정적 함수만 사용할 수 있어요.

2.1. CHECK 제약 조건의 역사적 예외

위에서 CHECK 제약 조건에는 비결정적 함수를 사용할 수 없다고 했어요. 원래는 그래야 하지만, 과거의 버그와 하위 호환성 유지에 대한 바람 때문에 그 제한이 실제로는 강제되지 않아요. CHECK 제약 조건 안에 비결정적 함수를 넣어도 SQLite 는 불평하지 않아요. 그렇게 해도 크래시나 메모리 오류가 발생하지는 않아요. 하지만 크래시가 나지 않는다는 것 외에는, 그 CHECK 제약 조건이 어떻게 동작할지에 대해 어떤 보장도 없어요. 그러니까 하지 않는 것이 좋아요. 이 조언을 무시하고 예상치 못한 동작을 겪어도, 그것은 버그로 간주되지 않아요 (크래시는 항상 버그로 간주되므로, 크래시가 나는 경우는 제외해요).

3. 날짜/시간 함수의 특별 처리

SQLite 의 내장 날짜/시간 함수는 특별한 경우예요. 이 함수들은 보통 결정적 함수로 간주돼요. 하지만 이 함수들이 날짜로 "now" 문자열을 사용하거나, localtime 수식어나 utc 수식어를 사용하면 비결정적 함수로 간주돼요. 함수 입력이 런타임까지 반드시 알려지는 것은 아니므로, 결정적 함수만 허용되는 문맥에서 비결정적 특성을 만나면 날짜/시간 함수는 예외를 던져요.

SQLite 3.20.0 (2017-08-01) 이전에는 모든 날짜/시간 함수가 항상 비결정적 함수로 간주됐어요. 인자에 따라 어떤 때는 결정적이고 어떤 때는 비결정적일 수 있는 기능은 3.20.0 릴리스에서 추가됐어요.

3.1. 버전 3.35.2 의 버그 수정

SQLite 3.20.0 에서 날짜/시간 함수가 현재 시간에 의존하지 않으면 결정적 함수로 간주되도록 개선했을 때, 한 가지 경우가 간과됐어요. 많은 날짜/시간 함수가 인자 없이 호출될 수 있다는 점이에요. 인자 없는 날짜/시간 함수는 "'now'" 인자가 하나 있는 것처럼 동작해요. 따라서 "datetime()" 과 "datetime('now')" 는 둘 다 현재 날짜와 시간을 만들어내요. 하지만 두 번째 형태만 비결정적으로 인식됐어요. 이는 개발자가 비결정적 "datetime()" 형태를 CHECK 제약 조건, 인덱스 표현식, 생성된 컬럼 표현식 등 비결정적 함수가 어울리지 않는 곳에 몰래 넣을 수 있다는 뜻이었어요. 이 간과는 버전 3.35.2 (2021-03-17) 에서 수정됐어요. 하지만 SQLite 3.20.0 에서 3.35.1 사이 버전으로 만들어져 스키마에 비결정적 날짜/시간 함수가 있는 레거시 데이터베이스가 유통되고 있을 수 있어요.

4. 애플리케이션 정의 결정적 함수

기본적으로 애플리케이션 정의 SQL 함수는 비결정적 함수로 간주돼요. 하지만 sqlite3_create_function_v2() 의 4번째 매개변수에 SQLITE_DETERMINISTIC 을 OR 하면, SQLite 는 그 함수를 결정적 함수처럼 취급해요.

비결정적 함수에 SQLITE_DETERMINISTIC 태그를 달고, 그 함수가 부분 인덱스의 WHERE 절이나 표현식 인덱스에서 사용된다면, 함수가 다른 답을 반환하기 시작할 때 관련 인덱스가 손상될 수 있으니 주의하세요. 거의 결정적인 SQL 함수(즉, sqlite_version() 처럼 아주 드물게만 바뀌는 함수)가 손상된 인덱스에서 사용된다면, REINDEX 를 실행해 손상을 고칠 수 있어요.

입력에 따라 어떤 때는 결정적이고 어떤 때는 비결정적인 함수(내장 날짜/시간 함수처럼)를 만드는 데 필요한 인터페이스는 공개되지 않았어요. 일반적인 애플리케이션 정의 SQL 함수는 항상 결정적이거나 항상 비결정적이어야 해요.

더 알아보기 (Learn more)