본문으로 건너뛰기
안동민 개발노트 아이콘

안동민 개발노트

본문 시작
4장 : 값 가공·집계

문자열·NULL·날짜 함수

표준 함수와 MySQL 전용 함수를 구분하며 문자열 결합, NULL 대체, 날짜 차이를 안전하게 표현합니다.

게시글 카드에 ‘안동민 · SQL WHERE · 2026-07-13’ 같은 문구를 만들 때 문자열 하나라도 NULL이면 전체 문구가 사라질 수 있습니다.

날짜 차이도 단순 문자열 자르기로 계산하면 월말과 시간대 경계에서 깨집니다.

함수는 편의 문법이 아니라 입력 부재와 타입 변환 규칙을 함께 가진 연산입니다.

회원 게시판의 name, title, view_count, created_at, published_at을 사용해 표시 문자열과 경과 시간을 만듭니다.

저장값을 꾸미는 함수와 검색 조건식을 섞지 않고, 표준 COALESCE와 MySQL 전용 CONCAT_WS, TIMESTAMPDIFF, DATE_FORMAT의 경계를 표시합니다.


함수 예제 데이터

날짜와 경과 시간이 이전 장의 INSERT 순서에 따라 달라지지 않도록 회원 두 명과 게시글 세 건을 전용 테이블에 고정합니다.

ch4-2 함수 fixture
DROP TABLE IF EXISTS ch4_function_post_demo;
DROP TABLE IF EXISTS ch4_function_member_demo;

CREATE TABLE ch4_function_member_demo (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  name VARCHAR(80) NOT NULL
) ENGINE = InnoDB;

CREATE TABLE ch4_function_post_demo (
  id BIGINT UNSIGNED NOT NULL PRIMARY KEY,
  author_id BIGINT UNSIGNED NOT NULL,
  title VARCHAR(80) NOT NULL,
  view_count BIGINT UNSIGNED NOT NULL,
  created_at DATETIME(6) NOT NULL,
  published_at DATETIME(6) NULL,
  FOREIGN KEY (author_id) REFERENCES ch4_function_member_demo (id)
) ENGINE = InnoDB;

INSERT INTO ch4_function_member_demo (id, name)
VALUES (1, '안동민'), (2, '이하나');

INSERT INTO ch4_function_post_demo
  (id, author_id, title, view_count, created_at, published_at)
VALUES
  (1, 1, 'SELECT 기초', 30, '2026-07-13 08:00:00', '2026-07-13 08:30:00'),
  (2, 1, 'WHERE 조건', 50, '2026-07-13 09:00:00', '2026-07-13 10:00:00'),
  (3, 2, 'JOIN 계획', 40, '2026-07-13 11:00:00', NULL);

CONCAT과 NULL

MySQL의 CONCAT은 인자 중 하나가 NULL이면 전체 결과가 NULL입니다.

선택적인 게시일을 마지막에 붙인 다음 쿼리는 아직 게시하지 않은 정상 초안의 카드까지 모두 사라지게 만듭니다.

NULL 하나가 전체 문자열을 지우는 결합
SELECT
  p.id,
  CONCAT(
    m.name,
    ' · ',
    p.title,
    ' · ',
    DATE_FORMAT(p.published_at, '%Y-%m-%d')
  ) AS label
FROM ch4_function_post_demo AS p
JOIN ch4_function_member_demo AS m ON m.id = p.author_id
ORDER BY p.id;

게시일이 NULL인 행의 표시명이 부분 문자열이 아니라 NULL 전체로 반환됩니다.

오류 실행 결과
id | label
1  | 안동민 · SELECT 기초 · 2026-07-13
2  | 안동민 · WHERE 조건 · 2026-07-13
3  | NULL

NULL은 ‘빈 글자’가 아니므로 CONCAT이 임의로 생략하지 않습니다.

표시에서 생략하려면 CONCAT_WS의 NULL 무시 규칙을 선택하거나 COALESCE(DATE_FORMAT(published_at, '%Y-%m-%d'), '미게시')로 대체 의미를 명시합니다.

둘은 결과가 다릅니다.

전자는 항목 자체를 생략하고 후자는 부재 사실을 사용자에게 보여 줍니다.


함수의 NULL·타입·지역 규칙

문자열 함수는 결합, 자르기, 공백 정리, 대소문자 변환처럼 결과 표현을 바꿉니다.

날짜 함수는 저장 타입을 날짜 단위로 해석하며 문자열 포맷은 표시용 결과를 만듭니다.

COALESCE(a,b,c)는 왼쪽부터 첫 비NULL 값을 선택하므로 후보 우선순위를 표현합니다.

함수가 컬럼을 감싸면 검색과 정렬에서 기존 인덱스 표현과 달라질 수 있으므로 SELECT 표시와 WHERE 탐색을 구분합니다.

COALESCE, LOWER, UPPER, TRIM, SUBSTRING, CAST, EXTRACT는 표준 SQL 계열에서 널리 쓰입니다.

CONCAT_WS, IFNULL, DATE_FORMAT, TIMESTAMPDIFF는 MySQL 전용 또는 제품별 문법 차이가 큽니다.

교재 예제는 전용 함수를 표시하고 이식이 필요한 코드에서는 표준 대안을 우선합니다.


생략과 대체값 선택

카드의 필수 항목은 항상 결합하고 선택적인 게시일은 NULL일 때 생략합니다.

실제 경과 시간은 게시된 글에서만 계산하고 단위를 별칭에 남깁니다.

NULL 안전 카드와 경과 시간
SELECT
  p.id,
  CONCAT_WS(
    ' · ',
    m.name,
    p.title,
    CONCAT(p.view_count, '회'),
    DATE_FORMAT(p.published_at, '%Y-%m-%d')
  ) AS post_label,
  CASE
    WHEN p.published_at IS NULL THEN NULL
    ELSE TIMESTAMPDIFF(MINUTE, p.created_at, p.published_at)
  END AS elapsed_minutes
FROM ch4_function_post_demo AS p
JOIN ch4_function_member_demo AS m ON m.id = p.author_id
ORDER BY p.id;

게시일이 없어도 필수 문자열은 유지되고, 미게시 글의 경과 시간은 계산 불가인 NULL로 남습니다.

개선 실행 결과
id | post_label                              | elapsed_minutes
1  | 안동민 · SELECT 기초 · 30회 · 2026-07-13 | 30
2  | 안동민 · WHERE 조건 · 50회 · 2026-07-13  | 60
3  | 이하나 · JOIN 계획 · 40회                 | NULL

view_count는 게시글이 조회된 누적 횟수이고 elapsed_minutes는 작성부터 게시까지의 벽시계 차이입니다.

두 값은 단위와 의미가 완전히 다릅니다.

함수로 숫자를 만들었다고 같은 의미가 되는 것이 아니므로 이름과 설명을 분리합니다.


문자열 가공 결과 비교

검색용 문자열을 만들기 전에 앞뒤 공백과 대소문자가 실제 데이터에 얼마나 있는지 진단합니다.

UPDATE로 덮어쓰기 전에 원본과 변환 결과를 함께 봅니다.

표시 정규화 후보 조회
SELECT
  id,
  name AS original_name,
  TRIM(name) AS trimmed_name,
  CHAR_LENGTH(name) AS original_length,
  CHAR_LENGTH(TRIM(name)) AS trimmed_length
FROM ch4_function_member_demo
WHERE name <> TRIM(name);

결과가 있는 경우 입력 경로, 기존 데이터 보정, 앞으로의 제약을 함께 결정합니다.

SELECT에서 매번 TRIM해 보이는 것만 고치면 저장 오염은 계속되고 다른 소비자는 원본 공백을 받습니다.

날짜를 DATE_FORMAT 문자열로 바꾸면 사람이 읽기 쉽지만 날짜 타입의 정렬·범위 의미를 잃습니다.

API에서는 가능한 한 ISO 형식과 시간대 정보를 가진 원본을 전달하고, 지역화 표시는 클라이언트가 맡는 편이 안전할 수 있습니다.

데이터베이스에서 월별 그룹 키를 만들 때는 표시 문자열과 정렬 키를 분리합니다.

함수 실습에는 한글, 영문, 이모지, 앞뒤 공백, 빈 문자열, NULL을 모두 넣습니다.

LENGTH는 바이트 길이, CHAR_LENGTH는 문자 수라는 MySQL 차이를 직접 확인해야 다국어 길이 제한을 오해하지 않습니다.

날짜 차이에는 같은 시각, 작성 전 게시 오류, 자정을 넘겨 게시한 글, 장기간 초안으로 둔 글을 넣습니다.

서버 세션의 time_zone이 TIMESTAMP 해석에 영향을 줄 수 있으므로 연결 설정을 기록하고 DATETIME과 TIMESTAMP의 선택 이유를 스키마 문서에 남깁니다.

함수로 개인정보를 마스킹한 결과만 조회한다고 원본 접근 권한이 사라지는 것은 아닙니다.

SQL을 실행하는 계정은 원본 컬럼을 읽을 수 있으므로 뷰 권한이나 별도 노출 계층을 검토합니다.

문자열 정규화는 로그인 이메일, 표시 이름, 검색 키마다 요구가 다릅니다.

모든 문자열을 LOWER/TRIM하는 공통 규칙은 원본 표기 보존을 해칠 수 있습니다.

저장 원본, 비교용 정규화 값, 표시 값을 분리할 필요가 있는지 판단합니다.

COALESCE 후보들의 타입이 다르면 MySQL이 공통 타입을 추론해 예상치 못한 문자열·숫자 변환을 할 수 있습니다.

기본값 리터럴도 컬럼과 같은 의미의 타입을 사용합니다.

CONCAT_WS는 NULL을 건너뛰지만 빈 문자열은 건너뛰지 않으므로 빈 값 정책을 별도로 정합니다.

날짜 포맷 문자열을 동적으로 받으면 지원 토큰과 결과 길이를 제한합니다.

함수 선택은 짧은 SQL보다 데이터 의미와 이식 범위를 우선합니다.

회원 게시판 모델은 원본 문자열을 보존하면서 표시 변환을 SELECT에 둡니다.

회원 이름의 앞뒤 공백은 입력 오류라면 보정 대상이지만, 게시글 본문의 줄바꿈과 공백은 작성 내용이므로 같은 TRIM 정책을 적용하지 않습니다.

게시 시각은 계산과 정렬에 쓸 타입으로 유지하고 카드용 날짜 문자열은 결과 경계에서만 만듭니다.

이렇게 저장·비교·표시의 세 층을 분리하면 이후 뷰를 만들 때도 어떤 함수가 재사용 규칙이고 어떤 함수가 한 화면의 장식인지 결정하기 쉽습니다.

함수 목록을 외우기보다 입력 부재, 결과 타입, 표준 여부를 매번 확인하는 습관이 더 오래 유지됩니다.


데이터 가공의 책임 범위

판단 축확인할 질문
부재NULL을 생략할지 대체 문구로 보여 줄지 정했는가?
타입함수 입력과 결과 타입이 소비자 규칙과 맞는가?
표준MySQL 전용 함수의 이식 비용을 표시했는가?
탐색WHERE 컬럼을 함수로 감싸 인덱스 경로를 바꾸지 않았는가?
지역문자 정렬 규칙과 날짜 time_zone 정책이 명시됐는가?

표시 편의를 위한 함수는 SELECT 가장자리에서 사용하고, 저장 무결성과 검색 정규화는 입력·스키마 단계에서 해결하는 것을 기본으로 합니다.

함수가 반복되면 복사보다 의미 있는 이름의 뷰나 생성 컬럼을 검토합니다.


연습 문제

회원 이름, 게시글 제목, 조회수를 /로 결합하세요.

게시된 글은 YYYY-MM-DD 게시일을 붙이고 미게시 글은 ‘미게시’를 붙이세요.

해설과 예시 답안

날짜 표시는 MySQL DATE_FORMAT을 의도적으로 사용합니다.

SELECT
  CONCAT_WS(
    ' / ',
    m.name,
    p.title,
    CONCAT(p.view_count, '회'),
    COALESCE(DATE_FORMAT(p.published_at, '%Y-%m-%d'), '미게시')
  ) AS post_summary
FROM ch4_function_post_demo AS p
JOIN ch4_function_member_demo AS m ON m.id = p.author_id
ORDER BY p.id;

게시일이 있는 글과 없는 글에서 카드의 나머지 항목이 유지되는지 확인합니다.


핵심 정리

  • CONCAT의 NULL 전파와 CONCAT_WS의 NULL 생략은 다른 정책입니다.
  • COALESCE는 첫 비NULL 후보를 선택하고 빈 문자열은 별도로 처리합니다.
  • 날짜 타입은 계산용으로 유지하고 포맷 문자열은 표시 경계에서 만듭니다.
  • 표준 함수와 MySQL 전용 함수의 범위를 코드에 드러냅니다.

다음 문서에서는 여러 행을 하나의 통계로 축약하는 집계 함수와 GROUP BY를 배웁니다.