문자열과 날짜, 엔진별로

문자열과 날짜 함수는 엔진마다 이름과 동작이 크게 다릅니다. 같은 결과를 만들 때 MySQL, PostgreSQL, SQL Server, Oracle, SQLite에서 무엇을 써야 하는지 정리합니다.

텍스트 이어붙이기

1

SELECT first_name || ' ' || last_name AS customer,
       country
FROM customers
LIMIT 10;
Planche에서 실행

결과 — 10행 중 처음 5행

customercountry
Liam FischerES
Mateo CostaFR
Yuki JangDE
Ines MeyerAU
Leo NovakES

브라우저의 새 탭에서 Planche가 열리고, 샘플 데이터베이스가 로드된 채로 이 쿼리가 들어갑니다. 이 페이지의 다른 예제도 같은 탭을 다시 씁니다. F9를 누르기 전까지는 아무것도 실행되지 않습니다.

The same expression elsewhere

-- PostgreSQL, Oracle: identical to SQLite.
SELECT first_name || ' ' || last_name AS customer FROM customers;

-- MySQL / MariaDB: || means OR unless PIPES_AS_CONCAT is set.
SELECT CONCAT(first_name, ' ', last_name) AS customer FROM customers;

-- SQL Server: + concatenates, and CONCAT() exists from 2012.
SELECT first_name + ' ' + last_name AS customer FROM customers;
SELECT CONCAT(first_name, ' ', last_name) AS customer FROM customers;

The same expression elsewhere — 비교용입니다. 브라우저 샘플 데이터베이스는 SQLite라서 이 문장은 여기서 실행할 수 없습니다.

||는 표준 연산자이며 PostgreSQL, Oracle, SQLite에서 문자열을 잇는 데 씁니다. MySQL에서는 PIPES_AS_CONCAT 모드가 아니면 논리 OR로 해석해 오류 대신 0이나 1을 반환합니다. SQL Server는 +를 쓰지만, 숫자 컬럼끼리는 덧셈이 되므로 이 역시 주의가 필요합니다.

CONCAT()은 MySQL, MariaDB, PostgreSQL, SQL Server(2012 이상)에서 사용할 수 있습니다. 네 엔진을 오가는 쿼리라면 가장 무난한 선택입니다. NULL 처리도 연산자와 다릅니다.

표기뿐 아니라 동작도 다른 두 가지가 있습니다.

  • 연산자에서는 NULL이 전파됩니다. PostgreSQL과 SQLite에서 'a' || NULL은 NULL이고, SQL Server에서 'a' + NULL도 NULL입니다. 그래서 중간 이름 하나가 비면 그 컬럼 전체가 비어버립니다. CONCAT()은 NULL을 빈 문자열로 다룹니다. Oracle의 ||도 NULL을 무시합니다. 이유는 다음 항목에 있습니다.
  • Oracle은 빈 문자열을 NULL로 취급합니다. 거기서는 '' IS NULL이 참이고 다른 곳에서는 거짓입니다. 그래서 MySQL에서 행을 찾던 WHERE name = ''이 Oracle에서는 한 행도 찾지 못합니다.

날짜 포맷

3

SELECT order_number,
       ordered_at,
       strftime('%Y-%m', ordered_at) AS month,
       strftime('%d/%m/%Y', ordered_at) AS day_first
FROM orders
LIMIT 10;
Planche에서 실행

결과 — 10행 중 처음 5행

order_numberordered_atmonthday_first
ORD-2022-0000012022-03-09 16:12:522022-0309/03/2022
ORD-2023-0000022023-12-13 00:59:502023-1213/12/2023
ORD-2024-0000032024-06-01 20:22:142024-0601/06/2024
ORD-2023-0000042023-04-12 22:22:302023-0412/04/2023
ORD-2025-0000052025-07-15 11:11:562025-0715/07/2025

Formatting a date elsewhere

-- MySQL / MariaDB
SELECT DATE_FORMAT(ordered_at, '%Y-%m') AS month FROM orders;

-- PostgreSQL, Oracle
SELECT to_char(ordered_at, 'YYYY-MM') AS month FROM orders;

-- SQL Server (2012+)
SELECT FORMAT(ordered_at, 'yyyy-MM') AS month FROM orders;
-- ...or, much faster in bulk, and the usual choice for grouping:
SELECT CONVERT(char(7), ordered_at, 126) AS month FROM orders;

Formatting a date elsewhere — 비교용입니다. 브라우저 샘플 데이터베이스는 SQLite라서 이 문장은 여기서 실행할 수 없습니다.

함수 이름뿐 아니라 포맷 문자열도 다릅니다. strftime은 C 스타일 %Y-%m, DATE_FORMAT은 비슷한 퍼센트 표기, to_char는 YYYY-MM, FORMAT은 .NET의 yyyy-MM을 사용합니다. 마지막 표기는 대소문자도 구분합니다. mm은 월이 아니라 분입니다.

화면 표시가 아니라 그룹화에 쓸 값이라면 계산 비용도 고려하세요. SQL Server에서는 행이 많을 때 CONVERT(char(7), col, 126)이 FORMAT보다 빠르고, PostgreSQL에서는 date_trunc('month', col)이 to_char보다 효율적이며 결과도 날짜로 유지합니다. 어느 경우든 컬럼을 함수로 감싸면 해당 컬럼의 인덱스를 사용하기 어려울 수 있습니다.

Planche는 엔진이 반환한 날짜 값을 YYYY-MM-DD HH:MM:SS[.mmm] 형식으로 표시해 그리드 형식을 통일합니다. Oracle 드라이버도 NLS_DATE_FORMAT을 이에 맞춥니다. 따라서 직접 작성한 TO_CHAR 결과와 그리드의 날짜 값이 서로 다른 형식으로 보이지 않습니다.

두 날짜의 간격

5

SELECT o.order_number,
       o.ordered_at,
       s.delivered_at,
       ROUND(julianday(s.delivered_at) - julianday(o.ordered_at), 1) AS days_to_deliver
FROM orders o
JOIN shipments s ON s.order_id = o.id
WHERE s.delivered_at IS NOT NULL
ORDER BY days_to_deliver DESC
LIMIT 15;
Planche에서 실행

결과 — 15행 중 처음 5행

order_numberordered_atdelivered_atdays_to_deliver
ORD-2023-0001522023-11-27 00:00:562023-12-07 22:33:0010.9
ORD-2025-0012882025-10-18 04:20:052025-10-28 19:54:5610.6
ORD-2024-0018162024-01-23 03:41:562024-02-02 18:40:5210.6
ORD-2023-0029762023-05-23 02:17:372023-06-02 15:33:5310.6
ORD-2022-0013452022-05-02 07:54:462022-05-12 19:12:5610.5

Days between two dates elsewhere

-- MySQL / MariaDB: whole days, first argument minus second.
SELECT DATEDIFF(delivered_at, ordered_at) AS days FROM ...;

-- PostgreSQL: subtracting two timestamps gives an interval.
SELECT delivered_at - ordered_at AS elapsed FROM ...;
SELECT EXTRACT(DAY FROM delivered_at - ordered_at) AS days FROM ...;

-- SQL Server: the unit comes first, and the arguments are the other way round.
SELECT DATEDIFF(day, ordered_at, delivered_at) AS days FROM ...;

-- Oracle: subtracting two DATEs gives a number of days.
SELECT delivered_at - ordered_at AS days FROM ...;

Days between two dates elsewhere — 비교용입니다. 브라우저 샘플 데이터베이스는 SQLite라서 이 문장은 여기서 실행할 수 없습니다.

함수 이름만 다른 것이 아닙니다. MySQL의 DATEDIFF(a, b)는 a - b를 일 단위 정수로 반환합니다. SQL Server의 DATEDIFF(day, a, b)는 단위를 먼저 쓰고 b - a를 계산합니다. 두 엔진 사이에서 쿼리를 옮기면 인수 순서 때문에 결과 부호가 반대로 나올 수 있습니다. PostgreSQL과 Oracle은 뺄셈을 사용하며, 결과는 각각 interval과 일수입니다.

SQL Server의 DATEDIFF는 실제 경과 시간이 아니라 넘은 날짜 경계를 셉니다. DATEDIFF(day, '2025-01-01 23:00', '2025-01-02 01:00')은 두 시간 차이지만 날짜 경계를 하나 넘으므로 1입니다.

NULL 대체하기

7

SELECT id,
       first_name,
       phone,
       COALESCE(phone, '(none)') AS portable,
       IFNULL(phone, '(none)')   AS sqlite_and_mysql
FROM customers
WHERE phone IS NULL
LIMIT 10;
Planche에서 실행

결과 — 10행 중 처음 5행

idfirst_namephoneportablesqlite_and_mysql
2MateoNULL(none)(none)
9SvenNULL(none)(none)
21LaylaNULL(none)(none)
25FreyaNULL(none)(none)
28PriyaNULL(none)(none)

Replacing a NULL elsewhere

-- Everywhere, and the one to reach for:
SELECT COALESCE(phone, '(none)') FROM customers;

-- MySQL / MariaDB / SQLite
SELECT IFNULL(phone, '(none)') FROM customers;

-- SQL Server. Note ISNULL, not IFNULL, and it takes exactly two arguments.
SELECT ISNULL(phone, '(none)') FROM customers;

-- Oracle
SELECT NVL(phone, '(none)') FROM customers;

Replacing a NULL elsewhere — 비교용입니다. 브라우저 샘플 데이터베이스는 SQLite라서 이 문장은 여기서 실행할 수 없습니다.

COALESCE는 표준 함수로 대부분의 엔진에서 쓸 수 있으며, NULL이 아닌 첫 값을 반환합니다. 엔진별 함수는 인자를 두 개만 받습니다. MySQL과 SQLite는 IFNULL, SQL Server는 ISNULL을 씁니다. 새 쿼리라면 이식성이 좋은 COALESCE를 우선 고려하세요.

요약

여러 엔진에서 실행해야 한다면 CONCAT(), COALESCE(), CAST()부터 고려하세요. 날짜 포맷은 이식성이 낮으므로, 사용자 로케일과 시간대를 이미 아는 애플리케이션에서 처리하는 편이 나은 경우가 많습니다.