Skip to content

CEIL / FLOOR / MOD / POWER ​

取整与幂运算是一组「看着简单、用错很贵」的函数:ceil 与 floor 决定装箱、分页、分桶的边界口径,mod 决定了取余的符号方向,power 与 ^ 决定了增长模型的精度。它们的共同陷阱是整数除法先截断与类型分派静默改变结果——ceil(7/2) 得到 3 而不是 4,mod 根本不接受 double precision。本节把四个函数与配套的 div、gcd、width_bucket 一起讲透,并给出装箱、分页、复利、直方图四类实战。

前置知识

阅读本节前,建议先了解:ABS / SIGN / ROUND / TRUNC

基础概念 ​

概念术语说明
向上取整Ceiling取不小于入参的最小整数,向 +∞ 方向
向下取整Floor取不大于入参的最大整数,向 −∞ 方向
取余Modulo / Remaindery 除以 x 后的余数,符号跟随被除数
整数商Integer Quotientdiv(y, x),向零截断的商,不含余数
幂运算Exponentiationa 的 b 次方,记作 a^b
底数Base幂运算中被乘方的数 a
指数Exponent幂运算中的次数 b
最大公约数Greatest Common Divisorgcd,能同时整除两数的最大正整数
最小公倍数Least Common Multiplelcm,两数公倍数中最小的正数
阶乘Factorialn!,1 到 n 的连乘,增长极快
分桶Bucket / Histogramwidth_bucket 把连续值映射到等宽区间编号
整数除法截断Integer Division Truncationinteger / integer 结果仍为整数,先截断再参与运算

四个函数的语义方向:

函数方向输入 2.1输入 2.9输入 -2.1输入 -2.9
ceil / ceiling向 +∞33-2-2
floor向 −∞22-3-3
round最近,远离零23-2-3
trunc向零22-2-2

按需求选择函数:

text
你的需求是什么?
  ├─ 计算「需要几个容器」→ 一定是向上取整
  │    └─ ceil(总量 / 单容容量)
  │         └─ 注意:两个 integer 相除会先截断,必须转 numeric
  ├─ 计算「完整的组数」→ 一定是向下取整
  │    └─ floor(总量 / 单组容量)
  ├─ 判断是否能整除 / 分组落桶
  │    └─ mod(总量, 单组容量) = 0
  ├─ 分页页码 / 批次号
  │    └─ div(行号 - 1, 页大小) + 1
  └─ 增长模型 / 面积体积 / 距离
       └─ power(底数, 指数) 或 底数 ^ 指数

一句话记忆:「装得下几个容器」用 ceil,「凑得成几份整的」用 floor,「余下几个」用 mod。

语法与用法 ​

sql
-- 向上取整:向正无穷
ceil(x)                 -- numeric → numeric,double precision → double precision
ceiling(x)              -- 与 ceil 完全等价

-- 向下取整:向负无穷
floor(x)                -- numeric → numeric,double precision → double precision

-- 取余(不支持 double precision)
mod(y, x)               -- smallint / integer / bigint / numeric
y % x                   -- 与 mod 等价的运算符

-- 整数商:向零截断
div(y numeric, x numeric) → numeric

-- 幂运算
power(a, b)             -- numeric 版与 double precision 版
a ^ b                   -- 运算符形式,默认左结合

-- 数论与分桶配套工具
gcd(a, b)               -- 最大公约数(integer / bigint / numeric)
lcm(a, b)               -- 最小公倍数(integer / bigint / numeric)
factorial(bigint)       -- 阶乘,返回 numeric
width_bucket(operand, low, high, count)  -- 等宽分桶编号

最小可运行示例:

sql
-- 四个函数的最小演示
SELECT
  ceil(42.2)      AS ceil_pos,      -- 43
  ceil(-42.8)     AS ceil_neg,      -- -42(向 +∞)
  ceiling(95.3)   AS ceiling_alias, -- 96
  floor(42.8)     AS floor_pos,     -- 42
  floor(-42.8)    AS floor_neg,     -- -43(向 -∞)
  mod(9, 4)       AS mod_pos,       -- 1
  mod(-9, 4)      AS mod_neg,       -- -1(符号跟随被除数)
  div(9, 4)       AS div_pos,       -- 2(向零截断)
  div(-9, 4)      AS div_neg,       -- -2
  power(9, 3)     AS power_fn,      -- 729
  2 ^ 3           AS power_op;      -- 8
text
 ceil_pos | ceil_neg | ceiling_alias | floor_pos | floor_neg | mod_pos | mod_neg | div_pos | div_neg | power_fn | power_op
----------+----------+---------------+-----------+-----------+---------+---------+---------+---------+----------+----------
       43 |      -42 |            96 |        42 |       -43 |       1 |      -1 |       2 |      -2 |      729 |        8

准备本节贯穿使用的示例表(示例库 demo):

sql
-- 出货单明细:用于演示装箱、分页、复利、分桶
CREATE TABLE shipments (
  id            integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no      text          NOT NULL,
  sku_name      text          NOT NULL,
  item_count    integer       NOT NULL,       -- 商品件数
  units_per_box integer       NOT NULL,       -- 每箱容量(件)
  unit_weight   numeric(8, 3) NOT NULL,       -- 单件重量 kg
  box_limit_kg  numeric(8, 3) NOT NULL,       -- 单箱限重 kg
  freight       numeric(12,2) NOT NULL        -- 运费基数,用于复利演示
);

INSERT INTO shipments (order_no, sku_name, item_count, units_per_box, unit_weight, box_limit_kg, freight) VALUES
  ('SO20260101001', '陶瓷马克杯',   17, 6, 0.480, 12.000,  860.00),
  ('SO20260101002', '玻璃花瓶',      7, 4, 2.300, 10.000, 1240.50),
  ('SO20260101003', '茶具套装',     25, 8, 0.950, 15.000, 2180.00),
  ('SO20260101004', '不锈钢保温杯', 40, 12, 0.360, 9.000,  640.25),
  ('SO20260101005', '木质托盘',      9, 5, 1.840, 11.000,  410.00),
  ('SO20260101006', '亚克力展示架', 13, 6, 1.120, 8.000,  975.80);
text
INSERT 0 6

详细说明 ​

ceil 与 ceiling:向正无穷取整 ​

ceil 与 ceiling 是完全相同的函数(PostgreSQL 中两者互为别名),保留两个名字只是为了兼容不同 SQL 方言的写法。它的语义是「取值不小于入参的最小整数」,方向永远朝 +∞:

sql
-- ceil 的方向:正数向上,负数同样向上
SELECT
  ceil(2.1)     AS a,   --  3
  ceil(2.9)     AS b,   --  3
  ceil(-2.1)    AS c,   -- -2(注意:不是 -3,-2 比 -2.1 大)
  ceil(-2.9)    AS d,   -- -2
  ceil(0)       AS e;   --  0
text
 a | b | c | d | e
---+---+---+---+---
 3 | 3 | -2| -2| 0

整数除法会先截断,ceil 救不回来

这是本节最高频的错误。item_count 与 units_per_box 都是 integer,item_count / units_per_box 会先做整数除法并截断,结果已经是整数,此时再套 ceil 毫无作用:

sql
-- 错误!7 / 4 先截断成 1,ceil(1) 仍是 1,但实际需要 2 个箱子
SELECT ceil(7 / 4);              -- 1(错,直觉答案是 2)

-- 正确:先把分子转成 numeric,让除法保留小数
SELECT ceil(7::numeric / 4);     -- 2

判断规律:只要「被除数与除数都是整数类型」,就要警惕这个陷阱。

floor:向负无穷取整 ​

floor 的方向永远是 -∞,与 ceil 恰好对称:

sql
-- floor 的方向:正数向下,负数同样向下
SELECT
  floor(2.1)    AS a,   --  2
  floor(2.9)    AS b,   --  2
  floor(-2.1)   AS c,   -- -3(不是 -2,-3 比 -2.1 小)
  floor(-2.9)   AS d,   -- -3
  floor(0)      AS e;   --  0
text
 a | b | c | d | e
---+---+---+---+---
 2 | 2 | -3| -3| 0

ceil 与 floor 的负数行为对照 ​

正数上 ceil 与 floor 是「向上/向下」,直觉不容易出错;负数上则经常被误认为「向远离零/靠近零」,务必用下表固化记忆:

表达式结果记忆点
ceil(2.1)3向上跨越到整数
floor(2.9)2向下退回到整数
ceil(-2.1)-2负数向上 = 靠近零
floor(-2.1)-3负数向下 = 远离零
ceil(x) 与 floor(x) 相等当 x 本身是整数ceil(5)=floor(5)=5

ceil 与 floor 都是 IMMUTABLE 函数,因此可以安全用于表达式索引与分区表达式。

mod:取余(不支持 double precision) ​

mod(y, x) 返回 y 除以 x 的余数,支持 smallint / integer / bigint / numeric,不支持 double precision:

sql
-- 错误!mod 没有 double precision 重载
SELECT mod(9.5::double precision, 2.0);
-- ERROR: function mod(double precision, double precision) does not exist

-- 正确:转成 numeric 或使用浮点专用函数
SELECT
  mod(9.5::numeric, 2.0) AS as_numeric,   -- 1.5
  9.5::double precision % 2.0 AS via_op;  -- 运算符同样不支持,报错

余数的符号跟随被除数,这与 C 语言、Java 的 % 一致,但与 Python 的 %(符号跟随除数)不同:

sql
-- 余数符号规则:符号永远跟被除数 y
SELECT
  mod(9, 4)     AS a,   --  1
  mod(-9, 4)    AS b,   -- -1(被除数为负,余数为负)
  mod(9, -4)    AS c,   --  1(被除数为正,余数为正)
  mod(-9, -4)   AS d,   -- -1
  mod(9.75, 4)  AS e;   --  1.75
text
 a | b  | c | d  |  e
---+----+---+----+------
 1 | -1 | 1 | -1 | 1.75

除数为零会直接报错,不会返回 NULL,所以做分桶前要先确认分母非零:

sql
-- 错误!除数为零
SELECT mod(5, 0);
text
ERROR:  division by zero

需要「分母可能为零也不报错」时,用 nullif 把它转成 NULL,NULL 参与运算会得到 NULL:

sql
SELECT
  mod(5, 2)                        AS ok,          -- 1
  mod(5, nullif(0, 0))             AS guarded;     -- NULL,不报错

最常见的用途是判断奇偶——对 id 取 mod 2,余数 0 是偶数、1 是奇数:

sql
-- 按订单号奇偶分两条处理流水线
SELECT
  order_no,
  mod(order_no, 2)                       AS parity,
  CASE WHEN mod(order_no, 2) = 0 THEN '偶数批' ELSE '奇数批' END AS pipeline
FROM shipments
ORDER BY order_no;
text
    order_no    | parity | pipeline
----------------+--------+----------
 SO20260101001  |      1 | 奇数批
 SO20260101002  |      0 | 偶数批
 SO20260101003  |      1 | 奇数批
 SO20260101004  |      0 | 偶数批
 SO20260101005  |      1 | 奇数批
 SO20260101006  |      0 | 偶数批

跨语言迁移时余数符号是高频事故点

Python、Ruby 的 % 会让余数符号跟随除数:-9 % 4 在 Python 中是 3,在 PostgreSQL 中是 -1。把 Python 的业务逻辑直接翻译成 SQL 做分桶时,负值会被分到完全相反的桶里。跨语言场景建议改用 floor 与 div 显式表达口径。

与 div 的恒等关系:截断除法下,被除数 = 商 × 除数 + 余数,这个恒等式是校验分桶逻辑的利器:

sql
-- 恒等式:y = div(y, x) * x + mod(y, x)
SELECT
  y,
  div(y, x)                    AS quotient,
  mod(y, x)                    AS remainder,
  div(y, x) * x + mod(y, x)    AS recomposed,   -- 必然等于 y
  (div(y, x) * x + mod(y, x)) = y AS identity_ok
FROM (VALUES (17, 6), (-17, 6), (25, 8), (-25, 8)) AS t(y, x);
text
  y  | quotient | remainder | recomposed | identity_ok
-----+----------+-----------+------------+-------------
  17 |        2 |         5 |         17 | t
 -17 |       -2 |        -5 |        -17 | t
  25 |        3 |         1 |         25 | t
 -25 |       -3 |        -1 |        -25 | t

div:整数商(向零截断) ​

div(y, x) 返回 y / x 的整数商,方向是「向零截断」,返回类型为 numeric。它与 floor(y/x) 在负数上结论不同:

sql
-- div 与 floor 在负数上的分歧
SELECT
  div(9, 4)                  AS div_pos,      --  2
  div(-9, 4)                 AS div_neg,      -- -2(向零)
  floor(-9 / 4.0)            AS floor_neg,    -- -3(向负无穷)
  trunc(-9 / 4.0)            AS trunc_neg;    -- -2(向零,与 div 一致)
text
 div_pos | div_neg | floor_neg | trunc_neg
---------+---------+-----------+-----------
       2 |      -2 |        -3 |        -2

结论:div(y,x) 等价于 trunc(y/x),而 floor(y/x) 在负数上会再低一档。分页页码、批次编号这类「向零」语义的场景用 div;做时间窗归类这类「向负无穷」语义的场景用 floor。

div 的可用版本

div() 在 PostgreSQL 13 的官方数学函数表中已经存在,属于长期可用的内建函数,不需要额外扩展。旧代码里常见的 trunc(y::numeric / x) 写法可以直接替换为 div(y, x),可读性更好且避免了隐式转换。

时间分桶:floor 的经典用法。把时间戳转成 epoch 秒数后除以桶宽再 floor,就能把任意时刻归到固定长度的时间窗口起点。这也是本节唯一需要「向负无穷」语义的典型场景:

sql
-- 把事件按 10 分钟(600 秒)一个窗口分桶
SELECT
  to_timestamp(floor(extract(epoch FROM happened_at) / 600) * 600) AS window_start,
  count(*)                                                          AS events
FROM page_events
GROUP BY window_start
ORDER BY window_start;
text
    window_start     | events
---------------------+--------
 2026-09-17 14:00:00 |      7
 2026-09-17 14:10:00 |     12
 2026-09-17 14:20:00 |      5

extract(epoch FROM ...) 得到带小数秒的秒数,除以 600 后 floor 到窗口编号,再乘回 600 并 to_timestamp 还原为时间。这里不能用 trunc 或 div 之外的「向零」语义替代负时间戳场景,但由于 epoch 在本例中恒为正,trunc 与 floor 结果一致;一旦处理 1970 年之前的时间或负数偏移,两者就会分道扬镳,因此时间窗口统一用 floor 是更稳妥的约定。

power 与 ^ 运算符:左结合陷阱 ​

power(a, b) 与运算符 ^ 完全等价,但 ^ 有一个严重违反数学直觉的行为:默认左结合。

sql
-- ^ 是左结合,不是数学上的右结合
SELECT
  2 ^ 3 ^ 3            AS left_assoc,     -- 512   = (2^3)^3 = 8^3
  2 ^ (3 ^ 3)          AS right_assoc,    -- 134217728 = 2^27
  power(2, 3 ^ 3)      AS power_right;    -- 134217728
text
 left_assoc | right_assoc | power_right
------------+-------------+-------------
        512 |   134217728 |   134217728
sql
-- 错误!以为得到 2^(3^3),实际是 (2^3)^3
SELECT 2 ^ 3 ^ 3;        -- 512
-- 正确:需要右结合时必须显式加括号
SELECT 2 ^ (3 ^ 3);      -- 134217728

幂运算的类型分派:power 有 numeric 与 double precision 两个版本,返回类型跟随入参。金额、利率一律走 numeric:

sql
-- 类型分派:numeric 入参得 numeric,double 入参得 double
SELECT
  pg_typeof(power(2, 10))                    AS int_input_type,   -- numeric
  pg_typeof(power(2.0::double precision, 10)) AS dbl_input_type,  -- double precision
  power(2, 10)                               AS exact_result,     -- 1024
  power(2, 0.5)                              AS sqrt_like;        -- 1.4142135623730951
text
 int_input_type | dbl_input_type  | exact_result |     sqrt_like
----------------+-----------------+--------------+--------------------
 numeric        | double precision|         1024 | 1.4142135623730951

0 ^ 0 在 PostgreSQL 中返回 1;负数底数配非整数指数会报错,因为结果不是实数:

sql
-- 边界行为
SELECT
  power(0, 0)         AS zero_zero,      -- 1
  power(0, 5)         AS zero_pow,       -- 0
  power(2, -2)        AS neg_exp;        -- 0.25000000000000000000

-- 错误!负数底数 + 非整数指数没有实数解
SELECT power(-8, 0.5);
-- ERROR: a negative number raised to a non-integer power yields a complex result

数论与分桶工具箱 ​

除四个主角外,PostgreSQL 还提供一组配套函数,处理整数与分桶时非常省事:

函数返回说明示例
gcd(a, b)同入参类型最大公约数,两数皆零返回 0gcd(1071, 462) → 21
lcm(a, b)同入参类型最小公倍数,任一为零返回 0lcm(1071, 462) → 23562
factorial(n)numericn 的阶乘factorial(5) → 120
width_bucket(v, lo, hi, n)integer把 v 映射到 lo~hi 的 n 个等宽桶width_bucket(5.35, 0.024, 10.06, 5) → 3
sql
-- 数论函数与分桶函数
SELECT
  gcd(1071, 462)                        AS gcd_val,       -- 21
  lcm(1071, 462)                        AS lcm_val,       -- 23562
  factorial(5)                          AS fact5,         -- 120
  factorial(20)                         AS fact20,        -- 2432902008176640000
  width_bucket(5.35, 0.024, 10.06, 5)   AS bucket_mid,    -- 3
  width_bucket(0.001, 0.024, 10.06, 5)  AS bucket_below,  -- 0(低于下界)
  width_bucket(99.0, 0.024, 10.06, 5)   AS bucket_above;  -- 6(= count+1,超出上界)
text
 gcd_val | lcm_val | fact5 |      fact20       | bucket_mid | bucket_below | bucket_above
---------+---------+-------+-------------------+------------+--------------+--------------
      21 |   23562 |   120 | 2432902008176640000 |          3 |            0 |            6

width_bucket 的桶编号规则:下界包含、上界不包含;小于 low 返回 0,大于等于 high 返回 count + 1。factorial 的结果是 numeric,因此 factorial(100) 这类超大值也不会溢出(整数类型会)。

实战示例 ​

场景一:计算所需箱数(ceil 的正确写法) ​

装箱问题的标准解法是 ceil(件数 / 每箱容量),但必须先解决整数除法截断问题,并同时考虑重量约束:

sql
-- 件数约束与重量约束,取两者较大值
SELECT
  order_no,
  sku_name,
  item_count,
  units_per_box,
  ceil(item_count::numeric / units_per_box)                       AS boxes_by_count,   -- 按件数
  ceil(item_count * unit_weight / box_limit_kg)                   AS boxes_by_weight,  -- 按重量
  greatest(
    ceil(item_count::numeric / units_per_box),
    ceil(item_count * unit_weight / box_limit_kg)
  )                                                               AS boxes_final       -- 最终箱数
FROM shipments
ORDER BY id;
text
    order_no    |    sku_name    | item_count | units_per_box | boxes_by_count | boxes_by_weight | boxes_final
----------------+----------------+------------+---------------+----------------+-----------------+-------------
 SO20260101001  | 陶瓷马克杯     |         17 |             6 |              3 |               1 |           3
 SO20260101002  | 玻璃花瓶       |          7 |             4 |              2 |               2 |           2
 SO20260101003  | 茶具套装       |         25 |             8 |              4 |               2 |           4
 SO20260101004  | 不锈钢保温杯   |         40 |            12 |              4 |               2 |           4
 SO20260101005  | 木质托盘       |          9 |             5 |              2 |               2 |           2
 SO20260101006  | 亚克力展示架   |         13 |             6 |              3 |               2 |           3
sql
-- 对比错误写法与正确写法:整数除法截断会让箱数偏小
SELECT
  order_no,
  item_count,
  units_per_box,
  ceil(item_count / units_per_box)                  AS wrong_boxes,   -- 错误!先截断
  ceil(item_count::numeric / units_per_box)         AS right_boxes    -- 正确
FROM shipments
WHERE item_count / units_per_box * units_per_box <> item_count  -- 只挑有不能整除的
ORDER BY id;
text
    order_no    | item_count | units_per_box | wrong_boxes | right_boxes
----------------+------------+---------------+-------------+-------------
 SO20260101001  |         17 |             6 |           2 |           3
 SO20260101002  |          7 |             4 |           1 |           2
 SO20260101003  |         25 |             8 |           3 |           4
 SO20260101005  |          9 |             5 |           1 |           2
 SO20260101006  |         13 |             6 |           2 |           3

注意 WHERE 里那条整除判断:item_count / units_per_box * units_per_box <> item_count 正是「不能整除」的判定,本质就是 mod(item_count, units_per_box) <> 0,后一种写法可读性更好。

场景二:分页页码与批次分组(div 与 mod) ​

分页是 div 的经典用途:给定行号与页大小,页码 = div(行号 - 1, 页大小) + 1:

sql
-- 20 行数据按每页 6 条分页,输出每行所属页码与是否为页首
SELECT
  n                                             AS row_no,
  div(n - 1, 6) + 1                             AS page_no,       -- 页码
  div(n - 1, 6)                                 AS page_zero_based,
  mod(n - 1, 6) + 1                             AS position_in_page,
  (mod(n - 1, 6) = 0)                           AS is_first_in_page
FROM generate_series(1, 20) AS n
ORDER BY n;
text
 row_no | page_no | page_zero_based | position_in_page | is_first_in_page
--------+---------+-----------------+------------------+------------------
      1 |       1 |               0 |                1 | t
      2 |       1 |               0 |                2 | f
      3 |       1 |               0 |                3 | f
      4 |       1 |               0 |                4 | f
      5 |       1 |               0 |                5 | f
      6 |       1 |               0 |                6 | f
      7 |       2 |               1 |                1 | t
      8 |       2 |               1 |                2 | f
      9 |       2 |               1 |                3 | f
     10 |       2 |               1 |                4 | f
     11 |       2 |               1 |                5 | f
     12 |       2 |               1 |                6 | f
     13 |       3 |               2 |                1 | t
     14 |       3 |               2 |                2 | f
     15 |       3 |               2 |                3 | f
     16 |       3 |               2 |                4 | f
     17 |       3 |               2 |                5 | f
     18 |       3 |               2 |                6 | f
     19 |       4 |               3 |                1 | t
     20 |       4 |               3 |                2 | f

用 mod 做分片(sharding)与批次落桶同样常见:把订单按 mod(order_id, 4) 分成 4 个处理队列,天然负载均衡:

sql
-- 按订单号哈希落 4 个分片队列
SELECT
  order_no,
  mod(id, 4)              AS shard_no,
  div(id - 1, 4) + 1      AS batch_no
FROM shipments
ORDER BY id;
text
    order_no    | shard_no | batch_no
----------------+----------+----------
 SO20260101001  |        1 |        1
 SO20260101002  |        2 |        1
 SO20260101003  |        3 |        1
 SO20260101004  |        0 |        2
 SO20260101005  |        1 |        2
 SO20260101006  |        2 |        2

场景三:复利与增长模型(power) ​

运费按「每年递增 4.5%,连续 5 年」计算时,必须用 power 而非循环乘法,且金额必须走 numeric:

sql
-- 复利终值:本金 × (1 + 年利率) ^ 年数
SELECT
  order_no,
  freight                                   AS base_amount,
  1.045::numeric                            AS yearly_rate,
  power(1.045::numeric, 5)                  AS growth_factor,   -- 五年增长倍数
  round(freight * power(1.045::numeric, 5), 2) AS amount_in_5y, -- 终值
  round(freight * power(1.045::numeric, 5), 2) - freight AS delta
FROM shipments
ORDER BY freight DESC
LIMIT 3;
text
    order_no    | base_amount | yearly_rate |    growth_factor     | amount_in_5y |  delta
----------------+-------------+-------------+----------------------+--------------+---------
 SO20260101003  |     2180.00 |       1.045 | 1.246182110226853125 |      2716.68 |  536.68
 SO20260101002  |     1240.50 |       1.045 | 1.246182110226853125 |      1545.89 |  305.39
 SO20260101001  |      860.00 |       1.045 | 1.246182110226853125 |      1071.72 |  211.72

power 在几何计算中同样高频:球的体积 4/3 × π × r³、立方体体积、平方反比衰减等。注意 power(x, 3) 与 x ^ 3 等价,但整数底数要转 numeric 才能拿到小数结果:

sql
-- 球的体积(半径单位:米)
SELECT
  r,
  round(4.0 / 3.0 * pi() * power(r, 3), 4)         AS volume_numeric,  -- 正确
  round(4.0 / 3.0 * pi() * power(r::numeric, 3), 4) AS volume_cast
FROM (VALUES (1.0), (2.5), (0.75)) AS t(r);
text
  r   | volume_numeric | volume_cast
------+----------------+-------------
    1 |         4.1888 |      4.1888
  2.5 |        65.4498 |     65.4498
 0.75 |         1.7671 |      1.7671

场景四:等宽分桶统计(width_bucket) ​

width_bucket 把连续值映射到等宽桶编号,是手写直方图的标准工具。下面把单件重量分成 4 桶,统计落桶分布:

sql
-- 把 unit_weight 从 0 到 2.5 分成 4 个等宽桶
SELECT
  width_bucket(unit_weight, 0, 2.5, 4)  AS bucket_no,
  count(*)                              AS sku_count,
  min(unit_weight)                      AS min_weight,
  max(unit_weight)                      AS max_weight
FROM shipments
GROUP BY 1
ORDER BY 1;
text
 bucket_no | sku_count | min_weight | max_weight
-----------+-----------+------------+------------
         0 |         1 |      0.000 |      0.000
         1 |         2 |      0.360 |      0.480
         2 |         1 |      0.950 |      0.950
         3 |         2 |      1.120 |      1.840
sql
-- 与手写 CASE 版本对比:width_bucket 更短且边界统一
SELECT
  sku_name,
  unit_weight,
  width_bucket(unit_weight, 0, 2.5, 4)                       AS by_function,
  CASE
    WHEN unit_weight >= 2.5        THEN 5
    WHEN unit_weight < 0           THEN 0
    ELSE floor(unit_weight / 0.625)::integer + 1
  END                                                         AS by_case
FROM shipments
ORDER BY unit_weight;
text
    sku_name    | unit_weight | by_function | by_case
----------------+-------------+-------------+---------
 不锈钢保温杯   |       0.360 |           1 |       1
 陶瓷马克杯     |       0.480 |           1 |       1
 茶具套装       |       0.950 |           2 |       2
 亚克力展示架   |       1.120 |           3 |       3
 木质托盘       |       1.840 |           3 |       3
 玻璃花瓶       |       2.300 |           4 |       4

width_bucket 的返回值是 integer,可以直接用于 GROUP BY、ORDER BY,也便于再拼上桶区间文字做报表。

注意事项 ​

  1. 整数除法先截断,ceil 无法补救。ceil(7/4) 得 1 而不是 2,必须写 ceil(7::numeric / 4)。

  2. mod 不支持 double precision。浮点取余会报 function mod(double precision, double precision) does not exist,需转 numeric:

    sql
    -- 错误!mod 无 double precision 重载
    SELECT mod(9.5::double precision, 2);
    -- 正确
    SELECT mod(9.5::numeric, 2);
  3. 余数符号跟随被除数,与 Python 相反。mod(-9, 4) = -1,而 Python 的 -9 % 4 = 3。跨语言分桶务必统一口径。

  4. ^ 默认左结合。2 ^ 3 ^ 3 = 512 而非 134217728,需要右结合时显式加括号。

  5. 负数底数配非整数指数会报错。power(-8, 0.5) 抛 a negative number raised to a non-integer power yields a complex result。

  6. div 与 floor 在负数上不同。div(-9,4) = -2(向零),floor(-9/4.0) = -3(向负无穷),按业务口径选。

  7. factorial 的入参是 bigint。factorial(5) 合法,但 factorial(5.5) 会因无重载而报错,需先取整。

    sql
    -- 错误!function factorial(numeric) does not exist
    SELECT factorial(5.5);
    -- 正确
    SELECT factorial(5);
  8. width_bucket 的桶边界是「下闭上开」。low 计入第 1 桶,high 落到 count + 1,低于 low 返回 0,画图时要留意这两个哨兵值。

  9. width_bucket 要求 low < high。若 low > high 行为会镜像反转(桶 1 变成 low 下方那格),极易写错,不建议使用反向区间。

  10. lcm 任一入参为零返回 0,gcd 两入参皆零返回 0。gcd/lcm 只支持 integer / bigint / numeric,不支持 real / double precision:

    sql
    -- 错误!function gcd(double precision, double precision) does not exist
    SELECT gcd(12.5::double precision, 4);
  11. 不要在 WHERE 里对列套 ceil/floor,会让索引失效:

    sql
    -- 不推荐!无法使用 unit_weight 上的索引
    SELECT * FROM shipments WHERE ceil(unit_weight) = 2;
    -- 正确:改成范围条件
    SELECT * FROM shipments WHERE unit_weight > 1 AND unit_weight <= 2;
  12. power 的浮点版会丢精度。power(1.1::double precision, 20) 与 power(1.1::numeric, 20) 结果可能末位不同,金额相关的增长模型一律用 numeric。

最佳实践 ​

  1. 「需要几个容器」一律用 ceil,且分子强制转 numeric。把 ceil(x::numeric / y) 作为团队约定的标准写法,避免整数除法陷阱。

  2. 「凑成几份整的」用 floor,负数场景先确认口径。时间窗口、年龄段归类这类「向下」语义用 floor;分页、批次这类「向零」语义用 div。

  3. 整数取余统一用 mod(y, x),不要写 % 混用。mod 更易读,且在 EXPLAIN 输出与代码评审中一眼可辨。

  4. 金额与增长模型一律使用 numeric 版本的 power,禁止浮点。把 power(1.05::numeric, years) 写进财务模型规范。

  5. 幂运算超过两层时强制加括号。哪怕确定要左结合,也写成 (a ^ b) ^ c,避免下一位维护者误读。

  6. 用恒等式 div(y,x)*x + mod(y,x) = y 写单元测试。新增分桶逻辑时用这条恒等式批量校验正负样本。

  7. 分桶统计优先用 width_bucket,避免手写 CASE 时漏掉边界与哨兵值。

  8. 桶编号写成常量表达式并加注释,例如 width_bucket(weight, 0, 2.5, 4) 旁标明「桶宽 0.625 kg」。

  9. ceil/floor/mod/power 都是 IMMUTABLE,可以放心用于表达式索引与生成列:

    sql
    -- 为装箱数建表达式索引,避免每次查询重算
    CREATE INDEX idx_shipments_boxes
      ON shipments (ceil(item_count::numeric / units_per_box));
  10. 分片键用 mod(hashtext(key), n) 而非 mod(id, n),避免自增 ID 造成的数据倾斜。

  11. factorial、lcm 这类结果增长极快的函数,先估算量级再执行。factorial(10000) 会消耗大量 CPU 与内存。

  12. 把「取整方向」写进接口文档。对外提供「预计箱数」「预计批次数」时,明确声明用的是 ceil 还是 floor,避免与前端口径不一致。

下一节

继续学习:SQRT / CBRT / EXP / LN / LOG

参考链接 ​