字符串和日期,逐个引擎看

字符串和日期函数在不同数据库中的名称和行为差异很大。本课比较各引擎实现相同结果时需要使用的语法。

把文本接起来

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 用 +,它的歧义在另一个方向:两个数值列相遇时它是加法。

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(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() 基本够用。日期格式化是移植不了的,而且通常更适合放到应用里做——那里本来就有用户的区域设置和时区。