MariaDB / 함수 / CONCAT, CONCAT_WS / 문자열 합치는 함수

MariaDB의 CONCAT()은 인수를 그대로 이어 붙이고, CONCAT_WS()는 첫 인수로 지정한 구분자를 값 사이에 넣습니다. 일반 SQL 모드에서 CONCAT()은 인수 하나라도 NULL이면 전체 결과가 NULL이지만, CONCAT_WS()는 구분자 뒤의 NULL 값을 건너뜁니다. 빈 문자열은 NULL이 아니므로 구분자 처리까지 같다고 생각하면 안 됩니다.

CONCAT: 문자열을 그대로 이어 붙이기

CONCAT() 공식 문서의 기본 형태는 CONCAT(값1, 값2, …)입니다. 다음 조회는 테이블 없이 실행할 수 있습니다. SQL을 실행할 수 있는 MariaDB 접속만 준비하면 됩니다.

SELECT CONCAT('Maria', 'DB') AS product,
       CONCAT('김', ' ', '민수') AS full_name,
       CONCAT('item-', CAST(42 AS CHAR)) AS item_label,
       CONCAT('a', NULL, 'b') AS with_null;

결과는 MariaDB, 김 민수, item-42, NULL입니다. 공백도 자동으로 들어가는 것이 아니라 별도 문자열 인수로 넣었습니다. 숫자를 연결하면 문자열 표현으로 변환되므로, 표현 의도를 명확하게 하려면 CAST(… AS CHAR)를 사용할 수 있습니다. 문자열 연결은 산술 덧셈과 다르므로 + 연산자로 이름을 합치려고 하지 않습니다.

아래 설명은 Oracle 호환 모드가 아닌 일반 모드 기준입니다. 공식 문서는 Oracle 모드에서 CONCAT이 NULL을 건너뛰는 차이를 별도로 설명합니다. 기존 시스템의 결과가 다르면 SELECT @@session.sql_mode로 접속 모드를 확인하고, 결과를 맞추려고 운영 모드를 임의로 바꾸지는 마세요. 바이너리 인수가 섞인 경우 반환 문자열의 성격도 달라질 수 있습니다.

CONCAT_WS: 구분자와 NULL·빈 문자열의 차이

CONCAT_WS()의 WS는 With Separator를 뜻합니다. 첫 인수는 구분자이며, 그 뒤에 연결할 값을 넣습니다. 쉼표 뒤 공백까지 필요하다면 구분자를 단순한 쉼표가 아니라 쉼표와 공백을 함께 가진 문자열로 지정합니다.

SELECT CONCAT_WS(', ', '서울', '부산', '대전') AS city_list,
       CONCAT_WS('-', 'a', NULL, 'b') AS skip_null,
       CONCAT_WS('-', 'a', '', 'b') AS keep_empty,
       CONCAT_WS(NULL, 'a', 'b') AS null_separator,
       CONCAT('[', CONCAT_WS('-', NULL, NULL), ']') AS all_null;

city_list는 서울, 부산, 대전, skip_null은 a-b, keep_empty는 a--b입니다. 구분자 자체가 NULL인 null_separator는 NULL이며, 모든 값이 NULL인 all_null은 []입니다. 즉 CONCAT_WS는 NULL이 아닌 구분자를 사용하면 값들이 빠져 빈 문자열이 될 수도 있습니다. 빈 결과와 값이 없음을 나타내는 NULL을 같은 것으로 볼지는 애플리케이션의 요구에 맞게 정합니다.

실제 열 연결: 선택 항목이 없는 주소 만들기

다음은 실습용 learning 데이터베이스에서 CREATE·INSERT·SELECT 권한으로 실행합니다. concat_address_demo가 이미 있다면 다른 실습 이름을 사용하세요. 주소는 함수 동작을 보여 주기 위한 합성 데이터이며, district에는 NULL·빈 문자열·공백만 있는 문자열을 각각 넣었습니다.

USE learning;
CREATE TABLE concat_address_demo (
    id INT PRIMARY KEY,
    city VARCHAR(40) NOT NULL,
    district VARCHAR(40) NULL,
    street VARCHAR(80) NOT NULL
) DEFAULT CHARSET=utf8mb4;
INSERT INTO concat_address_demo VALUES
    (1, '서울', '중구', '세종대로 1'),
    (2, '부산', NULL, '중앙대로 2'),
    (3, '대전', '', '대학로 3'),
    (4, '광주', '   ', '문화로 4');

SELECT id,
       CONCAT(city, ' ', district, ' ', street) AS plain_join,
       CONCAT_WS(' ', city, district, street) AS with_separator,
       CONCAT_WS(' ', city, NULLIF(TRIM(district), ''), street) AS cleaned
FROM concat_address_demo ORDER BY id;

id=1은 세 결과가 모두 서울 중구 세종대로 1입니다. id=2의 plain_join은 NULL이지만 나머지는 부산 중앙대로 2입니다. id=3의 with_separator에는 대전과 대학로 사이에 공백 두 개가 남고, id=4는 district의 공백까지 연결됩니다. cleaned는 선택 항목 district의 양끝 공백을 TRIM으로 제거하고, 빈 결과를 NULLIF로 NULL로 바꿔 건너뛰게 하므로 각 주소 부분 사이에 공백 하나만 남습니다.

이 정리는 “선택 항목의 공백만 있는 값은 없는 항목으로 취급한다”는 규칙을 정한 예입니다. 원본 테이블을 수정하지 않고 조회 표현식에서만 바꿉니다. 의미 있는 앞뒤 공백을 보존해야 하는 데이터나 탭·줄바꿈·여러 유니코드 공백까지 다뤄야 하는 정규화 작업을 기본 TRIM 한 번으로 모두 해결했다고 생각하지 마세요.

NULL을 건너뛸지 자리와 대체 문구를 남길지 선택

선택 항목이 없어도 모든 필드 자리를 유지해야 한다면, 각 NULL을 빈 문자열로 바꾼 뒤 연결하는 방식이 더 적합할 수 있습니다. 반대로 화면용 주소처럼 없는 항목을 생략하려면 CONCAT_WS와 조건에 맞는 NULL 처리를 사용합니다. 필수 항목이 없다는 사실을 결과 전체의 NULL로 유지하는 것이 목적이라면 일반 모드의 CONCAT 동작을 이용할 수 있습니다.

SELECT CONCAT('city=', IFNULL(NULL, '(미입력)'), '; street=', '도로 1')
           AS explicit_missing,
       CONCAT_WS('|', 'a', IFNULL(NULL, ''), 'b') AS fixed_positions,
       CONCAT_WS('|', 'a', 0, 'b') AS numeric_zero;

결과는 city=(미입력); street=도로 1, a||b, a|0|b입니다. 0은 NULL이나 빈 문자열이 아니며 정상 값으로 남습니다. 여러 열을 한 문자열로 만든 뒤 다시 나눌 계획이라면 데이터 안의 구분자 처리와 인코딩 규칙도 필요합니다. CONCAT_WS로 쉼표를 넣었다는 사실만으로 따옴표·줄바꿈을 처리하는 올바른 CSV 파일이 만들어지지는 않습니다.

여러 행을 합치는 작업과의 차이

CONCAT과 CONCAT_WS는 한 행 안의 여러 값 또는 인수를 합치는 함수입니다. 여러 행의 값을 한 문자열로 집계하려는 작업은 GROUP_CONCAT 같은 별도 집계 함수의 범위입니다. 함수가 문자열을 만든다는 이유만으로 행을 줄이거나 그룹을 자동 생성하지는 않습니다.

조회 결과가 통째로 NULL이면 인수 중 NULL과 구분자 자체를 먼저 확인하고, 구분자가 두 번 보이면 빈 문자열·공백이 남았는지 살펴보세요. 표시용으로 만든 이름·주소 문자열을 고유한 식별자 대신 사용하거나, CONCAT으로 사용자 입력을 이어 붙여 SQL을 만드는 방식으로 확장하지 않습니다. 원본 열은 의미에 맞게 저장하고 연결은 필요한 조회·표시 지점에서 수행합니다.