빌드노트
PostgreSQL에서 이름을 합쳤는데 전체가 NULL이 된다
이름 일부가 NULL이면 ||로 합친 이름 전체가 NULL이 된다. PostgreSQL 18.6에서 concat·concat_ws·COALESCE의 결과와 남는 공백을 비교하고, 빈 문자열도 생략하거나 이름이 모두 없을 때 대체 문구를 표시하는 식을 설명한다.
PostgreSQL에서 family_name || ' ' || given_name으로 표시할 이름을 만들면, 어느 한쪽이 NULL인 행은 이름 전체가 NULL이 된다. 성이 있어도 조립한 결과에는 남지 않는다. text 문자열을 합치는 ||가 NULL을 결과로 전파하기 때문이다.
있는 부분만 표시하기로 했다면 concat_ws(' ', family_name, given_name)을 쓸 수 있다. NULL인 이름 부분을 빼고 남은 값 사이에만 공백을 넣는다. 반대로 어느 부분이든 미정이면 전체 이름도 미정이어야 한다면 ||의 결과를 유지하는 편이 그 뜻에 맞다. 먼저 정할 것은 누락된 이름을 어떻게 보여 줄지다.
concat(family_name, ' ', given_name)도 NULL 인자를 생략하지만, 가운데 넣은 ' '는 별도의 값이라 남는다. concat_ws는 첫 번째 인자를 구분자로 취급한다는 차이가 있다. PostgreSQL 18 공식 문서도 두 함수의 NULL 인자 생략과 구분자의 역할을 설명한다. 문자열 함수와 연산자 문서 (새 탭에서 열림)
NULL과 빈 문자열을 같은 입력에서 비교한다
아래는 합성 이름을 넣어 PostgreSQL 18.6 전용 컨테이너에서 실행한 결과다. 직접 확인하려면 PostgreSQL 테스트 DB에 접속할 수 있는 psql이 필요하다. 접속한 뒤 아래 입력을 실행하고, 뒤의 조회도 같은 세션에서 이어서 실행한다. 테이블을 만들거나 데이터를 변경하지 않는 조회다.
앞의 \pset 세 줄은 psql의 출력 형식을 맞춘다. quote_nullable는 결과를 읽기 위한 함수다. SQL NULL은 NULL, 빈 문자열은 '', 공백 하나는 ' '로 나타내므로 화면의 빈칸을 보고 추측할 필요가 없다.
\pset format unaligned
\pset fieldsep '|'
\pset footer off
WITH names (n, family_name, given_name) AS (
VALUES (1, 'Kim'::text, 'Min'::text),
(2, 'Kim', NULL), (3, NULL, 'Min'),
(4, 'Kim', ''), (5, '', 'Min'),
(6, NULL, NULL), (7, '', '')
)
SELECT n,
quote_nullable(family_name || ' ' || given_name) AS op,
quote_nullable(concat(family_name, ' ', given_name)) AS cat,
quote_nullable(concat_ws(' ', family_name, given_name)) AS ws,
quote_nullable(COALESCE(family_name, '') || ' ' ||
COALESCE(given_name, '')) AS filled
FROM names ORDER BY n;
출력 형식 설정 안내를 제외한 실제 조회 결과는 다음과 같았다. op는 ||, cat은 concat, ws는 concat_ws, filled는 각 NULL을 빈 문자열로 바꾼 뒤 ||로 붙인 결과다.
n|op|cat|ws|filled
1|'Kim Min'|'Kim Min'|'Kim Min'|'Kim Min'
2|NULL|'Kim '|'Kim'|'Kim '
3|NULL|' Min'|'Min'|' Min'
4|'Kim '|'Kim '|'Kim '|'Kim '
5|' Min'|' Min'|' Min'|' Min'
6|NULL|' '|''|' '
7|' '|' '|' '|' '
2·3번 행에서는 ||만 NULL이 된다. concat과 filled는 성이나 이름을 보존하지만 앞뒤 공백도 보존한다. NULL을 빈 문자열로 바꾸는 것과 구분자를 생략하는 것은 다른 처리다.
4·5번은 NULL 대신 빈 문자열을 넣은 행이다. 이때는 concat_ws도 공백을 남긴다. 빈 문자열은 길이가 0인 값이므로 NULL 생략 규칙의 대상이 아니다. 7번처럼 둘 다 빈 문자열이면 두 값 사이의 구분자 한 칸이 결과가 된다.
COALESCE의 위치도 결과를 바꾼다. COALESCE(family_name || ' ' || given_name, '')는 한쪽 NULL 때문에 이미 사라진 조립 결과 전체를 ''로 바꾼다. 남아 있던 성을 복구하지는 않는다. 'Kim'과 NULL로 실행했을 때 이 식은 '', NULL인 이름 부분을 먼저 바꾼 식은 'Kim '였다. 성을 남기고 구분자까지 정리할 목적이라면 concat_ws가 맞는지 판단한다.
구분자가 필요 없으면 concat(family_name, given_name)을 쓸 수 있다. 같은 일곱 입력에서 별도로 실행했을 때 1번은 'KimMin', 2번은 'Kim', 3번은 'Min', 6번은 ''였다. 공백을 넣을지 여부도 함수 선택에 포함된다.
자기 조회에서는 조립 전의 두 열도 함께 확인한다. 예를 들어 quote_nullable(family_name)과 quote_nullable(given_name)을 결과 열에 추가하면 NULL인지 빈 문자열인지 구별할 수 있다. text와 위 식을 그대로 썼는데 결과가 다르다면, 실제 입력과 SQL의 COALESCE 위치를 먼저 대조한다. DB에서는 예상대로 나오는데 화면에서만 다르면 드라이버가 받은 값과 화면의 표시 처리를 따로 확인해야 한다.
모든 이름이 NULL이어도 concat_ws 결과는 NULL이 아니다
6번의 ws는 NULL이 아니라 빈 문자열이었다. 따라서 COALESCE(concat_ws(' ', family_name, given_name), '(unknown)')만으로는 두 이름이 모두 NULL일 때 대체 문구가 나오지 않는다. 실제로 이 식도 ''를 반환했다. COALESCE는 첫 번째 NULL 아닌 인자를 고르므로 빈 문자열을 그대로 선택한다. COALESCE 문서 (새 탭에서 열림)
구분자에는 별도 경계가 있다. 다음 조회도 같은 환경에서 실행했다.
SELECT quote_nullable(concat_ws(NULL::text, 'Kim', 'Min')) AS null_sep,
quote_nullable(concat_ws('', 'Kim', 'Min')) AS empty_sep,
quote_nullable(concat(NULL::text, NULL::text)) AS all_null_cat,
quote_nullable(concat_ws(' ', NULL::text, NULL::text)) AS all_null_ws;
null_sep|empty_sep|all_null_cat|all_null_ws
NULL|'KimMin'|''|''
이름 두 값이 멀쩡해도 구분자가 NULL이면 concat_ws 결과가 NULL이었다. 구분자가 없다는 뜻으로는 빈 문자열 ''를 쓴다. 구분자를 열이나 설정값에서 가져오는 조회라면 그 값도 확인해야 한다. 공식 문서는 첫 번째 구분자 인자에 NULL을 쓰지 않도록 설명하며, 나머지 인자의 NULL 생략과 구별한다.
빈 문자열도 생략하려면 그 규칙을 명시한다
이름 열의 NULL과 ''를 모두 ‘표시할 부분 없음’으로 취급하고, 남은 이름이 하나도 없으면 대체 문구를 보여 주기로 했다고 하자. 그 경우에는 각 입력의 ''를 NULLIF로 NULL로 바꾼 뒤 조립한다. 조립 결과도 ''이면 다시 NULL로 바꿔야 COALESCE가 대체 문구를 고른다. NULLIF(a, b)는 두 값이 같을 때 NULL을 돌려준다. NULLIF 문서 (새 탭에서 열림)
SELECT quote_nullable(
COALESCE(NULLIF(concat_ws(' ', NULLIF(family_name, ''),
NULLIF(given_name, '')), ''),
'(unknown)')) AS display
FROM (VALUES (1, 'Kim'::text, ''::text),
(2, NULL, NULL), (3, '', ''), (4, 'Kim', ' '))
AS names(n, family_name, given_name)
ORDER BY n;
display
'Kim'
'(unknown)'
'(unknown)'
'Kim '
마지막 입력의 ' '는 빈 문자열이 아니라 공백 한 글자다. 이 식은 그것까지 지우지 않으므로 'Kim' 뒤에 구분자와 입력 공백이 합쳐져 두 칸이 남았다. 공백만 있는 값도 누락으로 볼지는 별도로 정해야 한다. 원래 입력을 보존해야 하는 데이터라면 표시용 식에만 이런 변환을 적용한다.
이번 확인 범위는 PostgreSQL 18.6의 text 값, NULL, 빈 문자열과 위 공백 입력에 대한 읽기 전용 SELECT다. 다른 DB의 같은 이름 함수, 고정 길이 char 열, 애플리케이션의 NULL 전달과 HTML 공백 표시 방식은 검증하지 않았다. SQL 결과가 NULL인지, 빈 문자열인지, 공백이 있는 문자열인지까지 확인한 뒤 화면 쪽 문제로 범위를 옮길 수 있다.