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 / Remainder | y 除以 x 后的余数,符号跟随被除数 |
| 整数商 | Integer Quotient | div(y, x),向零截断的商,不含余数 |
| 幂运算 | Exponentiation | a 的 b 次方,记作 a^b |
| 底数 | Base | 幂运算中被乘方的数 a |
| 指数 | Exponent | 幂运算中的次数 b |
| 最大公约数 | Greatest Common Divisor | gcd,能同时整除两数的最大正整数 |
| 最小公倍数 | Least Common Multiple | lcm,两数公倍数中最小的正数 |
| 阶乘 | Factorial | n!,1 到 n 的连乘,增长极快 |
| 分桶 | Bucket / Histogram | width_bucket 把连续值映射到等宽区间编号 |
| 整数除法截断 | Integer Division Truncation | integer / integer 结果仍为整数,先截断再参与运算 |
四个函数的语义方向:
| 函数 | 方向 | 输入 2.1 | 输入 2.9 | 输入 -2.1 | 输入 -2.9 |
|---|---|---|---|---|---|
ceil / ceiling | 向 +∞ | 3 | 3 | -2 | -2 |
floor | 向 −∞ | 2 | 2 | -3 | -3 |
round | 最近,远离零 | 2 | 3 | -2 | -3 |
trunc | 向零 | 2 | 2 | -2 | -2 |
按需求选择函数:
你的需求是什么?
├─ 计算「需要几个容器」→ 一定是向上取整
│ └─ ceil(总量 / 单容容量)
│ └─ 注意:两个 integer 相除会先截断,必须转 numeric
├─ 计算「完整的组数」→ 一定是向下取整
│ └─ floor(总量 / 单组容量)
├─ 判断是否能整除 / 分组落桶
│ └─ mod(总量, 单组容量) = 0
├─ 分页页码 / 批次号
│ └─ div(行号 - 1, 页大小) + 1
└─ 增长模型 / 面积体积 / 距离
└─ power(底数, 指数) 或 底数 ^ 指数一句话记忆:「装得下几个容器」用 ceil,「凑得成几份整的」用 floor,「余下几个」用 mod。
语法与用法
-- 向上取整:向正无穷
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) -- 等宽分桶编号最小可运行示例:
-- 四个函数的最小演示
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 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):
-- 出货单明细:用于演示装箱、分页、复利、分桶
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);INSERT 0 6详细说明
ceil 与 ceiling:向正无穷取整
ceil 与 ceiling 是完全相同的函数(PostgreSQL 中两者互为别名),保留两个名字只是为了兼容不同 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 a | b | c | d | e
---+---+---+---+---
3 | 3 | -2| -2| 0整数除法会先截断,ceil 救不回来
这是本节最高频的错误。item_count 与 units_per_box 都是 integer,item_count / units_per_box 会先做整数除法并截断,结果已经是整数,此时再套 ceil 毫无作用:
-- 错误!7 / 4 先截断成 1,ceil(1) 仍是 1,但实际需要 2 个箱子
SELECT ceil(7 / 4); -- 1(错,直觉答案是 2)
-- 正确:先把分子转成 numeric,让除法保留小数
SELECT ceil(7::numeric / 4); -- 2判断规律:只要「被除数与除数都是整数类型」,就要警惕这个陷阱。
floor:向负无穷取整
floor 的方向永远是 -∞,与 ceil 恰好对称:
-- 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 a | b | c | d | e
---+---+---+---+---
2 | 2 | -3| -3| 0ceil 与 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:
-- 错误!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 的 %(符号跟随除数)不同:
-- 余数符号规则:符号永远跟被除数 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 a | b | c | d | e
---+----+---+----+------
1 | -1 | 1 | -1 | 1.75除数为零会直接报错,不会返回 NULL,所以做分桶前要先确认分母非零:
-- 错误!除数为零
SELECT mod(5, 0);ERROR: division by zero需要「分母可能为零也不报错」时,用 nullif 把它转成 NULL,NULL 参与运算会得到 NULL:
SELECT
mod(5, 2) AS ok, -- 1
mod(5, nullif(0, 0)) AS guarded; -- NULL,不报错最常见的用途是判断奇偶——对 id 取 mod 2,余数 0 是偶数、1 是奇数:
-- 按订单号奇偶分两条处理流水线
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; 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 的恒等关系:截断除法下,被除数 = 商 × 除数 + 余数,这个恒等式是校验分桶逻辑的利器:
-- 恒等式: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); 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 | tdiv:整数商(向零截断)
div(y, x) 返回 y / x 的整数商,方向是「向零截断」,返回类型为 numeric。它与 floor(y/x) 在负数上结论不同:
-- 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 一致) 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,就能把任意时刻归到固定长度的时间窗口起点。这也是本节唯一需要「向负无穷」语义的典型场景:
-- 把事件按 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; window_start | events
---------------------+--------
2026-09-17 14:00:00 | 7
2026-09-17 14:10:00 | 12
2026-09-17 14:20:00 | 5extract(epoch FROM ...) 得到带小数秒的秒数,除以 600 后 floor 到窗口编号,再乘回 600 并 to_timestamp 还原为时间。这里不能用 trunc 或 div 之外的「向零」语义替代负时间戳场景,但由于 epoch 在本例中恒为正,trunc 与 floor 结果一致;一旦处理 1970 年之前的时间或负数偏移,两者就会分道扬镳,因此时间窗口统一用 floor 是更稳妥的约定。
power 与 ^ 运算符:左结合陷阱
power(a, b) 与运算符 ^ 完全等价,但 ^ 有一个严重违反数学直觉的行为:默认左结合。
-- ^ 是左结合,不是数学上的右结合
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 left_assoc | right_assoc | power_right
------------+-------------+-------------
512 | 134217728 | 134217728-- 错误!以为得到 2^(3^3),实际是 (2^3)^3
SELECT 2 ^ 3 ^ 3; -- 512
-- 正确:需要右结合时必须显式加括号
SELECT 2 ^ (3 ^ 3); -- 134217728幂运算的类型分派:power 有 numeric 与 double precision 两个版本,返回类型跟随入参。金额、利率一律走 numeric:
-- 类型分派: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 int_input_type | dbl_input_type | exact_result | sqrt_like
----------------+-----------------+--------------+--------------------
numeric | double precision| 1024 | 1.41421356237309510 ^ 0 在 PostgreSQL 中返回 1;负数底数配非整数指数会报错,因为结果不是实数:
-- 边界行为
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) | 同入参类型 | 最大公约数,两数皆零返回 0 | gcd(1071, 462) → 21 |
lcm(a, b) | 同入参类型 | 最小公倍数,任一为零返回 0 | lcm(1071, 462) → 23562 |
factorial(n) | numeric | n 的阶乘 | factorial(5) → 120 |
width_bucket(v, lo, hi, n) | integer | 把 v 映射到 lo~hi 的 n 个等宽桶 | width_bucket(5.35, 0.024, 10.06, 5) → 3 |
-- 数论函数与分桶函数
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,超出上界) gcd_val | lcm_val | fact5 | fact20 | bucket_mid | bucket_below | bucket_above
---------+---------+-------+-------------------+------------+--------------+--------------
21 | 23562 | 120 | 2432902008176640000 | 3 | 0 | 6width_bucket 的桶编号规则:下界包含、上界不包含;小于 low 返回 0,大于等于 high 返回 count + 1。factorial 的结果是 numeric,因此 factorial(100) 这类超大值也不会溢出(整数类型会)。
实战示例
场景一:计算所需箱数(ceil 的正确写法)
装箱问题的标准解法是 ceil(件数 / 每箱容量),但必须先解决整数除法截断问题,并同时考虑重量约束:
-- 件数约束与重量约束,取两者较大值
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; 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-- 对比错误写法与正确写法:整数除法截断会让箱数偏小
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; 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:
-- 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; 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 个处理队列,天然负载均衡:
-- 按订单号哈希落 4 个分片队列
SELECT
order_no,
mod(id, 4) AS shard_no,
div(id - 1, 4) + 1 AS batch_no
FROM shipments
ORDER BY id; 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:
-- 复利终值:本金 × (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; 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.72power 在几何计算中同样高频:球的体积 4/3 × π × r³、立方体体积、平方反比衰减等。注意 power(x, 3) 与 x ^ 3 等价,但整数底数要转 numeric 才能拿到小数结果:
-- 球的体积(半径单位:米)
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); 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 桶,统计落桶分布:
-- 把 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; 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-- 与手写 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; 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 | 4width_bucket 的返回值是 integer,可以直接用于 GROUP BY、ORDER BY,也便于再拼上桶区间文字做报表。
注意事项
整数除法先截断,
ceil无法补救。ceil(7/4)得1而不是2,必须写ceil(7::numeric / 4)。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);余数符号跟随被除数,与 Python 相反。
mod(-9, 4) = -1,而 Python 的-9 % 4 = 3。跨语言分桶务必统一口径。^默认左结合。2 ^ 3 ^ 3 = 512而非134217728,需要右结合时显式加括号。负数底数配非整数指数会报错。
power(-8, 0.5)抛a negative number raised to a non-integer power yields a complex result。div与floor在负数上不同。div(-9,4) = -2(向零),floor(-9/4.0) = -3(向负无穷),按业务口径选。factorial的入参是bigint。factorial(5)合法,但factorial(5.5)会因无重载而报错,需先取整。sql-- 错误!function factorial(numeric) does not exist SELECT factorial(5.5); -- 正确 SELECT factorial(5);width_bucket的桶边界是「下闭上开」。low计入第 1 桶,high落到count + 1,低于low返回0,画图时要留意这两个哨兵值。width_bucket要求low < high。若low > high行为会镜像反转(桶 1 变成low下方那格),极易写错,不建议使用反向区间。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);不要在
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;power的浮点版会丢精度。power(1.1::double precision, 20)与power(1.1::numeric, 20)结果可能末位不同,金额相关的增长模型一律用numeric。
最佳实践
「需要几个容器」一律用
ceil,且分子强制转numeric。把ceil(x::numeric / y)作为团队约定的标准写法,避免整数除法陷阱。「凑成几份整的」用
floor,负数场景先确认口径。时间窗口、年龄段归类这类「向下」语义用floor;分页、批次这类「向零」语义用div。整数取余统一用
mod(y, x),不要写%混用。mod更易读,且在EXPLAIN输出与代码评审中一眼可辨。金额与增长模型一律使用
numeric版本的power,禁止浮点。把power(1.05::numeric, years)写进财务模型规范。幂运算超过两层时强制加括号。哪怕确定要左结合,也写成
(a ^ b) ^ c,避免下一位维护者误读。用恒等式
div(y,x)*x + mod(y,x) = y写单元测试。新增分桶逻辑时用这条恒等式批量校验正负样本。分桶统计优先用
width_bucket,避免手写CASE时漏掉边界与哨兵值。桶编号写成常量表达式并加注释,例如
width_bucket(weight, 0, 2.5, 4)旁标明「桶宽 0.625 kg」。ceil/floor/mod/power都是IMMUTABLE,可以放心用于表达式索引与生成列:sql-- 为装箱数建表达式索引,避免每次查询重算 CREATE INDEX idx_shipments_boxes ON shipments (ceil(item_count::numeric / units_per_box));分片键用
mod(hashtext(key), n)而非mod(id, n),避免自增 ID 造成的数据倾斜。factorial、lcm这类结果增长极快的函数,先估算量级再执行。factorial(10000)会消耗大量 CPU 与内存。把「取整方向」写进接口文档。对外提供「预计箱数」「预计批次数」时,明确声明用的是
ceil还是floor,避免与前端口径不一致。
下一节