DB·SQL 실무 가이드 · Part 2

NULL과 데이터 타입이 만드는 함정

조용히 틀린 결과를 만드는 NULL과 타입 문제를 먼저 걷어내기

작성 기준2026년 7월버전과 지원 현황은 이후 달라질 수 있으니 공식 문서를 함께 확인하세요.

이 파트에서 다루는 내용

빈 문자열은 NULLNULL 연산 규칙NULL 다루는 함수정렬 순서암묵적 형변환날짜 타입
01

Oracle에서 빈 문자열은 NULL입니다

다른 DBMS를 쓰다 온 사람이 가장 크게 데는 지점입니다. Oracle은 길이 0인 문자열을 NULL로 저장합니다. MSSQL, MySQL, PostgreSQL은 빈 문자열과 NULL을 구분하는데, Oracle만 다릅니다.

이게 위험한 이유는 오류가 나지 않기 때문입니다. 쿼리는 정상 실행되고 결과만 조용히 틀립니다. 이런 종류의 버그가 가장 늦게 발견됩니다.

NOT NULL 제약이 안 막아 줌
이관 시 사고

다른 DB에서 빈 문자열로 들어 있던 값이 Oracle로 넘어오면 NULL이 됩니다. NOT NULL 컬럼이면 이관 자체가 실패하고, 아니면 조용히 NULL이 됩니다.

애플리케이션 코드와 어긋남
흔한 증상

Java에서 빈 문자열을 저장했는데 조회하면 null로 돌아옵니다. 화면에서 값이 사라지는 버그의 흔한 원인입니다.

대응 방법
권장

빈 값을 의미 있게 구분해야 하면 공백 한 칸이나 별도 코드값을 쓰는 대신, 애초에 NULL 허용 여부를 명확히 설계합니다.

빈 문자열을 넣으면 벌어지는 일sql
CREATE TABLE t_empty (col1 VARCHAR2(10));

INSERT INTO t_empty VALUES ('');
COMMIT;

-- 빈 문자열로 찾으면 안 나옵니다
SELECT COUNT(*) FROM t_empty WHERE col1 = '';   --> 0

-- NULL로 찾아야 나옵니다
SELECT COUNT(*) FROM t_empty WHERE col1 IS NULL; --> 1

위 첫 번째 쿼리는 사실 = NULL 과 같아서 항상 0건입니다. 문법 오류가 아니라 논리적으로 참이 될 수 없는 조건입니다.

02

NULL은 값이 아니라 '모름'입니다

NULL은 0도 아니고 빈 값도 아닙니다. '알 수 없음'입니다. 그래서 NULL과의 비교는 참도 거짓도 아닌 UNKNOWN이 되고, WHERE 절은 참인 행만 반환하므로 결과에서 빠집니다.

  • `col = NULL` 은 절대 참이 되지 않습니다. `IS NULL` 을 써야 합니다.
  • `col != 'A'` 로 조회하면 col이 NULL인 행은 나오지 않습니다. NULL은 'A'와 다르다고 단정할 수 없기 때문입니다.
  • 산술 연산에 NULL이 끼면 결과 전체가 NULL입니다. `amount + NULL` 은 NULL입니다.
  • 문자열 결합은 예외입니다. Oracle에서 `'A' || NULL` 은 'A'입니다. 다른 DBMS와 다른 부분입니다.
  • 집계 함수는 NULL을 무시합니다. `AVG(col)` 은 NULL인 행을 분모에서 뺍니다. 0으로 계산되길 기대했다면 결과가 달라집니다.
  • `COUNT(*)` 는 전체 행, `COUNT(col)` 은 col이 NULL이 아닌 행만 셉니다.
NOT IN에 NULL이 섞이면 결과가 통째로 사라집니다sql
-- grade_cd 에 NULL이 하나라도 있으면 결과는 0건
SELECT COUNT(*)
FROM   orders o
WHERE  o.customer_id NOT IN (
         SELECT c.customer_id FROM customer c WHERE c.grade_cd IS NULL
       );

-- NOT EXISTS 는 NULL의 영향을 받지 않습니다
SELECT COUNT(*)
FROM   orders o
WHERE  NOT EXISTS (
         SELECT 1 FROM customer c
         WHERE  c.customer_id = o.customer_id
         AND    c.grade_cd IS NULL
       );

NOT IN 의 목록에 NULL이 하나라도 있으면 모든 비교가 UNKNOWN이 되어 결과가 0건이 됩니다. 실무에서는 NOT EXISTS 를 기본으로 쓰는 편이 안전합니다.

가장 많이 나오는 실무 버그

NOT IN 서브쿼리에 NULL이 섞이는 문제는 데이터가 늘어난 뒤에야 드러납니다. 처음에는 잘 돌다가 어느 날 결과가 0건이 되는 배치가 있다면 여기를 먼저 봅니다.

03

NULL을 다루는 함수

NVL(a, b)
가장 기본

a가 NULL이면 b를 반환합니다. 두 값의 타입이 호환돼야 합니다. 다른 DBMS의 ISNULL, IFNULL에 해당합니다.

NVL2(a, b, c)
Oracle 고유

a가 NULL이 아니면 b, NULL이면 c를 반환합니다. 값의 존재 여부로 분기할 때 짧게 씁니다.

COALESCE(a, b, c...)
표준

인자를 순서대로 보면서 처음 만나는 NULL이 아닌 값을 반환합니다. 표준 SQL이라 이관을 고려하면 이쪽이 낫습니다.

NULLIF(a, b)
역방향

a와 b가 같으면 NULL을 반환합니다. 0으로 나누는 것을 막을 때 분모에 자주 씁니다.

DECODE
Oracle 고유

등호 비교만 가능한 오래된 분기 함수입니다. 레거시 코드에 많이 남아 있지만, 새로 쓸 때는 CASE를 권합니다.

CASE WHEN
권장

범위 비교와 복합 조건이 가능하고 표준입니다. DECODE로 표현할 수 있는 것은 CASE로도 다 됩니다.

0으로 나누기 방지sql
-- 분모가 0이면 ORA-01476 오류
SELECT total_amount / order_cnt FROM ...;

-- NULLIF로 0을 NULL로 바꾸면 결과가 NULL이 될 뿐 오류는 안 납니다
SELECT total_amount / NULLIF(order_cnt, 0) FROM ...;

-- 화면에 0으로 보이길 원하면 감쌉니다
SELECT NVL(total_amount / NULLIF(order_cnt, 0), 0) FROM ...;
04

NULL의 정렬 순서는 제품마다 다릅니다

Oracle은 NULL을 가장 큰 값으로 취급합니다. 그래서 오름차순 정렬하면 NULL이 맨 뒤에, 내림차순이면 맨 앞에 옵니다.

MSSQL과 MySQL은 반대로 NULL을 가장 작은 값으로 봅니다. 같은 쿼리를 이관했는데 화면의 정렬 순서가 달라지는 원인이 대개 이것입니다.

정렬 위치를 명시하면 제품과 무관해집니다sql
-- Oracle 기본 동작
SELECT grade_cd FROM customer ORDER BY grade_cd;       -- NULL이 뒤
SELECT grade_cd FROM customer ORDER BY grade_cd DESC;  -- NULL이 앞

-- 명시하면 의도가 분명해집니다
SELECT grade_cd FROM customer ORDER BY grade_cd NULLS FIRST;
SELECT grade_cd FROM customer ORDER BY grade_cd DESC NULLS LAST;

NULLS FIRST / NULLS LAST 는 Oracle과 PostgreSQL에서 지원합니다. MSSQL과 MySQL에는 없어서 CASE로 대체해야 합니다. 부록 대조표를 참고합니다.

05

암묵적 형변환은 성능까지 무너뜨립니다

타입이 다른 값을 비교하면 Oracle이 알아서 맞춰 줍니다. 편해 보이지만 두 가지 문제를 만듭니다.

첫째, 변환 방향이 예상과 다를 수 있습니다. 문자와 숫자를 비교하면 Oracle은 문자 쪽을 숫자로 바꿉니다. 컬럼이 문자라면 컬럼에 변환이 걸립니다.

둘째, 컬럼에 변환이 걸리면 그 컬럼의 인덱스를 못 씁니다. 트랙 B에서 자세히 다루지만, 이 파트에서 미리 짚어 둡니다.

문자 컬럼에 숫자 조건을 주면sql
-- status_cd 는 VARCHAR2 인데 숫자를 비교하면
SELECT * FROM orders WHERE status_cd = 1;
-- 내부적으로 TO_NUMBER(status_cd) = 1 이 되어
-- status_cd 인덱스를 못 타고, 숫자로 못 바꾸는 값이 있으면 ORA-01722 까지 납니다

-- 타입을 맞춰 줍니다
SELECT * FROM orders WHERE status_cd = '1';

애플리케이션에서 바인딩할 때도 같습니다. 문자 컬럼에 Integer를 바인딩하면 같은 문제가 생깁니다. 운영에서 갑자기 느려진 쿼리의 흔한 원인입니다.

06

Oracle의 DATE에는 시분초가 들어 있습니다

다른 DBMS에서 DATE가 날짜만 담는 것과 달리, Oracle의 DATE는 연월일시분초를 모두 담습니다. 초 단위 아래가 필요하면 TIMESTAMP를 씁니다.

그래서 '오늘 주문'을 찾겠다고 `order_dt = SYSDATE` 라고 쓰면 거의 아무것도 안 나옵니다. 시분초까지 정확히 같아야 하기 때문입니다.

날짜 범위 조회의 올바른 형태sql
-- 나쁨: 컬럼을 가공해서 인덱스를 못 씁니다
SELECT * FROM orders
WHERE  TRUNC(order_dt) = TRUNC(SYSDATE);

-- 좋음: 범위로 잡으면 인덱스를 씁니다
SELECT * FROM orders
WHERE  order_dt >= TRUNC(SYSDATE)
AND    order_dt <  TRUNC(SYSDATE) + 1;

-- 특정 월 전체
SELECT * FROM orders
WHERE  order_dt >= TO_DATE('2026-07-01', 'YYYY-MM-DD')
AND    order_dt <  ADD_MONTHS(TO_DATE('2026-07-01', 'YYYY-MM-DD'), 1);

끝 날짜를 <= 로 잡으면 그날 00:00:00 만 포함되어 하루치가 통째로 빠집니다. 시작은 >= , 끝은 다음 경계 미만으로 잡는 습관을 들입니다.

형식 문자열에 의존하지 않기sql
-- 위험: 세션의 NLS_DATE_FORMAT 에 따라 결과가 달라집니다
SELECT * FROM orders WHERE order_dt > '2026-07-01';

-- 안전: 형식을 명시합니다
SELECT * FROM orders
WHERE  order_dt > TO_DATE('2026-07-01', 'YYYY-MM-DD');

문자열을 날짜처럼 쓰면 개발 환경에서는 되고 운영에서는 ORA-01861 이 나는 일이 생깁니다. 세션 설정에 기대지 않습니다.

이 파트의 핵심

여기서 다룬 문제들은 오류를 내지 않고 결과만 틀리게 만든다는 공통점이 있습니다. 빈 문자열, NOT IN의 NULL, 날짜 경계, 암묵적 형변환. 이 네 가지만 의심할 줄 알아도 원인 모를 데이터 불일치의 상당수가 잡힙니다.

체크

이 파트 완료 기준