Skip to content

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 Zeronumeric 的舍入规则:恰好折半时向远离零的方向进位
半程取偶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 / 1sign(-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

选择流程可以概括为:

text
你要的是「保留几位」还是「变成整数」?
  ├─ 变成整数
  │    ├─ 就近舍入(会计、显示)→ round(x)
  │    └─ 直接砍掉小数(分页、切片)→ trunc(x)
  └─ 保留小数位
       ├─ 会舍入 → round(x, s)
       └─ 只截断 → trunc(x, s)

一句话记忆:round 是「四舍五入」,trunc 是「一刀切」,两者在负数上最容易混淆。

语法与用法 ​

sql
-- 单参数形式:入参是什么类型,返回值就是什么类型
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)

最小可运行示例:

sql
-- 四个函数的基本行为
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;       -- 截断两位
text
 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,舍入会进位,截断不会。

准备本节贯穿使用的示例表:

sql
-- 商品表: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);
sql
-- 一次看清四个函数在同一批数据上的表现
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;
text
  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 |           1

299.00 / 3 = 99.6666...,round 给出 99.67,trunc 给出 99.66,这就是「舍入」与「截断」最直观的差别。

详细说明 ​

abs:绝对值与 int4 溢出陷阱 ​

abs 的签名是「返回类型与入参一致」:

入参类型返回类型说明
smallintsmallint范围 -32768 ~ 32767
integerinteger范围 -2147483648 ~ 2147483647
bigintbigint范围约 ±9.22e18
numericnumeric任意精度,无溢出风险
real / double precision同入参浮点,-0.0 会得到 0

核心陷阱在于有符号整数的最小值没有对应的正数。int4 的取值范围是 -2147483648 到 2147483647,绝对值 2147483648 装不回去:

sql
-- 错误!int4 的最小值取绝对值会溢出
SELECT abs(-2147483648);
text
ERROR:  integer out of range

修复方式只有一个:先提升类型再取绝对值。

sql
-- 正确:先转成 bigint 或 numeric
SELECT
  abs(-2147483648::bigint)   AS as_bigint,
  abs(-2147483648::numeric)  AS as_numeric;
text
   as_bigint   |   as_numeric
---------------+---------------
    2147483648 |    2147483648

bigint 也有同样的问题,只是触发值更大:

sql
-- 错误!bigint 的最小值同样溢出
SELECT abs((-9223372036854775808)::bigint);
text
ERROR:  bigint out of range

而 numeric 没有这个问题,这也是金额、ID 差值这类场景应优先用 numeric 的原因之一。

abs 还可以写成操作符 @:

sql
SELECT @ -5.0 AS op_abs, abs(-5.0) AS fn_abs;
text
 op_abs | fn_abs
--------+--------
    5.0 |    5.0

NaN 会被原样传播:

sql
SELECT abs('NaN'::numeric) AS abs_nan, sign('NaN'::numeric) AS sign_nan;
text
 abs_nan | sign_nan
---------+----------
     NaN |      NaN

sign:只有三种返回值的符号函数 ​

sign 的结果只可能是 -1、0、1(外加 NaN 传播)。它回答的是「方向」问题,而不是「大小」问题。

sql
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;
text
 neg | zero | pos | tiny_neg | zero_float | null_input
-----+------+-----+----------+------------+------------
  -1 |    0 |   1 |       -1 |          0 |

两个要点:

  1. 0 是精确判断。numeric 的 0.0 与 0 完全相同,sign 返回 0;但浮点数的「近似零」仍然会被判为负数或正数。
  2. NULL 输入返回 NULL,规则与其他标量函数一致。

sign 最实用的场合是把差值映射成业务状态:

sql
-- 用 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;
text
  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 的规则:

sql
-- 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;
text
 r_0_5 | r_1_5 | r_2_5 | r_neg_2_5
-------+-------+-------+-----------
     1 |     2 |     3 |        -3

再看 double precision:

sql
-- 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;
text
 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 成立——浮点数是另一套规则,且结果依赖宿主机。

接下来是双参数形式,也是最多人踩的坑:

sql
-- 错误!双参数 round 只接受 numeric,不接受 float8
SELECT round(2.675::float8, 2);
text
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:

sql
-- 正确:先显式转 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;
text
 from_numeric | from_float | check_value
--------------+------------+-------------
         2.68 |       2.68 | t

numeric 存的是精确的十进制 2.675,折半后远离零得到 2.68。而浮点路径先把二进制近似值转成 numeric,虽然此处结果相同,但并非所有数字都这么幸运:

sql
-- 浮点误差的经典演示: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;
text
      float_sum | numeric_sum | float_equal | numeric_equal
----------------+-------------+-------------+---------------
 0.30000000000000004 |         0.3 | f           | t

金额计算必须用 numeric,浮点数只用于科学计算。 这是本节最需要带走的结论。

双参数的 s 允许为负,表示在小数点左侧舍入:

sql
SELECT
  round(1234.56::numeric, -1)  AS round_tens,     -- 舍入到十位
  round(1234.56::numeric, -2)  AS round_hundreds, -- 舍入到百位
  round(1250::numeric, -2)     AS round_1250;
text
 round_tens | round_hundreds | round_1250
------------+----------------+------------
       1230 |           1200 |       1300

round(1250, -2) 得到 1300——折半远离零,与「银行家舍入」会给出的 1200 不同。

trunc:向零截断 ​

trunc 不做任何判断,直接把指定精度之后的位丢掉。关键在于它向零靠拢,而不是向下:

sql
SELECT
  trunc(42.8::numeric)   AS t_pos,     -- 正数向下
  trunc(-42.8::numeric)  AS t_neg,     -- 负数向上(不是 -43!)
  floor(-42.8::numeric)  AS f_neg;     -- 对比:floor 才是向下
text
 t_pos | t_neg | f_neg
-------+-------+-------
    42 |   -42 |   -43

trunc(-42.8) = -42 而 floor(-42.8) = -43。做「按天切片」「按份数切块」这类逻辑时,如果数据可能为负(时差、账户变动),务必先想清楚要的是哪一种。

保留小数位的用法:

sql
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;
text
  t_2  |  t_3   | t_neg_2
-------+--------+---------
 42.43 | 42.438 |  -42.43

numeric 的精度由 numeric(p, s) 定义保证,所以 trunc 到 2 位后再存入 NUMERIC(10, 2) 列不会报错;但若截断后整数部分超出 p - s 位,仍会报 numeric field overflow。

sql
-- 错误!截断后整数部分有 9 位,超过 NUMERIC(10, 2) 允许的 8 位
INSERT INTO products (sku, name, price, cost, stock)
VALUES ('XX-999', '越界商品', trunc(999999999.99::numeric, 2), 0, 0);
text
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,但这两个函数属于完全不同的领域,参数与返回值毫无交集:

维度truncdate_trunc
作用对象数值(numeric / 浮点 / 整数)时间值(timestamp / timestamptz / interval)
第二个参数保留的小数位数(integer)时间精度字符串('day' / 'month' / 'hour' …)
做什么砍掉指定小数位之后的数字把时间「向下对齐」到指定精度的起点
返回类型与入参同类型,或 numeric与入参同类型的时间值
所属分类数学函数日期时间函数

一句话区分:trunc 截断的是小数,date_trunc 截断的是时间。

sql
-- 数值截断:丢掉小数位
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;
text
 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

两个关键差异需要记牢:

  1. date_trunc 是「向下对齐」,不是「向零」。它永远向时间轴的过去方向回退到精度起点,因此不存在负数方向的问题——时间点 2026-09-17 14:35:52 截到天就是当天零点,而不是「砍掉时分秒」。
  2. date_trunc 的精度名是字符串,写错直接报错,不像 trunc 的位数是个数字:
sql
-- 错误!日期精度名拼错
SELECT date_trunc('days', now());
text
ERROR:  unit "days" not recognized

正确写法用单数形式:'day'、'hour'、'month'、'quarter'、'year'。另外 trunc 只接受 numeric 作为第二参数,而 date_trunc 从不涉及小数位——把两者混用是初学者最常犯的抄错函数名事故,编译器不会给出任何提示,只会静默给出完全不同量级的结果。

scale / min_scale / trim_scale:标度三兄弟 ​

round 和 trunc 调整的是「值」,这三个函数处理的是「表示形式」:

sql
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
text
 s | ms |  ts  | s2 | ms_zero
---+----+------+----+---------
 4 |  2 | 8.41 |  2 |       0

trim_scale 在比较和聚合时很有用:

sql
-- 去掉尾随零后再比较,避免 '1.50' 与 '1.5' 看起来不同
SELECT '1.500'::numeric = '1.5'::numeric AS equal_raw,
       trim_scale('1.500'::numeric)      AS trimmed;
text
 equal_raw | trimmed
-----------+---------
 t         |     1.5

numeric 的相等比较本来就忽略标度差异(1.500 = 1.5 为真),所以 trim_scale 主要改善的是文本输出,让展示层不出现多余的零。

除法 / 本身就是截断的 ​

整数除法不需要 trunc,它天然向零截断:

sql
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;
text
 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 来源。

参数类型决定返回类型 ​

同一个函数名按入参类型分派到不同实现,返回类型也随之改变:

sql
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;
text
 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 折重新定价,结果必须精确保留两位小数且符合会计直觉(四舍五入)。

sql
-- 步骤 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;
text
  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.00

KB-001 的 299.00 / 3 = 99.6666...,round 进位到 99.67,trunc 直接丢掉得到 99.66,差了整整一分钱。而 HP-003 的 899.50 / 3 = 299.8333...,第 3 位小数是 3,舍入不起作用,两者相同。同一批数据里,有的行差一分、有的行不差,正是这种「看起来偶尔才对」的差异最难排查——乘以百万订单量就是真实的对账缺口。

sql
-- 步骤 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;
text
  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 找出成本与售价偏离过大的异常项。

sql
-- 步骤 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;
text
  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
sql
-- 步骤 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;
text
  sku   |   name   | price  |  cost  | profit_gap
--------+----------+--------+--------+------------
 HP-003 | 头戴耳机 | 899.50 | 610.25 |     289.25
 KB-001 | 机械键盘 | 299.00 | 180.50 |     118.50
sql
-- 步骤 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;
text
 total | no_stock | in_stock | avg_price
-------+----------+----------+-----------
     5 |        1 |        4 |    281.46

场景三:时长截断与对齐 ​

需求:把秒数切成「整分钟」,并区分「已播完的整分钟」与「剩余零头」。

sql
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;
text
 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.21

s4 展示了负数行为:trunc(-0.2083) 得到 0(向零靠拢),所以 full_minutes 为 0 而不是 -1。若业务语义是「欠费时长」,这里应改用 floor。

  • count(*) FILTER (...) 的写法:PostgreSQL 支持在聚合函数后加 FILTER (WHERE ...),比 CASE WHEN 更易读,且能与其他聚合在同一趟扫描中完成。

注意事项 ​

  1. abs(-2147483648) 会报 integer out of range,因为 int4 的最小值没有正数对应。先转 bigint 或 numeric:
    sql
    -- 错误!直接对 int4 最小值取绝对值
    SELECT abs(-2147483648);
    -- 正确
    SELECT abs(-2147483648::numeric);
  2. 双参数 round / trunc 只接受 numeric,传 float8 会报 function round(double precision, integer) does not exist。
  3. numeric 与 float8 的舍入规则不同。numeric 半程远离零(round(2.5)=3),float8 多数平台半程取偶(round(2.5::float8)=2),且依赖宿主机。
  4. trunc 向零靠拢,floor 向下,负数上差别显著:trunc(-42.8) = -42,floor(-42.8) = -43。
  5. 不要用浮点数存金额,0.1 + 0.2 <> 0.3 是二进制浮点的固有属性,不是 PostgreSQL 的 bug。
  6. 整数除法静默截断,5 / 2 得到 2。需要小数时至少一侧要写成 5.0 或 5::numeric。
  7. round(v, s) 的 s 可以为负,表示在小数点左侧舍入,round(1234.56, -1) 得到 1230。
  8. 字面量 17 的类型是 integer 不是 bigint,超大数字面量务必显式加 ::bigint 或 ::numeric 后缀,否则会意外溢出。
    sql
    -- 不推荐:依赖隐式类型
    SELECT abs(-17) FROM t;
    -- 推荐:明确位宽
    SELECT abs(-17::bigint) FROM t;
  9. sign 与 abs 遇到 NULL 返回 NULL,聚合时会跳过,不要指望它们把空值当零。
  10. NaN 在 numeric 中等于自身且大于一切数字,abs / sign 会原样传播 NaN,用它做判断前要先过滤。
  11. round / trunc 不能用于表达式索引的稳定匹配:它们本身是 IMMUTABLE 可以用于索引,但索引表达式必须与查询逐字符一致,且 round(x, s) 的 s 必须是常量。
    sql
    -- 可用,但查询必须写出一模一样的表达式
    CREATE INDEX idx_products_price_round ON products ((round(price, 0)));
  12. trunc 之后仍可能触发 numeric field overflow,截断只影响小数位,不会缩小整数部分。

最佳实践 ​

  1. 金额、税额、折扣一律用 numeric,从列定义到中间计算全程不引入浮点。浮点只留给几何距离、概率、物理量。
  2. 展示层统一用 round(v, 2) 收口,不要在每个查询里各写各的精度。把这个规则固化进视图:
    sql
    CREATE 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;
  3. 需要「砍掉」而非「舍入」时显式用 trunc,不要依赖整数除法,代码意图更清楚:
    sql
    -- 不推荐:靠整数除法的隐式截断
    SELECT amount_cents / 100 FROM orders;
    -- 推荐:意图明确
    SELECT trunc(amount_cents / 100.0) FROM orders;
  4. 负数场景先确认语义。涉及时长、余额、温差这类可能为负的指标,先问「向零」还是「向下」,再选 trunc 或 floor。
  5. sign 优于手写三段 CASE,既简洁又让相同表达式在查询中保持可复用:
    sql
    -- 推荐
    CASE sign(delta) WHEN 1 THEN 'up' WHEN 0 THEN 'flat' ELSE 'down' END
  6. 浮点数比较永远用容差,不要写 =:
    sql
    -- 错误!浮点相等不可靠
    SELECT * FROM measurements WHERE value = 0.3;
    -- 正确
    SELECT * FROM measurements WHERE abs(value - 0.3) < 1e-9;
  7. 写测试用例时覆盖这几个边界值:0、-0.0、±最小值、0.5 折半、负数折半、NaN、NULL。round 与 trunc 在这些点上的差异最大。
  8. round 的精度参数写成常量并配注释,便于后续统一调整:
    sql
    -- 展示精度:分(2 位)
    round(amount, 2)
  9. abs 用于差值比较时优先在应用层剔除无效数据,不要依赖 abs(NULL) 返回 NULL 来「安全跳过」,那会静默影响聚合结果。
  10. 超大数运算前先做量级估算,int4 上限约 21 亿,bigint 约 922 亿亿,超出就该上 numeric。位宽选小了只会得到报错,不会得到错误结果——这算是 PostgreSQL 的一个安全设计。
  11. scale / min_scale / trim_scale 用于数据校验与清洗,例如检测「本该两位小数的金额却存了三位」:
    sql
    SELECT sku, amount FROM invoices WHERE scale(amount) > 2;
  12. 避免在 WHERE 中对索引列调用 round / trunc,这会阻止索引使用:
    sql
    -- 不推荐!无法使用 price 上的索引
    SELECT * FROM products WHERE round(price, 0) = 300;
    -- 推荐:改用范围条件
    SELECT * FROM products WHERE price >= 299.5 AND price < 300.5;

下一节

继续学习:CEIL / FLOOR / MOD / POWER

参考链接 ​