본문 바로가기
데이터베이스(SQL)/데이터베이스 개념

SQLite CHECK에 NULL이 통과하는 이유: NOT NULL과 양수 조건 나누기

by char_lie 2026. 10. 27.
반응형

SQLite에서 CHECK(quantity > 0)만 선언하면 quantity가 NULL인 행이 들어갈 수 있습니다. CHECK 식이 0일 때 위반이고 NULL이면 위반으로 처리하지 않기 때문입니다. 값이 반드시 있어야 하는 업무라면 NOT NULL을 따로 선언하고 양수 조건과 함께 검사해야 합니다.

하지만 NOT NULL을 더했다고 수량이 반드시 정수가 되는 것은 아닙니다. 아래 실습은 NULL 차단과 값 범위, 저장된 타입 검사를 나누어 보여 줍니다. Python sqlite3의 메모리 데이터베이스만 만들며 기존 운영 테이블이나 데이터를 바꾸지 않습니다.

양수 CHECK 조건만으로는 미정인 NULL 입력의 저장을 막지 못하는 상황의 개념 그림

WHERE와 CHECK에서 NULL을 똑같이 생각하면 놓칩니다

NULL > 0의 결과는 참이나 거짓이 아닌 NULL입니다. WHERE는 참으로 평가된 행을 선택하므로 이 행이 조회에서 빠질 수 있습니다. SQLite의 CHECK 제약은 식이 0이면 거부하고 NULL이나 0이 아닌 값이면 통과시킵니다. CHECK를 양수 조건의 존재 여부까지 보장하는 장치로 생각하면 안 됩니다.

값이 아직 정해지지 않았다는 의미로 NULL을 허용하는 설계도 가능합니다. 그 경우에는 NULL을 무조건 0으로 바꾸거나 NOT NULL을 덧붙이는 것이 해결책이 아닙니다. 미정 상태를 허용할지 먼저 정한 뒤 조회·집계에서의 처리까지 연결해야 합니다.

같은 입력을 세 가지 제약에 넣어 봅니다

Python 3.14.7·SQLite 3.53.4에서 실행했습니다. check_null_demo.py로 저장해 python3 check_null_demo.py로 실행하세요. 각 INSERT는 예제에서 독립적으로 실행해 통과한 입력을 관찰합니다. 실제 데이터 이전이나 일괄 적재의 트랜잭션 절차를 대신하지 않습니다.

import sqlite3

connection = sqlite3.connect(':memory:', isolation_level=None)
try:
    connection.executescript('''
        CREATE TABLE check_only(quantity INTEGER CHECK(quantity > 0));
        CREATE TABLE required(quantity INTEGER NOT NULL CHECK(quantity > 0));
        CREATE TABLE positive_integer(
            quantity INTEGER NOT NULL
            CHECK(typeof(quantity) = 'integer' AND quantity > 0)
        );
    ''')
    for table in ['check_only', 'required', 'positive_integer']:
        accepted = []
        for value in [None, 0, -1, 1, 1.5, 'abc']:
            try:
                connection.execute(f'INSERT INTO {table} VALUES (?)', (value,))
                accepted.append(value)
            except sqlite3.IntegrityError:
                pass
        stored = connection.execute(
            f'SELECT quantity, typeof(quantity) FROM {table} ORDER BY rowid').fetchall()
        print(table, 'input accepted=', accepted)
        print(table, 'stored=', stored)
    print('NULL > 0:', connection.execute('SELECT NULL > 0').fetchone()[0])
    bad_audit = connection.execute(
        'SELECT quantity FROM check_only WHERE NOT (quantity > 0) ORDER BY rowid').fetchall()
    null_audit = connection.execute(
        'SELECT quantity FROM check_only WHERE quantity IS NULL ORDER BY rowid').fetchall()
    integer_audit = connection.execute('''
        SELECT quantity, typeof(quantity) FROM check_only
        WHERE quantity IS NULL OR typeof(quantity) != 'integer' OR quantity <= 0
        ORDER BY rowid
    ''').fetchall()
    print('NOT-only audit:', bad_audit)
    print('NULL audit:', null_audit)
    print('positive-integer audit:', integer_audit)
    assert null_audit == [(None,)]
    assert integer_audit == [(None, 'null'), (1.5, 'real'), ('abc', 'text')]
finally:
    connection.close()

실제 출력: 존재·양수·타입은 다른 조건입니다

check_only input accepted= [None, 1, 1.5, 'abc']
check_only stored= [(None, 'null'), (1, 'integer'), (1.5, 'real'), ('abc', 'text')]
required input accepted= [1, 1.5, 'abc']
required stored= [(1, 'integer'), (1.5, 'real'), ('abc', 'text')]
positive_integer input accepted= [1]
positive_integer stored= [(1, 'integer')]
NULL > 0: None
NOT-only audit: []
NULL audit: [(None,)]
positive-integer audit: [(None, 'null'), (1.5, 'real'), ('abc', 'text')]

값의 존재 여부와 양수 조건을 서로 다른 단계에서 검사하는 데이터 제약의 개념 그림

NOT NULL만으로는 타입 계약이 완성되지 않습니다

check_only에는 NULL이 남고 required에는 남지 않습니다. 반면 일반 SQLite INTEGER 열에서는 1.5와 문자열 abc가 두 테이블에 모두 들어갑니다. 동적 타입과 비교 규칙이 작용하기 때문에 INTEGER라는 선언을 모든 입력 타입의 엄격한 검사로 해석하면 안 됩니다. 이 반례를 다른 DBMS의 엄격한 INTEGER 열에 그대로 일반화하지 않습니다.

positive_integer는 저장된 typeof(quantity)가 integer이고 값이 양수인 경우만 받습니다. 이것도 원래 요청의 타입을 그대로 검사하는 규칙은 아닙니다. 예를 들어 숫자로 변환 가능한 문자열은 INTEGER affinity에 의해 정수로 저장될 수 있습니다. 원본 형식까지 제한해야 한다면 애플리케이션 입력 검증도 따로 필요합니다.

SQLite 3.37.0 이상에서 지원하는 STRICT 테이블은 타입 규칙을 강화하는 별도 선택이지만, 그것만으로 모든 열에 NOT NULL이나 양수 업무 조건이 자동 추가되지는 않습니다. 기존 테이블을 그 모드로 바꾸는 마이그레이션은 이번 실습에서 수행하지 않습니다.

기존 데이터 감사에서도 NULL을 명시합니다

NOT-only audit가 빈 목록이어도 원본 테이블에 문제가 없다는 뜻은 아닙니다. NOT(NULL > 0)도 NULL이므로 WHERE에 선택되지 않습니다. 양의 정수 계약을 감사하려면 예제처럼 IS NULL, 저장 타입, 값 범위를 명시적으로 나누어 확인해야 합니다.

이미 저장된 NULL이 업무상 미정인지 입력 오류인지 확인하고 원본과 복원 사본을 확보한 뒤 제약 변경을 검토하세요. 단순히 오류를 없애려고 값을 0으로 채우면 양수 조건에도 맞지 않고 원래 의미까지 바뀔 수 있습니다. 저장 뒤 NULL이 집계에서 어떻게 보이는지는 COUNT(*)와 COUNT(컬럼)의 차이로 이어집니다.

핵심 정리

CHECK(quantity > 0), NOT NULL, 정수 타입 계약은 각각 확인할 조건입니다. SQLite에서는 CHECK의 NULL 통과와 WHERE의 NULL 제외를 구별하고, 현재 입력과 기존 저장 데이터 모두에 맞는 검증을 정해야 합니다.

반응형

댓글