SQL Server에서 NULL 값을 대체할 때 ISNULL과 COALESCE 두 가지 선택지가 있다. 둘 다 "NULL이면 다른 값으로 바꿔줘"라는 같은 목적을 가지고 있지만, 내부 동작 방식과 세부 특성이 다르다.
단순히 "ISNULL이 빠르니까 ISNULL 쓰자"로 끝나는 게 아니다. 반환 타입 차이 때문에 예상치 못한 버그가 발생할 수 있어서, 정확한 차이를 알고 상황에 맞게 선택해야 한다.
ISNULL(check_expression, replacement_value)
DECLARE @nickname NVARCHAR(50) = NULL
SELECT ISNULL(@nickname, '익명') -- 결과: '익명'
COALESCE(expression1, expression2, ... , expressionN)
DECLARE @a NVARCHAR(50) = NULL
DECLARE @b NVARCHAR(50) = NULL
DECLARE @c NVARCHAR(50) = '기본값'
SELECT COALESCE(@a, @b, @c, '최종 기본값') -- 결과: '기본값'
COALESCE는 인자를 순서대로 확인하면서 NULL이 아닌 첫 번째 값을 반환한다. 모두 NULL이면 마지막 값(이것도 NULL일 수 있음)을 반환한다.
{: .prompt-info }
이 부분이 실무에서 버그로 이어지는 핵심 차이다.
DECLARE @a VARCHAR(10) = NULL
DECLARE @b VARCHAR(100) = 'hello world test'
SELECT ISNULL(@a, @b)
-- 결과: 'hello worl' (10자로 잘림!)
-- 반환 타입: VARCHAR(10)
@a가 VARCHAR(10)이므로, 결과도 VARCHAR(10)이 된다. @b의 값이 아무리 길어도 첫 번째 인자의 타입 길이로 잘린다.
DECLARE @a VARCHAR(10) = NULL
DECLARE @b VARCHAR(100) = 'hello world test'
SELECT COALESCE(@a, @b)
-- 결과: 'hello world test' (전체 출력)
-- 반환 타입: VARCHAR(100)
COALESCE는 인자들 중 데이터 타입 우선순위가 가장 높은 타입 으로 결과를 반환한다. 길이도 가장 큰 것을 따른다.
-- 회원 테이블
CREATE TABLE TblMember (
_memberId INT,
_nickname NVARCHAR(10), -- 최대 10자
_email NVARCHAR(100)
)
-- 의도: 닉네임이 없으면 이메일 앞부분을 표시
SELECT
_memberId,
ISNULL(_nickname, _email) AS _displayName -- 버그!
FROM TblMember
_email이 verylongemail@example.com이라면, ISNULL 결과는 verylonge(10자)로 잘린다.
-- 수정: COALESCE 사용
SELECT
_memberId,
COALESCE(_nickname, _email) AS _displayName -- 정상
FROM TblMember
서로 다른 길이의 문자열 컬럼을 비교할 때는 COALESCE가 안전하다.
{: .prompt-warning }
ISNULL은 SQL Server 내장 함수로, 직접 NULL 체크 후 값을 반환한다.
COALESCE는 내부적으로 CASE 문으로 변환되어 실행된다.
-- 이 쿼리는
COALESCE(@a, @b, @c)
-- 내부적으로 이렇게 변환됨
CASE
WHEN @a IS NOT NULL THEN @a
WHEN @b IS NOT NULL THEN @b
ELSE @c
END
이론적으로 ISNULL이 더 빠르다. 하지만 체감할 정도의 차이는 거의 없다.
-- 100만 건 테스트 시
-- ISNULL: ~150ms
-- COALESCE: ~160ms
수백만 건을 반복 처리하는 배치 작업이 아니라면, 성능보다는 정확성(타입 안전성) 을 우선시하는 게 맞다.
SELECT INTO로 테이블을 생성할 때 차이가 발생한다.
SELECT
ISNULL(NULL, 1) AS isnull_col,
COALESCE(NULL, 1) AS coalesce_col
INTO #TempTable
-- 결과 테이블 구조
-- isnull_col: INT NOT NULL
-- coalesce_col: INT NULL
인덱싱된 뷰(Indexed View)나 계산 컬럼에서 이 차이가 영향을 줄 수 있다.
ISNULL은 2개만 받으므로, 3개 이상 비교하려면 중첩해야 한다.
-- ISNULL 중첩 (가독성 나쁨)
ISNULL(@a, ISNULL(@b, ISNULL(@c, 'default')))
-- COALESCE (깔끔함)
COALESCE(@a, @b, @c, 'default')
인자가 3개 이상이면 COALESCE가 유일한 선택지 다.
-- 연락 가능한 번호를 우선순위대로 반환
SELECT
_memberId,
COALESCE(_mobilePhone, _homePhone, _officePhone, '연락처 없음') AS _contactNumber
FROM TblMember
| 상황 | 권장 | 이유 |
|---|---|---|
| 인자 2개, 같은 타입 | ISNULL | 단순하고 약간 빠름 |
| 인자 2개, 다른 타입/길이 | COALESCE | 데이터 잘림 방지 |
| 인자 3개 이상 | COALESCE | ISNULL은 불가능 |
| 다른 DB 마이그레이션 가능성 | COALESCE | ANSI 표준 |
| NOT NULL 보장 필요 | ISNULL | Nullability 특성 |
COALESCE에 서브쿼리를 넣으면 여러 번 실행될 수 있다.
-- 비효율적: 서브쿼리가 2번 실행될 수 있음
SELECT COALESCE(
(SELECT TOP 1 _value FROM TblConfig WHERE _key = 'setting1'),
'default'
)
-- 개선: 변수에 먼저 담기
DECLARE @setting NVARCHAR(100)
SELECT @setting = _value FROM TblConfig WHERE _key = 'setting1'
SELECT ISNULL(@setting, 'default')
COALESCE 내부의 표현식은 NULL 여부 확인을 위해 여러 번 평가될 수 있다. 비용이 큰 서브쿼리는 변수로 먼저 받아두는 게 좋다.
{: .prompt-warning }
ISNULL과 COALESCE의 핵심 차이를 정리하면:
단순히 "ISNULL이 빠르니까"로 선택하지 말고, 데이터 타입과 길이가 다른지 먼저 확인하자. 타입이 다르면 COALESCE가 안전하고, 같으면 ISNULL을 써도 무방하다.