把文本接起来
1
SELECT first_name || ' ' || last_name AS customer,
country
FROM customers
LIMIT 10;
结果 — 共 10 行,这里是前 5 行
| customer | country |
|---|---|
| Liam Fischer | ES |
| Mateo Costa | FR |
| Yuki Jang | DE |
| Ines Meyer | AU |
| Leo Novak | ES |
浏览器会另开一个标签页打开 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;
结果 — 共 10 行,这里是前 5 行
| order_number | ordered_at | month | day_first |
|---|---|---|---|
| ORD-2022-000001 | 2022-03-09 16:12:52 | 2022-03 | 09/03/2022 |
| ORD-2023-000002 | 2023-12-13 00:59:50 | 2023-12 | 13/12/2023 |
| ORD-2024-000003 | 2024-06-01 20:22:14 | 2024-06 | 01/06/2024 |
| ORD-2023-000004 | 2023-04-12 22:22:30 | 2023-04 | 12/04/2023 |
| ORD-2025-000005 | 2025-07-15 11:11:56 | 2025-07 | 15/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;
结果 — 共 15 行,这里是前 5 行
| order_number | ordered_at | delivered_at | days_to_deliver |
|---|---|---|---|
| ORD-2023-000152 | 2023-11-27 00:00:56 | 2023-12-07 22:33:00 | 10.9 |
| ORD-2025-001288 | 2025-10-18 04:20:05 | 2025-10-28 19:54:56 | 10.6 |
| ORD-2024-001816 | 2024-01-23 03:41:56 | 2024-02-02 18:40:52 | 10.6 |
| ORD-2023-002976 | 2023-05-23 02:17:37 | 2023-06-02 15:33:53 | 10.6 |
| ORD-2022-001345 | 2022-05-02 07:54:46 | 2022-05-12 19:12:56 | 10.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;
结果 — 共 10 行,这里是前 5 行
| id | first_name | phone | portable | sqlite_and_mysql |
|---|---|---|---|---|
| 2 | Mateo | NULL | (none) | (none) |
| 9 | Sven | NULL | (none) | (none) |
| 21 | Layla | NULL | (none) | (none) |
| 25 | Freya | NULL | (none) | (none) |
| 28 | Priya | NULL | (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() 基本够用。日期格式化是移植不了的,而且通常更适合放到应用里做——那里本来就有用户的区域设置和时区。