ABS / SIGN / ROUND / TRUNC
取绝对值、判符号、四舍五入、截断小数,是数值处理中出现频率最高的一组操作。PostgreSQL 把它们拆成四个语义清晰的内建函数:abs() 求绝对值、sign() 取符号、round() 舍入、trunc() 向零截断。看似简单,但真正决定结果对错的往往不是函数本身,而是参数的数据类型——numeric 与 double precision 在同一函数下可能给出不同答案,而两参数形式的 round() 只接受 numeric。本节会把这些边界逐一验证清楚。
前置知识
阅读本节前,建议先了解:FORMAT / TO_CHAR(字符串格式化)
基础概念
先明确这几个函数各自解决的数学问题:
| 概念 | 术语 | 说明 |
|---|---|---|
| 绝对值 | Absolute Value | 数值到零的距离,永远非负,记作 |x| |
| 符号函数 | Sign Function | 返回 -1 / 0 / +1,表示方向而非大小 |
| 舍入 | Rounding | 按规则调整到指定精度,可能变大也可能变小 |
| 截断 | Truncation | 直接丢弃指定精度之后的位,向零靠拢 |
| 向零取整 | Round Toward Zero | 正数向下、负数向上,即丢弃小数部分 |
| 半程远离零 | Round Half Away From Zero | numeric 的舍入规则:恰好折半时向远离零的方向进位 |
| 半程取偶 | Round Half to Even | 浮点数常见的舍入规则,也叫银行家舍入 |
| 标度 | Scale | 小数点后的十进制位数,8.4100 的标度为 4 |
| 最小标度 | Min Scale | 精确表示该值所需的最少小数位,8.4100 的最小标度为 2 |
| 整数溢出 | Integer Overflow | 结果超出类型取值范围,PostgreSQL 直接报错而非回绕 |
| 参数类型决定返回类型 | Return Type Follows Argument | 同一函数名按参数类型分派到不同实现 |
四个函数一句话对照:
| 函数 | 数学含义 | numeric 示例 | 结果 | 返回类型 |
|---|---|---|---|---|
abs(x) | |x| | abs(-17.4) | 17.4 | 与入参同类型 |
sign(x) | -1 / 0 / 1 | sign(-8.4) | -1 | 与入参同类型 |
round(x) | 就近取整 | round(2.5) | 3 | 与入参同类型 |
round(x, s) | 保留 s 位小数 | round(42.4382, 2) | 42.44 | 恒为 numeric |
trunc(x) | 向零取整 | trunc(-2.7) | -2 | 与入参同类型 |
trunc(x, s) | 向零截到 s 位 | trunc(42.4382, 2) | 42.43 | 恒为 numeric |
选择流程可以概括为:
你要的是「保留几位」还是「变成整数」?
├─ 变成整数
│ ├─ 就近舍入(会计、显示)→ round(x)
│ └─ 直接砍掉小数(分页、切片)→ trunc(x)
└─ 保留小数位
├─ 会舍入 → round(x, s)
└─ 只截断 → trunc(x, s)一句话记忆:round 是「四舍五入」,trunc 是「一刀切」,两者在负数上最容易混淆。
语法与用法
-- 单参数形式:入参是什么类型,返回值就是什么类型
abs(x) -- x: smallint / integer / bigint / numeric / real / double precision
sign(x) -- 返回 -1 / 0 / 1,类型与入参一致
round(x) -- numeric → numeric,double precision → double precision
trunc(x) -- 向零取整,类型与入参一致
-- 双参数形式:只接受 numeric,返回恒为 numeric
round(v numeric, s integer) -- 保留 s 位小数,四舍五入
trunc(v numeric, s integer) -- 保留 s 位小数,直接截断
-- 相关的标度函数(均为 numeric 专用)
scale(numeric) -- 当前的十进制小数位数
min_scale(numeric) -- 能精确表示该值的最少小数位数
trim_scale(numeric) -- 去掉尾随零后的值
-- 操作符形式(与函数等价)
@ -5.0 -- 绝对值,等价于 abs(-5.0)最小可运行示例:
-- 四个函数的基本行为
SELECT
abs(-17.4) AS abs_num, -- 绝对值
sign(-8.4) AS sign_neg, -- 负数
sign(0) AS sign_zero, -- 零
sign(3.2) AS sign_pos, -- 正数
round(42.4382, 2) AS round_2, -- 保留两位
trunc(42.4382, 2) AS trunc_2; -- 截断两位 abs_num | sign_neg | sign_zero | sign_pos | round_2 | trunc_2
---------+----------+-----------+----------+---------+---------
17.4 | -1 | 0 | 1 | 42.44 | 42.43注意 round 得到 42.44,trunc 得到 42.43——第 3 位小数是 8,舍入会进位,截断不会。
准备本节贯穿使用的示例表:
-- 商品表:price / cost 用 numeric 保存金额,禁止使用 float
CREATE TABLE products (
id INTEGER GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sku TEXT NOT NULL,
name TEXT NOT NULL,
price NUMERIC(10, 2) NOT NULL CHECK (price >= 0),
cost NUMERIC(10, 2) NOT NULL,
stock INTEGER NOT NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO products (sku, name, price, cost, stock) VALUES
('KB-001', '机械键盘', 299.00, 180.50, 42),
('MS-002', '无线鼠标', 59.90, 25.00, 180),
('HP-003', '头戴耳机', 899.50, 610.25, 7),
('CB-004', 'Type-C 线', 19.90, 6.40, 320),
('ST-005', '笔记本支架', 129.00, 88.80, 0);-- 一次看清四个函数在同一批数据上的表现
SELECT
sku,
price,
round(price / 3, 2) AS round_third, -- 舍入到分
trunc(price / 3, 2) AS trunc_third, -- 截断到分
round(price * 0.88, 2) AS after_discount, -- 打八八折
sign(price - cost) AS profit_sign -- 是否盈利
FROM products
ORDER BY id; sku | price | round_third | trunc_third | after_discount | profit_sign
--------+--------+-------------+-------------+----------------+-------------
KB-001 | 299.00 | 99.67 | 99.66 | 263.12 | 1
MS-002 | 59.90 | 19.97 | 19.96 | 52.71 | 1
HP-003 | 899.50 | 299.83 | 299.83 | 791.56 | 1
CB-004 | 19.90 | 6.63 | 6.63 | 17.51 | 1
ST-005 | 129.00 | 43.00 | 43.00 | 113.52 | 1299.00 / 3 = 99.6666...,round 给出 99.67,trunc 给出 99.66,这就是「舍入」与「截断」最直观的差别。
详细说明
abs:绝对值与 int4 溢出陷阱
abs 的签名是「返回类型与入参一致」:
| 入参类型 | 返回类型 | 说明 |
|---|---|---|
smallint | smallint | 范围 -32768 ~ 32767 |
integer | integer | 范围 -2147483648 ~ 2147483647 |
bigint | bigint | 范围约 ±9.22e18 |
numeric | numeric | 任意精度,无溢出风险 |
real / double precision | 同入参 | 浮点,-0.0 会得到 0 |
核心陷阱在于有符号整数的最小值没有对应的正数。int4 的取值范围是 -2147483648 到 2147483647,绝对值 2147483648 装不回去:
-- 错误!int4 的最小值取绝对值会溢出
SELECT abs(-2147483648);ERROR: integer out of range修复方式只有一个:先提升类型再取绝对值。
-- 正确:先转成 bigint 或 numeric
SELECT
abs(-2147483648::bigint) AS as_bigint,
abs(-2147483648::numeric) AS as_numeric; as_bigint | as_numeric
---------------+---------------
2147483648 | 2147483648bigint 也有同样的问题,只是触发值更大:
-- 错误!bigint 的最小值同样溢出
SELECT abs((-9223372036854775808)::bigint);ERROR: bigint out of range而 numeric 没有这个问题,这也是金额、ID 差值这类场景应优先用 numeric 的原因之一。
abs 还可以写成操作符 @:
SELECT @ -5.0 AS op_abs, abs(-5.0) AS fn_abs; op_abs | fn_abs
--------+--------
5.0 | 5.0NaN 会被原样传播:
SELECT abs('NaN'::numeric) AS abs_nan, sign('NaN'::numeric) AS sign_nan; abs_nan | sign_nan
---------+----------
NaN | NaNsign:只有三种返回值的符号函数
sign 的结果只可能是 -1、0、1(外加 NaN 传播)。它回答的是「方向」问题,而不是「大小」问题。
SELECT
sign(-8.4::numeric) AS neg,
sign(0::numeric) AS zero,
sign(8.4::numeric) AS pos,
sign(-0.0001::numeric) AS tiny_neg, -- 极小的负数也是 -1
sign(0.0::numeric) AS zero_float,
sign(NULL::numeric) AS null_input; neg | zero | pos | tiny_neg | zero_float | null_input
-----+------+-----+----------+------------+------------
-1 | 0 | 1 | -1 | 0 |两个要点:
0是精确判断。numeric的0.0与0完全相同,sign返回0;但浮点数的「近似零」仍然会被判为负数或正数。NULL输入返回NULL,规则与其他标量函数一致。
sign 最实用的场合是把差值映射成业务状态:
-- 用 sign 把「涨跌平」映射成三种状态
SELECT
sku,
price - cost AS gross_profit,
sign(price - cost) AS direction,
CASE sign(price - cost)
WHEN 1 THEN '盈利'
WHEN 0 THEN '持平'
ELSE '亏损'
END AS status
FROM products
ORDER BY id; sku | gross_profit | direction | status
--------+--------------+-----------+--------
KB-001 | 118.50 | 1 | 盈利
MS-002 | 34.90 | 1 | 盈利
HP-003 | 289.25 | 1 | 盈利
CB-004 | 13.50 | 1 | 盈利
ST-005 | 40.20 | 1 | 盈利sign 通常比写三段 CASE WHEN x > 0 / x = 0 / x < 0 更简洁,也更容易被优化器识别成同一个表达式。
round:numeric 与 double precision 的舍入规则不同
这是本节最重要的一节。round 的行为由入参类型决定:
| 入参 | 规则 | 折半时的结果 |
|---|---|---|
numeric | 四舍五入(半程远离零) | round(2.5) → 3,round(-2.5) → -3 |
double precision | 平台相关,最常用的是「半程取偶」 | round(2.5::float8) → 2 |
先用一组对照实验看清 numeric 的规则:
-- numeric:半程一律向远离零的方向进位
SELECT
round(0.5::numeric) AS r_0_5,
round(1.5::numeric) AS r_1_5,
round(2.5::numeric) AS r_2_5,
round(-2.5::numeric) AS r_neg_2_5; r_0_5 | r_1_5 | r_2_5 | r_neg_2_5
-------+-------+-------+-----------
1 | 2 | 3 | -3再看 double precision:
-- float8:多数平台上采用「半程取偶」
SELECT
round(0.5::float8) AS r_0_5,
round(1.5::float8) AS r_1_5,
round(2.5::float8) AS r_2_5,
round(-2.5::float8) AS r_neg_2_5; r_0_5 | r_1_5 | r_2_5 | r_neg_2_5
-------+-------+-------+-----------
0 | 2 | 2 | -2同一批数字,numeric 得到 1 / 2 / 3 / -3,float8 得到 0 / 2 / 2 / -2。PostgreSQL 不做银行家舍入,但这句话只对 numeric 成立——浮点数是另一套规则,且结果依赖宿主机。
接下来是双参数形式,也是最多人踩的坑:
-- 错误!双参数 round 只接受 numeric,不接受 float8
SELECT round(2.675::float8, 2);ERROR: function round(double precision, integer) does not exist
HINT: No function matches the given name and argument types. You might need to add explicit type casts.正确做法是先转 numeric:
-- 正确:先显式转 numeric 再保留两位
SELECT
round(2.675::numeric, 2) AS from_numeric,
round(2.675::float8::numeric, 2) AS from_float,
round(2.675::float8::numeric, 2) = 2.68 AS check_value; from_numeric | from_float | check_value
--------------+------------+-------------
2.68 | 2.68 | tnumeric 存的是精确的十进制 2.675,折半后远离零得到 2.68。而浮点路径先把二进制近似值转成 numeric,虽然此处结果相同,但并非所有数字都这么幸运:
-- 浮点误差的经典演示:0.1 + 0.2 不等于 0.3
SELECT
0.1::float8 + 0.2::float8 AS float_sum,
0.1::numeric + 0.2::numeric AS numeric_sum,
(0.1::float8 + 0.2::float8) = 0.3::float8 AS float_equal,
(0.1::numeric + 0.2::numeric) = 0.3::numeric AS numeric_equal; float_sum | numeric_sum | float_equal | numeric_equal
----------------+-------------+-------------+---------------
0.30000000000000004 | 0.3 | f | t金额计算必须用 numeric,浮点数只用于科学计算。 这是本节最需要带走的结论。
双参数的 s 允许为负,表示在小数点左侧舍入:
SELECT
round(1234.56::numeric, -1) AS round_tens, -- 舍入到十位
round(1234.56::numeric, -2) AS round_hundreds, -- 舍入到百位
round(1250::numeric, -2) AS round_1250; round_tens | round_hundreds | round_1250
------------+----------------+------------
1230 | 1200 | 1300round(1250, -2) 得到 1300——折半远离零,与「银行家舍入」会给出的 1200 不同。
trunc:向零截断
trunc 不做任何判断,直接把指定精度之后的位丢掉。关键在于它向零靠拢,而不是向下:
SELECT
trunc(42.8::numeric) AS t_pos, -- 正数向下
trunc(-42.8::numeric) AS t_neg, -- 负数向上(不是 -43!)
floor(-42.8::numeric) AS f_neg; -- 对比:floor 才是向下 t_pos | t_neg | f_neg
-------+-------+-------
42 | -42 | -43trunc(-42.8) = -42 而 floor(-42.8) = -43。做「按天切片」「按份数切块」这类逻辑时,如果数据可能为负(时差、账户变动),务必先想清楚要的是哪一种。
保留小数位的用法:
SELECT
trunc(42.4382::numeric, 2) AS t_2,
trunc(42.4382::numeric, 3) AS t_3,
trunc(-42.4382::numeric, 2) AS t_neg_2; t_2 | t_3 | t_neg_2
-------+--------+---------
42.43 | 42.438 | -42.43numeric 的精度由 numeric(p, s) 定义保证,所以 trunc 到 2 位后再存入 NUMERIC(10, 2) 列不会报错;但若截断后整数部分超出 p - s 位,仍会报 numeric field overflow。
-- 错误!截断后整数部分有 9 位,超过 NUMERIC(10, 2) 允许的 8 位
INSERT INTO products (sku, name, price, cost, stock)
VALUES ('XX-999', '越界商品', trunc(999999999.99::numeric, 2), 0, 0);ERROR: numeric field overflow
DETAIL: A field with precision 10, scale 2 must round to an absolute value less than 10^8.容易混淆:trunc 与 date_trunc 是两回事
名字里都带 trunc,但这两个函数属于完全不同的领域,参数与返回值毫无交集:
| 维度 | trunc | date_trunc |
|---|---|---|
| 作用对象 | 数值(numeric / 浮点 / 整数) | 时间值(timestamp / timestamptz / interval) |
| 第二个参数 | 保留的小数位数(integer) | 时间精度字符串('day' / 'month' / 'hour' …) |
| 做什么 | 砍掉指定小数位之后的数字 | 把时间「向下对齐」到指定精度的起点 |
| 返回类型 | 与入参同类型,或 numeric | 与入参同类型的时间值 |
| 所属分类 | 数学函数 | 日期时间函数 |
一句话区分:trunc 截断的是小数,date_trunc 截断的是时间。
-- 数值截断:丢掉小数位
SELECT trunc(2026.0917::numeric, 2) AS num_trunc; -- 2026.09
-- 时间截断:对齐到某精度的起点
SELECT
date_trunc('day', TIMESTAMP '2026-09-17 14:35:52') AS by_day,
date_trunc('month', TIMESTAMP '2026-09-17 14:35:52') AS by_month,
date_trunc('hour', TIMESTAMP '2026-09-17 14:35:52') AS by_hour; num_trunc
-----------
2026.09
by_day | by_month | by_hour
---------------------+--------------------+---------------------
2026-09-17 00:00:00 | 2026-09-01 00:00:00 | 2026-09-17 14:00:00两个关键差异需要记牢:
date_trunc是「向下对齐」,不是「向零」。它永远向时间轴的过去方向回退到精度起点,因此不存在负数方向的问题——时间点2026-09-17 14:35:52截到天就是当天零点,而不是「砍掉时分秒」。date_trunc的精度名是字符串,写错直接报错,不像trunc的位数是个数字:
-- 错误!日期精度名拼错
SELECT date_trunc('days', now());ERROR: unit "days" not recognized正确写法用单数形式:'day'、'hour'、'month'、'quarter'、'year'。另外 trunc 只接受 numeric 作为第二参数,而 date_trunc 从不涉及小数位——把两者混用是初学者最常犯的抄错函数名事故,编译器不会给出任何提示,只会静默给出完全不同量级的结果。
scale / min_scale / trim_scale:标度三兄弟
round 和 trunc 调整的是「值」,这三个函数处理的是「表示形式」:
SELECT
scale(8.4100::numeric) AS s, -- 当前标度:4
min_scale(8.4100::numeric) AS ms, -- 最少需要几位:2
trim_scale(8.4100::numeric) AS ts, -- 去掉尾随零:8.41
scale(8.41::numeric) AS s2,
min_scale(8.000::numeric) AS ms_zero; -- 整数的最小标度为 0 s | ms | ts | s2 | ms_zero
---+----+------+----+---------
4 | 2 | 8.41 | 2 | 0trim_scale 在比较和聚合时很有用:
-- 去掉尾随零后再比较,避免 '1.50' 与 '1.5' 看起来不同
SELECT '1.500'::numeric = '1.5'::numeric AS equal_raw,
trim_scale('1.500'::numeric) AS trimmed; equal_raw | trimmed
-----------+---------
t | 1.5numeric 的相等比较本来就忽略标度差异(1.500 = 1.5 为真),所以 trim_scale 主要改善的是文本输出,让展示层不出现多余的零。
除法 / 本身就是截断的
整数除法不需要 trunc,它天然向零截断:
SELECT
5 / 2 AS int_div, -- 整数除法:截断
5.0 / 2 AS numeric_div, -- 小数参与:得到 numeric
-5 / 2 AS int_div_neg, -- 负数:向零
mod(5, 2) AS remainder, -- 配套的余数函数
5 % 2 AS mod_op; int_div | numeric_div | int_div_neg | remainder | mod_op
---------+-------------+-------------+-----------+--------
2 | 2.5000000000000000 | -2 | 1 | 1-5 / 2 得到 -2 而不是 -3,与 trunc(-2.5) = -2 一致。要真正的浮点除法,必须让至少一个操作数是 numeric 或 float——这也是分页计算里最常见的 bug 来源。
参数类型决定返回类型
同一个函数名按入参类型分派到不同实现,返回类型也随之改变:
SELECT
pg_typeof(abs(-17)) AS abs_int,
pg_typeof(abs(-17.4)) AS abs_num,
pg_typeof(abs(-17.4::float8)) AS abs_float,
pg_typeof(round(1)) AS round_int,
pg_typeof(round(1.5)) AS round_num; abs_int | abs_num | abs_float | round_int | round_num
---------+---------+-----------+-----------+-----------
integer | numeric | double precision | numeric | numeric注意 pg_typeof(abs(-17)) 是 integer——整数字面量 17 被判定为 integer 而非 bigint。这在超大数场景下会直接触发溢出,必须显式写成 17::bigint 或 17::numeric。
实战示例
场景一:折扣价精确到分
需求:按 88 折重新定价,结果必须精确保留两位小数且符合会计直觉(四舍五入)。
-- 步骤 1:先看原始价、折扣价,以及舍入与截断的差异
SELECT
sku,
name,
price,
round(price * 0.88, 2) AS discounted,
round(price / 3, 2) AS round_third,
trunc(price / 3, 2) AS trunc_third,
round(price / 3, 2) - trunc(price / 3, 2) AS round_minus_trunc
FROM products
ORDER BY id; sku | name | price | discounted | round_third | trunc_third | round_minus_trunc
--------+------------+--------+------------+-------------+-------------+-------------------
KB-001 | 机械键盘 | 299.00 | 263.12 | 99.67 | 99.66 | 0.01
MS-002 | 无线鼠标 | 59.90 | 52.71 | 19.97 | 19.96 | 0.01
HP-003 | 头戴耳机 | 899.50 | 791.56 | 299.83 | 299.83 | 0.00
CB-004 | Type-C 线 | 19.90 | 17.51 | 6.63 | 6.63 | 0.00
ST-005 | 笔记本支架 | 129.00 | 113.52 | 43.00 | 43.00 | 0.00KB-001 的 299.00 / 3 = 99.6666...,round 进位到 99.67,trunc 直接丢掉得到 99.66,差了整整一分钱。而 HP-003 的 899.50 / 3 = 299.8333...,第 3 位小数是 3,舍入不起作用,两者相同。同一批数据里,有的行差一分、有的行不差,正是这种「看起来偶尔才对」的差异最难排查——乘以百万订单量就是真实的对账缺口。
-- 步骤 2:落地到一张定价表,确保列定义与计算结果一致
CREATE TABLE product_pricing (
sku TEXT PRIMARY KEY,
original NUMERIC(10, 2) NOT NULL,
final_price NUMERIC(10, 2) NOT NULL,
discount_rate NUMERIC(4, 2) NOT NULL
);
INSERT INTO product_pricing (sku, original, final_price, discount_rate)
SELECT
sku,
price,
round(price * 0.88, 2),
0.88
FROM products;
SELECT sku, original, discount_rate, final_price FROM product_pricing ORDER BY sku; sku | original | discount_rate | final_price
--------+----------+---------------+-------------
CB-004 | 19.90 | 0.88 | 17.51
HP-003 | 899.50 | 0.88 | 791.56
KB-001 | 299.00 | 0.88 | 263.12
MS-002 | 59.90 | 0.88 | 52.71
ST-005 | 129.00 | 0.88 | 113.52场景二:盈利方向与异常检测
需求:用 sign 快速标出商品是否亏损,并用 abs 找出成本与售价偏离过大的异常项。
-- 步骤 1:方向标记 + 毛利率(用 round 控制展示精度)
SELECT
sku,
name,
price - cost AS gross_profit,
sign(price - cost) AS direction,
round((price - cost) / price * 100, 2) AS margin_pct
FROM products
ORDER BY id; sku | name | gross_profit | direction | margin_pct
--------+------------+--------------+-----------+------------
KB-001 | 机械键盘 | 118.50 | 1 | 39.63
MS-002 | 无线鼠标 | 34.90 | 1 | 58.26
HP-003 | 头戴耳机 | 289.25 | 1 | 32.16
CB-004 | Type-C 线 | 13.50 | 1 | 67.84
ST-005 | 笔记本支架 | 40.20 | 1 | 31.16-- 步骤 2:用 abs 找毛利绝对值异常的商品(可能录错价)
SELECT
sku,
name,
price,
cost,
abs(price - cost) AS profit_gap
FROM products
WHERE abs(price - cost) > 100
ORDER BY profit_gap DESC; sku | name | price | cost | profit_gap
--------+----------+--------+--------+------------
HP-003 | 头戴耳机 | 899.50 | 610.25 | 289.25
KB-001 | 机械键盘 | 299.00 | 180.50 | 118.50-- 步骤 3:统计没有库存的商品数量,先用 sign 归一化再聚合
SELECT
count(*) AS total,
count(*) FILTER (WHERE sign(stock) = 0) AS no_stock,
count(*) FILTER (WHERE sign(stock) = 1) AS in_stock,
round(avg(price), 2) AS avg_price
FROM products; total | no_stock | in_stock | avg_price
-------+----------+----------+-----------
5 | 1 | 4 | 281.46场景三:时长截断与对齐
需求:把秒数切成「整分钟」,并区分「已播完的整分钟」与「剩余零头」。
WITH sessions AS (
SELECT * FROM (VALUES
('s1', 125.0),
('s2', 59.9),
('s3', 3600.0),
('s4', -12.5)
) AS t(session_id, seconds)
)
SELECT
session_id,
seconds,
trunc(seconds / 60.0) AS full_minutes, -- 完整分钟数
seconds - trunc(seconds / 60.0) * 60 AS remainder_sec, -- 剩余秒数
round(seconds / 60.0, 2) AS minutes_rounded -- 展示用分钟
FROM sessions
ORDER BY session_id; session_id | seconds | full_minutes | remainder_sec | minutes_rounded
------------+---------+--------------+---------------+-----------------
s1 | 125.0 | 2 | 5.00 | 2.08
s2 | 59.9 | 0 | 59.90 | 1.00
s3 | 3600.0 | 60 | 0.00 | 60.00
s4 | -12.5 | 0 | -12.50 | -0.21s4 展示了负数行为:trunc(-0.2083) 得到 0(向零靠拢),所以 full_minutes 为 0 而不是 -1。若业务语义是「欠费时长」,这里应改用 floor。
count(*) FILTER (...)的写法:PostgreSQL 支持在聚合函数后加FILTER (WHERE ...),比CASE WHEN更易读,且能与其他聚合在同一趟扫描中完成。
注意事项
abs(-2147483648)会报integer out of range,因为int4的最小值没有正数对应。先转bigint或numeric:sql-- 错误!直接对 int4 最小值取绝对值 SELECT abs(-2147483648); -- 正确 SELECT abs(-2147483648::numeric);- 双参数
round/trunc只接受numeric,传float8会报function round(double precision, integer) does not exist。 numeric与float8的舍入规则不同。numeric半程远离零(round(2.5)=3),float8多数平台半程取偶(round(2.5::float8)=2),且依赖宿主机。trunc向零靠拢,floor向下,负数上差别显著:trunc(-42.8) = -42,floor(-42.8) = -43。- 不要用浮点数存金额,
0.1 + 0.2 <> 0.3是二进制浮点的固有属性,不是 PostgreSQL 的 bug。 - 整数除法静默截断,
5 / 2得到2。需要小数时至少一侧要写成5.0或5::numeric。 round(v, s)的s可以为负,表示在小数点左侧舍入,round(1234.56, -1)得到1230。- 字面量
17的类型是integer不是bigint,超大数字面量务必显式加::bigint或::numeric后缀,否则会意外溢出。sql-- 不推荐:依赖隐式类型 SELECT abs(-17) FROM t; -- 推荐:明确位宽 SELECT abs(-17::bigint) FROM t; sign与abs遇到NULL返回NULL,聚合时会跳过,不要指望它们把空值当零。NaN在numeric中等于自身且大于一切数字,abs/sign会原样传播NaN,用它做判断前要先过滤。round/trunc不能用于表达式索引的稳定匹配:它们本身是IMMUTABLE可以用于索引,但索引表达式必须与查询逐字符一致,且round(x, s)的s必须是常量。sql-- 可用,但查询必须写出一模一样的表达式 CREATE INDEX idx_products_price_round ON products ((round(price, 0)));trunc之后仍可能触发numeric field overflow,截断只影响小数位,不会缩小整数部分。
最佳实践
- 金额、税额、折扣一律用
numeric,从列定义到中间计算全程不引入浮点。浮点只留给几何距离、概率、物理量。 - 展示层统一用
round(v, 2)收口,不要在每个查询里各写各的精度。把这个规则固化进视图:sqlCREATE OR REPLACE VIEW v_product_display AS SELECT sku, name, round(price, 2) AS price, round(price * 0.88, 2) AS sale_price FROM products; - 需要「砍掉」而非「舍入」时显式用
trunc,不要依赖整数除法,代码意图更清楚:sql-- 不推荐:靠整数除法的隐式截断 SELECT amount_cents / 100 FROM orders; -- 推荐:意图明确 SELECT trunc(amount_cents / 100.0) FROM orders; - 负数场景先确认语义。涉及时长、余额、温差这类可能为负的指标,先问「向零」还是「向下」,再选
trunc或floor。 sign优于手写三段CASE,既简洁又让相同表达式在查询中保持可复用:sql-- 推荐 CASE sign(delta) WHEN 1 THEN 'up' WHEN 0 THEN 'flat' ELSE 'down' END- 浮点数比较永远用容差,不要写
=:sql-- 错误!浮点相等不可靠 SELECT * FROM measurements WHERE value = 0.3; -- 正确 SELECT * FROM measurements WHERE abs(value - 0.3) < 1e-9; - 写测试用例时覆盖这几个边界值:
0、-0.0、±最小值、0.5折半、负数折半、NaN、NULL。round与trunc在这些点上的差异最大。 round的精度参数写成常量并配注释,便于后续统一调整:sql-- 展示精度:分(2 位) round(amount, 2)abs用于差值比较时优先在应用层剔除无效数据,不要依赖abs(NULL)返回NULL来「安全跳过」,那会静默影响聚合结果。- 超大数运算前先做量级估算,
int4上限约 21 亿,bigint约 922 亿亿,超出就该上numeric。位宽选小了只会得到报错,不会得到错误结果——这算是 PostgreSQL 的一个安全设计。 scale/min_scale/trim_scale用于数据校验与清洗,例如检测「本该两位小数的金额却存了三位」:sqlSELECT sku, amount FROM invoices WHERE scale(amount) > 2;- 避免在
WHERE中对索引列调用round/trunc,这会阻止索引使用:sql-- 不推荐!无法使用 price 上的索引 SELECT * FROM products WHERE round(price, 0) = 300; -- 推荐:改用范围条件 SELECT * FROM products WHERE price >= 299.5 AND price < 300.5;
下一节