文字列と日付、エンジンごとの書き方

文字列と日付の関数は、データベースによって名前も動作も大きく異なります。同じ結果を得るための構文をエンジン別に整理します。

文字列をつなぐ

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 で動いていない限り)、MySQL ではこの式は失敗せずに 0 か 1 を返します。SQL Server は + を使いますが、こちらは逆向きに曖昧で、数値列2つなら足し算になります。

CONCAT() は MySQL、MariaDB、PostgreSQL、SQL Server(2012以降)で利用できます。この4つのエンジン間では、もっとも移植しやすい形です。ただし、NULL の扱いは演算子とは異なります。

綴りだけでなく、挙動が違うものが2つあります。

  • 演算子では NULL が伝播します。'a' || NULL は PostgreSQL と SQLite で NULL、'a' + NULL は SQL Server で NULL です。ミドルネームが1つ欠けているだけで列全体が空になります。CONCAT() は NULL を空文字として扱います。Oracle の || も NULL を無視しますが、それは次の点が理由です。
  • Oracle は空文字を NULL として扱います。そこでは '' IS NULL が真で、他のどのエンジンでも偽です。MySQL では行が見つかる WHERE name = '' が、Oracle では1行も返しません。

日付を書式化する

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 なので、この文はここでは実行できません。

関数名は4つ、パターン言語は3種類です。strftime は C 風の %Y-%m、DATE_FORMAT は同じパーセント記号でも一部の文字の意味が違い、to_char は YYYY-MM、FORMAT は .NET の yyyy-MM を使います。最後のものは大文字小文字が意味を持ちます。mm は分です。

表示ではなく集計に使うなら、そのエンジンが安く計算できる形を選んでください。SQL Server では行数が多いとき FORMAT より CONVERT(char(7), col, 126) のほうがずっと速く、PostgreSQL では date_trunc('month', col) が to_char より速いうえ結果が日付のままです。そしてインデックスのレッスンを思い出してください。列をこれらで包めば、その列のインデックスは使えなくなります。

Planche は、どのエンジンが返したものでも表示を揃えます。ドライバは日付を YYYY-MM-DD HH:MM:SS[.mmm] で描画してグリッドを統一し、Oracle ドライバは NLS_DATE_FORMAT をそれに合わせるので、自分で書いた TO_CHAR の結果と、その隣のグリッドの表示が食い違いません。

2つの日付の差

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) は2つの引数を取り、a - b を日数で返します。SQL Server の DATEDIFF(day, a, b) は単位を先に書き、b - a を返します。引数の順序が逆なので、片方からコピーしたクエリでは結果の符号が反転することがあります。PostgreSQL と Oracle は引き算を使い、それぞれ interval と日数を返します。

SQL Server 版はさらに、経過時間ではなく境界をまたいだ回数を数えます。DATEDIFF(day, '2025-01-01 23:00', '2025-01-02 01:00') は、2時間しか離れていなくても 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 を返します。エンジン固有のものはどれもちょうど2引数で、しかも2つは取り違えやすい名前です。MySQL と SQLite が IFNULL、SQL Server が ISNULL。新しいコードで COALESCE より優先する理由はありません。

まとめると

クエリが複数のエンジンで動く必要があるなら、CONCAT()、COALESCE()、CAST() でだいたい間に合います。日付の書式化は移植できません。ユーザーのロケールとタイムゾーンが手元にあるアプリ側でやったほうが、たいていうまくいきます。