Skip to content

FORMAT / TO_CHAR(字符串格式化) ​

格式化(Formatting)是把值「渲染」成人类可读文本的最后一公里:金额要带千位分隔符与货币符号、日期要按业务口径输出、日志要按固定模板拼装。PostgreSQL 提供两条路线:format() 处理模板占位符,to_char() 处理模式串(数字与日期各有自己的模式语言)。二者用途不同却常被混用,本节讲清什么时候该用哪个,以及数字模式下那些反直觉的规则。

前置知识

阅读本节前,建议先了解:SPLIT_PART / STRING_AGG

基础概念 ​

概念术语说明
函数式格式化format()%s / %I / %L 占位符,类似 printf
模式串Template / Patternto_char 的数字 / 日期模式语言
填充模式Fill Mode9(可省)、0(补零)、FM(去空格)
货币符号Currency SymbolL 按会话 lc_monetary 输出
符号位置Sign PositionS 前置、MI 后置、PR 尖括号、SG 带正号
罗马数字Roman NumeralsRN / rn 输出罗马数字
千位分隔Group Separator, 或 G,受 lc_numeric 影响
模板注入Template Injection%I / %L 防止拼接 SQL 被注入

两个函数的分工:

函数输入参数风格主要用途
format(fmt, ...)text 模板 + 任意值%s %I %L 占位符拼提示信息、生成 SQL、日志模板
to_char(value, fmt)numeric / date / timestamp / interval模式串金额、日期、时长的业务展示
to_number(text, fmt)text + 模式串模式串to_char 的逆操作,解析带格式数字
to_date(text, fmt)text + 模式串模式串解析非标准日期串

选择流程:

text
你要输出什么?
  ├─ 一段带变量的文本(消息 / SQL / 日志)
  │    └─ format()
  │         ├─ 值直接塞进去            → %s
  │         ├─ 塞的是标识符(表名/列名)→ %I(会自动加双引号)
  │         └─ 塞的是字面量(用户输入) → %L(会自动加单引号并转义)
  ├─ 数字 → 带千分位 / 补零 / 货币符号
  │    └─ to_char(numeric, '模式')
  ├─ 日期时间 → 指定格式
  │    └─ to_char(timestamp, '模式')
  └─ 把格式化文本解析回数值
       ├─ to_number(text, '模式')
       └─ to_date(text, '模式')

一句话记忆:format 管「模板拼装」,to_char 管「数值与日期的显示形态」。

语法与用法 ​

sql
-- 1) 模板式格式化
format(formatstr text [, formatarg any]) → text

-- 2) 数字 / 日期 → 文本
to_char(numeric, text) → text
to_char(timestamp, text) → text
to_char(timestamp with time zone, text) → text
to_char(interval, text) → text

-- 3) 文本 → 数值 / 日期
to_number(text, text) → numeric
to_date(text, text) → date

-- 4) 相关的引号工具
quote_ident(text)   → text   -- 需要时加双引号,安全引用标识符
quote_literal(text) → text   -- 加单引号并转义内部单引号
quote_nullable(text)→ text   -- 同上,但 NULL 返回字面 'NULL'

format() 的三种占位符:

占位符作用示例结果
%s直接转文本插入format('id=%s', 42)id=42
%I作为标识符,必要时加双引号format('%I', 'user name')"user name"
%L作为字面量,加单引号并转义format('%L', O''Neil)'O''Neil'
%%输出一个字面 %format('100%%')100%
%n$s指定第 n 个参数(PostgreSQL 9.1+,与 format() 同时引入)format('%2$s %1$s', 'a', 'b')b a
sql
SELECT
  format('user %s has %s orders', 'alice', 3)     AS plain,
  format('table: %I', 'orders')                   AS ident_plain,
  format('table: %I', 'order items')              AS ident_quoted,
  format('name = %L', 'O''Neil')                  AS literal_escape,
  format('progress: 100%%')                       AS percent,
  format('a %s c', NULL)                          AS null_arg;
text
            plain             |   ident_plain   |    ident_quoted     |    literal_escape    |    percent    | null_arg
------------------------------+-----------------+---------------------+----------------------+---------------+----------
 user alice has 3 orders      | table: orders   | table: "order items"| name = 'O''Neil'     | progress: 100%| a  c

注意最后一行:%s 遇到 NULL 输出空串(不是 NULL,也不是字面 NULL),这是与 concat 一致的「跳过 NULL」语义。

最小可运行示例 to_char:

sql
SELECT
  to_char(1234567.891, '999,999,999.99')     AS grouped,
  to_char(1234567.891, 'FM999,999,999.99')   AS fm_trimmed,
  to_char(1234567.891, '$999,999,999.99')    AS dollar,
  to_char(1234567.891, '000,000,000.00')     AS zero_padded,
  to_char(-123.45, '(999.99)')               AS paren_negative,
  to_char(0.5, 'FM990.00')                   AS fm_leading_zero;
text
        grouped        |    fm_trimmed    |       dollar       |   zero_padded    | paren_negative | fm_leading_zero
-----------------------+------------------+--------------------+------------------+----------------+-----------------
    1,234,567.89       | 1,234,567.89     |  $1,234,567.89     | 000,001,234,568  |    (123.45)    | 0.50

准备演示数据(示例库 demo):

sql
CREATE TABLE payments (
  id          integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no    varchar(32)   NOT NULL,
  amount      numeric(12,2) NOT NULL,
  fee         numeric(6,4),
  paid_at     timestamptz   NOT NULL,
  channel     varchar(16)   NOT NULL
);

INSERT INTO payments (order_no, amount, fee, paid_at, channel) VALUES
  ('SO-2026-0001', 577.00,  0.0060, '2026-01-05 09:12:33+08', 'alipay'),
  ('SO-2026-0002', 261.00,  0.0060, '2026-01-05 14:03:07+08', 'wechat'),
  ('SO-2026-0003', 293.50,  0.0055, '2026-02-11 20:45:59+08', 'card'),
  ('SO-2026-0004', 12888.00,0.0038, '2026-03-01 00:00:01+08', 'card'),
  ('SO-2026-0005', 0.99,    0.0100, '2026-03-15 23:59:59+08', 'wechat');
text
INSERT 0 5

详细说明 ​

数字模式:9 与 0 的区别 ​

这是 to_char 数字模式里最容易混淆的一对:

模式字符含义无该位时示例(输入 5)
9可选的数字位输出空格to_char(5,'99') → ' 5'
0必填的数字位,补零输出 0to_char(5,'00') → '05'
FM前缀修饰符,去首尾空格与补零—to_char(5,'FM99') → '5'
sql
SELECT
  '[' || to_char(5, '9999')    || ']' AS nine_mode,
  '[' || to_char(5, '0000')    || ']' AS zero_mode,
  '[' || to_char(5, 'FM9999')  || ']' AS fm_nine,
  '[' || to_char(5, 'FM0000')  || ']' AS fm_zero,
  '[' || to_char(5, '999990')  || ']' AS mixed,
  '[' || to_char(5, 'FM999990')|| ']' AS fm_mixed;
text
   nine_mode   | zero_mode | fm_nine | fm_zero |  mixed   | fm_mixed
---------------+-----------+---------+---------+----------+----------
 [    5]       | [0005]    | [5]     | [0005]  | [     5] | [5]

关键结论:9 不补零但补空格;0 补零。想要「不留空格的整数」必须用 FM 前缀。

FM 只作用于最外层且必须写在模式开头。多个 FM 无效。

符号与括号:S、MI、PL、PR、SG ​

模式含义to_char(123,'模式')to_char(-123,'模式')
S999前置符号(+/-)+123-123
999S后置符号123+123-
999MI负号后置,正数留空格 123123-
999PL正号后置,负数留空格123+ 123
999PR负数用尖括号包裹 123<123>
S9999SG前置符号 + 正号(SG 用于正数)+123-123
sql
SELECT
  to_char(123, 'S999')   AS s_pos,  to_char(-123, 'S999')  AS s_neg,
  to_char(123, '999S')   AS s_tail_pos, to_char(-123, '999S') AS s_tail_neg,
  to_char(123, '999MI')  AS mi_pos, to_char(-123, '999MI') AS mi_neg,
  to_char(123, '999PR')  AS pr_pos, to_char(-123, '999PR') AS pr_neg;
text
 s_pos | s_neg | s_tail_pos | s_tail_neg | mi_pos | mi_neg | pr_pos | pr_neg
-------+-------+------------+------------+--------+--------+--------+--------
 +123  |  -123 | 123+       | 123-       |  123   | 123-   |  123   | <123>

to_char 的数字输出常带前导空格

因为 9 用空格填充,to_char(123,'999') 得到 ' 123'(注意前导空格)。把它 || 拼进文本时会出现「莫名多一个空格」的 bug:

sql
SELECT '¥' || to_char(123, '999.00');       -- ¥ 123.00(中间有空格,不推荐)
SELECT '¥' || to_char(123, 'FM999.00');     -- ¥123.00(正确)

货币、千分位与小数点 ​

模式字符含义说明
,千位分隔符(模式里的定位符号)实际输出字符受 lc_numeric 影响
G同上,位置由 locale 决定与 , 等价但更「本地化」
D小数点,位置由 locale 决定与 . 等价但更「本地化」
L货币符号按 lc_monetary 输出
$固定美元符号与 locale 无关
sql
SELECT
  to_char(1234567.891, '999,999,999.99')   AS comma,
  to_char(1234567.891, '999G999G999D99')   AS locale_g,
  to_char(1234567.891, 'L999,999,999.99')  AS local_currency,
  to_char(1234567.891, '$999,999,999.99')  AS dollar,
  current_setting('lc_numeric')            AS lc_numeric,
  current_setting('lc_monetary')           AS lc_monetary;
text
       comma        |    locale_g      |  local_currency  |       dollar       | lc_numeric |      lc_monetary
--------------------+------------------+------------------+--------------------+------------+-----------------------
    1,234,567.89    |   1,234,567.89   | ¥1,234,567.89   |  $1,234,567.89     | C.UTF-8    | C.UTF-8

L 的输出依赖 lc_monetary。在 C / C.UTF-8 下通常输出 ¥ 或 $(视集群初始化时的 locale 而定);要让 L 稳定输出人民币符号,应显式设置会话或直接写死 '¥'。

罗马数字与特殊格式 ​

sql
SELECT
  to_char(2026, 'RN')     AS roman_upper,     -- MMXXVI
  to_char(2026, 'rn')     AS roman_lower,     -- mmxxvi
  to_char(4, 'FM9RN')     AS with_roman,      -- 4IV
  to_char(1234567, '9.99EEEE') AS sci_notation, -- 1.23e+06
  to_char(1234567, 'FM9.99EEEE') AS sci_fm;   -- 1.23e+06
text
 roman_upper | roman_lower | with_roman | sci_notation | sci_fm
-------------+-------------+------------+--------------+---------
 MMXXVI      | mmxxvi      | 4IV        |  1.23e+06    | 1.23e+06
  • RN / rn 只对 1~3999 的整数有意义。
  • EEEE 切换到科学计数法,仅用于 numeric / float 的展示,不建议用于金额。

to_char 的日期与时间模式 ​

日期模式与数字模式是两套完全不同的语言(同一个字符有不同含义,例如 D 在数字里是小数点、在日期里是「星期几」):

模式含义示例输出
YYYY4 位年份2026
YY2 位年份26
MM月份(01~12)03
MON月名缩写Mar
Month月名全称March
DD月内日(01~31)15
DDD年内日(001~366)074
DAY星期全称SUNDAY
DY星期缩写SUN
HH2424 小时制(00~23)23
HH12 / HH12 小时制11
MI分钟59
SS秒59
MS毫秒(000~999)123
US微秒(000000~999999)123456
TZ时区缩写CST
OF时区偏移+08
Q季度1
IWISO 周数11
IW/W月内周数3
sql
SELECT
  to_char(paid_at, 'YYYY-MM-DD')                   AS date_iso,
  to_char(paid_at, 'YYYY"年"MM"月"DD"日"')          AS date_cn,
  to_char(paid_at, 'Dy HH24:MI:SS')                AS datetime_short,
  to_char(paid_at, 'Day, DD Mon YYYY HH12:MI AM')  AS pretty,
  to_char(paid_at, 'Q')                            AS quarter,
  to_char(paid_at, 'DDD')                          AS day_of_year,
  to_char(paid_at, 'IW')                           AS iso_week,
  to_char(paid_at, 'TZ OF')                        AS tz_info
FROM payments
ORDER BY id
LIMIT 2;
text
  date_iso  |   date_cn    | datetime_short |          pretty          | quarter | day_of_year | iso_week | tz_info
------------+--------------+----------------+--------------------------+---------+-------------+----------+----------
 2026-01-05 | 2026年01月05日| Mon 09:12:33   | Monday   , 05 Jan 2026 09:12 AM | 1 | 005 | 02 | CST +08
 2026-01-05 | 2026年01月05日| Mon 14:03:07   | Monday   , 05 Jan 2026 02:03 PM | 1 | 005 | 02 | CST +08

中文模式里的「年月日」需要用双引号包起来

to_char(now(), 'YYYY年MM月DD日') 会解析失败或产生意外输出,因为 年 月 日 可能与模式字符冲突。正确写法是用双引号把它们标记为字面文本:

sql
SELECT to_char(now(), 'YYYY"年"MM"月"DD"日"');   -- 2026年09月24日

日期模式还有 FM 前缀,用于去掉 Month / DAY 等定长模式补的尾随空格:

sql
SELECT
  '[' || to_char(DATE '2026-03-15', 'Day')    || ']' AS padded,   -- [Sunday   ]
  '[' || to_char(DATE '2026-03-15', 'FMDay')  || ']' AS trimmed,  -- [Sunday]
  '[' || to_char(DATE '2026-03-15', 'Month')  || ']' AS month_pad,
  '[' || to_char(DATE '2026-03-15', 'FMMonth')|| ']' AS month_fm;
text
     padded      | trimmed |  month_pad  | month_fm
-----------------+---------+-------------+----------
 [Sunday   ]     | [Sunday]| [March    ] | [March]

format() 的模板注入防护 ​

format() 最有价值的用途不是「拼日志」,而是安全地生成动态 SQL。%I 会正确处理标识符,%L 会正确转义单引号。

拼接构造 SQL 等于开门揖盗

只要有一个来自用户输入的标识符或字面量是自己拼进 SQL 文本的,整个语句就不可信:

  • 标识符(表名、列名)必须过 %I 或 quote_ident(),否则 order items' WHERE 1=1 -- 之类的输入能改写语句结构;
  • 字面量(字符串值)必须过 %L、quote_literal() 或参数化查询,否则一个单引号就能闭合字符串并追加任意 SQL;
  • 更根本的解法是参数化:能用 EXECUTE ... USING 或客户端预编译语句时,优先用参数而不是 format() 拼值,%L 只是在无法参数化位置(标识符)或必须生成文本时的兜底。
sql
-- 危险:字符串拼接,遇到带引号的值直接注入
SELECT 'SELECT * FROM ' || 'orders' || ' WHERE customer = ''' || 'O''Neil' || '''';
-- 结果:SELECT * FROM orders WHERE customer = 'O'Neil'   ← 语法错误,可被注入

-- 安全:用 %I 与 %L
SELECT format('SELECT * FROM %I WHERE customer = %L', 'orders', 'O''Neil');
-- 结果:SELECT * FROM orders WHERE customer = 'O''Neil'
text
                     ?column?
-----------------------------------------------------
 SELECT * FROM orders WHERE customer = 'O''Neil'
工具何时自动加引号空值处理
%I标识符含大写、空格、保留字时NULL → 空串
%L总是(单引号)NULL → 字面 NULL(无引号)
%s从不NULL → 空串
quote_ident同上NULL → NULL
quote_literal总是NULL → NULL
quote_nullable总是NULL → 字符串 'NULL'
sql
SELECT
  quote_ident('orders')       AS qi_plain,
  quote_ident('Order Items')  AS qi_spaced,
  quote_literal('O''Neil')    AS ql_escape,
  quote_nullable(NULL)        AS qn_null,
  quote_literal(NULL)         AS ql_null,
  format('%L', NULL)          AS fmt_null_l;
text
 qi_plain |    qi_spaced     |   ql_escape    | qn_null | ql_null | fmt_null_l
----------+------------------+----------------+---------+---------+------------
 orders   | "Order Items"    | 'O''Neil'      | 'NULL'  |         | NULL

%L 遇到 NULL 时输出不带引号的 NULL(对 SQL 而言这正是空值字面量),而 quote_nullable(NULL) 输出带引号的字符串 'NULL'——两者用途不同,插入 SQL 用 %L 更合适。

to_number:to_char 的逆操作 ​

sql
SELECT
  to_number('1,234,567.89', '999,999,999.99')   AS parse_grouped,   -- 1234567.89
  to_number('$1,234.50', '$999,999.99')         AS parse_currency,  -- 1234.50
  to_number('  42  ', '999')                    AS parse_spaces,    -- 42
  to_number('12.5%', '99.9%')                   AS parse_percent,   -- 12.5(% 只是标记)
  to_number('abc', '999');                      -- 0(解析失败返回 0!)
text
 parse_grouped | parse_currency | parse_spaces | parse_percent | to_number
---------------+----------------+--------------+---------------+-----------
    1234567.89 |        1234.50 |           42 |          12.5 |         0

to_number 解析失败返回 0,不报错也不返回 NULL

这是数据清洗时最危险的行为:脏数据会被静默变成 0,进入金额字段后极难排查。务必先用正则或 CASE 校验:

sql
SELECT
  raw,
  CASE WHEN raw ~ '^-?\d+(\.\d+)?$'
       THEN to_number(raw, '999999999.99')
       ELSE NULL
  END AS safe_amount
FROM (VALUES ('12.50'), ('abc'), (''), ('1,234.00')) AS t(raw);
text
   raw    | safe_amount
----------+-------------
 12.50    |       12.50
 abc      |
          |
 1,234.00 |

format() 与 to_char() 的性能 ​

两者开销都远小于正则,但在大批量输出时仍有差异:

sql
\timing on

SELECT count(format('%s-%s', id, order_no)) FROM payments;
SELECT count(to_char(amount, 'FM999,999.99')) FROM payments;
SELECT count('SO-' || lpad(id::text, 4, '0')) FROM payments;
text
 count
-------
     5
Time: 0.412 ms

 count
-------
     5
Time: 0.531 ms

 count
-------
     5
Time: 0.207 ms

经验规律:纯拼接(||)最快,format 次之,to_char 稍慢(要解析模式串)。但差异在毫秒级,除极端热路径外不必为此牺牲可读性。

to_char 是 STABLE(因为依赖 lc_numeric / lc_monetary 等会话设置),不能用于表达式索引;format 在参数为常量时也是 STABLE。想在索引里固化格式化结果,只能用 || 与 lpad 这类 IMMUTABLE 操作。

函数易变性可否建表达式索引
|| 拼接IMMUTABLE可以
lpad / rpadIMMUTABLE可以
formatSTABLE不可以
to_char(数字/日期)STABLE不可以

实战示例 ​

场景一:订单对账单(金额 + 日期 + 序号) ​

sql
SELECT
  format('%s / %s', order_no, channel)                      AS tag,
  to_char(amount, 'FM999,999,990.00')                       AS amount_str,
  '¥' || to_char(amount, 'FM999,999,990.00')                AS amount_cny,
  to_char(fee * 100, 'FM990.00') || '%'                     AS fee_pct,
  to_char(paid_at, 'YYYY-MM-DD HH24:MI')                    AS paid_str,
  to_char(paid_at, 'FMDy')                                  AS weekday
FROM payments
ORDER BY paid_at;
text
            tag             |  amount_str   |  amount_cny   | fee_pct |     paid_str     | weekday
----------------------------+---------------+---------------+---------+------------------+---------
 SO-2026-0001 / alipay      | 577.00        | ¥577.00       | 0.60%   | 2026-01-05 09:12 | Mon
 SO-2026-0002 / wechat      | 261.00        | ¥261.00       | 0.60%   | 2026-01-05 14:03 | Mon
 SO-2026-0003 / card        | 293.50        | ¥293.50       | 0.55%   | 2026-02-11 20:45 | Wed
 SO-2026-0004 / card        | 12,888.00     | ¥12,888.00    | 0.38%   | 2026-03-01 00:00 | Sun
 SO-2026-0005 / wechat      | 0.99          | ¥0.99         | 1.00%   | 2026-03-15 23:59 | Sun

注意 FM999,999,990.00 里的 0:它保证整数部分至少输出到个位,避免 0.99 被渲染成 .99。这是金额格式化里最容易被忽略的细节。

场景二:按月汇总的对账报表 ​

sql
SELECT
  to_char(paid_at, 'YYYY-MM')                       AS month,
  to_char(sum(amount), 'FM9,999,999,990.00')        AS total,
  count(*)                                          AS orders,
  to_char(avg(amount), 'FM990.00')                  AS avg_amount
FROM payments
GROUP BY 1
ORDER BY 1;
text
  month   |     total      | orders | avg_amount
----------+----------------+--------+------------
 2026-01  | 838.00         |      2 | 419.00
 2026-02  | 293.50         |      1 | 293.50
 2026-03  | 12,888.99      |      2 | 6444.50

to_char(paid_at,'YYYY-MM') 也可以直接写 GROUP BY 1,因为 to_char 的结果是文本,按月分组时按字典序排也正确。

场景三:生成可审计的动态 SQL ​

sql
-- 用 format 拼装带标识符与字面量的动态 SQL,避免注入
DO $$
DECLARE
  v_table  text := 'orders';
  v_cust   text := 'O''Neil';
  v_sql    text;
BEGIN
  v_sql := format(
    'SELECT count(*) FROM %I WHERE customer = %L',
    v_table, v_cust
  );
  RAISE NOTICE 'SQL: %', v_sql;
END $$;
text
NOTICE:  SQL: SELECT count(*) FROM orders WHERE customer = 'O''Neil'
DO

把这段逻辑放进函数里,配合 EXECUTE ... USING 更安全:

sql
CREATE OR REPLACE FUNCTION count_orders_by_customer(p_customer text)
RETURNS bigint
LANGUAGE plpgsql
AS $$
DECLARE
  v_count bigint;
BEGIN
  -- 标识符用 %I 校验,值用参数绑定(USING)而非拼进字符串
  EXECUTE format('SELECT count(*) FROM %I WHERE customer = $1', 'orders')
    INTO v_count
    USING p_customer;
  RETURN v_count;
END $$;

SELECT count_orders_by_customer('alice') AS alice_orders,
       count_orders_by_customer('O''Neil') AS oneil_orders;
text
 alice_orders | oneil_orders
--------------+--------------
            1 |            0

场景四:to_number 与 to_date 解析非标准输入 ​

sql
SELECT
  to_number('1.234,56', '9G999D99')                    AS eu_format,
  to_date('20260315', 'YYYYMMDD')                      AS compact_date,
  to_date('15/03/2026', 'DD/MM/YYYY')                  AS slash_date,
  to_date('Mar 15, 2026', 'Mon DD, YYYY')              AS named_month;
text
 eu_format | compact_date | slash_date | named_month
-----------+--------------+------------+-------------
   1234.56 | 2026-03-15   | 2026-03-15 | 2026-03-15

上例的 9G999D99 在 . 为小数点、, 为千分位的 locale 下解析 1.234,56 时会得到 1234.56(G 匹配千分位、D 匹配小数点)。这依赖 locale 设置,跨环境时结果可能不同,生产代码里应显式验证 locale,或改用 replace 预处理分隔符再解析。

注意事项 ​

  1. format 的 %s 遇 NULL 输出空串,无法与空串区分:

    sql
    SELECT format('a%sb', NULL);      -- 'ab'
    SELECT format('a%sb', '');        -- 'ab'
  2. %L 遇 NULL 输出不带引号的 NULL,而 %s 输出空串:

    sql
    SELECT format('%L', NULL);        -- NULL
    SELECT format('%s', NULL);        -- (空串)
  3. % 必须写成 %%,否则会被当成占位符:

    sql
    SELECT format('100% done');       -- 错误!报错 too few arguments
    SELECT format('100%% done');      -- 正确:100% done
  4. format 的参数个数不足会报错,多余参数会被忽略:

    sql
    SELECT format('%s %s', 'a');      -- 错误!too few arguments for format()
    SELECT format('%s', 'a', 'b');    -- 'a'(多余参数静默忽略)
  5. to_char 的数字模式里 9 会补空格,拼接时会出现多余空格:

    sql
    SELECT '¥' || to_char(123, '999.00');     -- 不推荐:'¥ 123.00'
    SELECT '¥' || to_char(123, 'FM999.00');   -- 正确:'¥123.00'
  6. 不写 0 会导致小数点前缺位:

    sql
    SELECT to_char(0.99, 'FM99.99');    -- 错误!'.99'
    SELECT to_char(0.99, 'FM990.99');   -- 正确:'0.99'
  7. FM 必须写在模式最前面,写在中间无效:

    sql
    SELECT to_char(5, '999FM9');        -- 无效写法,FM 不生效
    SELECT to_char(5, 'FM9999');        -- 正确:'5'
  8. to_char 的日期模式里普通汉字要加双引号,否则可能被误解析:

    sql
    -- 正确写法
    SELECT to_char(now(), 'YYYY"年"MM"月"DD"日"');
  9. to_char 的 Day / Month 等定长模式会补空格,直接比较会失败:

    sql
    SELECT to_char(DATE '2026-03-15', 'Day') = 'Sunday';   -- false(右侧少了空格)
    SELECT to_char(DATE '2026-03-15', 'FMDay') = 'Sunday'; -- true
  10. to_number 解析失败返回 0,静默吞掉脏数据:

    sql
    SELECT to_number('abc', '999');   -- 0(不是 NULL,也不是错误)
  11. to_char 是 STABLE,不能建表达式索引。需要在索引里用格式化结果时,改用 lpad / || 等 IMMUTABLE 操作。

  12. L 货币符号依赖 lc_monetary,跨环境结果可能不同。要稳定输出人民币符号应直接写 '¥' 字面量。

  13. 数字模式的长度决定输出宽度,模式位数不够会输出 #:

    sql
    SELECT to_char(12345, '999');    -- '###'(位数不足用 # 填充!)
    SELECT to_char(12345, '99999');  -- ' 12345'
  14. %I 加引号的规则是「必要时」,小写且非保留字的标识符不加引号。若要强制加引号,需自行拼接:

    sql
    SELECT format('%I', 'orders');      -- orders(无引号)
    SELECT format('%I', 'Order');       -- "Order"(有大写,加引号)

最佳实践 ​

  1. 金额、日期等业务展示统一用 to_char 封装,不要在应用层各写各的格式串。用视图把格式化结果固化下来:

    sql
    CREATE OR REPLACE VIEW v_payment_display AS
    SELECT
      id,
      order_no,
      '¥' || to_char(amount, 'FM999,999,990.00') AS amount_display,
      to_char(paid_at, 'YYYY-MM-DD HH24:MI')     AS paid_display
    FROM payments;
  2. 金额格式化模板固定为 FM999,999,990.00(或按业务位宽调整),其中的 0 保证小数点前至少一位,FM 去掉前导空格。把这条写进团队规范。

  3. 动态 SQL 一律用 format + %I / %L,绝不用 || 拼接用户输入。值优先用 EXECUTE ... USING 绑定参数,%L 作为次选。

  4. to_number 前必须先校验格式。用正则 ~ '^-?\d+(\.\d+)?$' 过滤,或改用 NULLIF 包一层,避免 0 静默污染。

  5. to_char 的输出不要参与后续计算。它是文本,'1,234.56' 无法直接做算术。计算用原始 numeric 列,只在最终展示时格式化。

  6. 日期格式化优先用 ISO 风格 YYYY-MM-DD。它是可排序的、无歧义的、跨 locale 稳定的;只有面向终端用户展示时才用本地化格式。

  7. 需要手动加引号的标识符用 %I,需要 NULL 字面量的用 quote_nullable,分清两者对 NULL 的处理差异。

  8. format 的模板字符串写成长常量并配注释,说明每个占位符的预期类型:

    sql
    -- %s = order_no, %s = channel, %L = customer
    format('%s/%s customer=%L', order_no, channel, customer)
  9. 避免在 WHERE 条件里对列调用 to_char。这是索引杀手:

    sql
    -- 不推荐!无法用 paid_at 上的索引
    SELECT * FROM payments WHERE to_char(paid_at, 'YYYY-MM') = '2026-01';
    -- 正确:用范围条件
    SELECT * FROM payments
    WHERE paid_at >= '2026-01-01' AND paid_at < '2026-02-01';
  10. format 的日志模板统一收口到一个函数,便于统一加前缀、脱敏、控制长度。

  11. 测试用例要覆盖边界值:金额 0、0.99、负数、超大数;日期 0001-01-01、闰年 2 月 29 日、跨时区。to_char 在这些点上行为差异明显。

  12. 跨 locale 的环境要在会话内显式设置,避免开发机与生产机输出不一致:

    sql
    SET lc_numeric = 'C';
    SET lc_monetary = 'C';

下一节

继续学习:ABS / SIGN / ROUND / TRUNC

参考链接 ​