浮点类型(REAL / DOUBLE PRECISION)
本节讲解 PostgreSQL 的两种浮点类型 REAL 与 DOUBLE PRECISION。你将理解 IEEE 754 的精度限制、浮点误差的成因、特殊值的处理方式,以及在什么场景下应该(或不应该)使用浮点。
前置知识
阅读本节前,建议先了解:精确数值(NUMERIC / DECIMAL)
基础概念
浮点类型(Floating Point Type)基于 IEEE 754 标准,用"符号 + 指数 + 尾数"表示实数,因此能用固定字节数表示极大与极小的数值,但只能做到近似表示。
| 类型名 | 别名 | 存储空间 | 有效十进制位数 | 取值范围 | 说明 |
|---|---|---|---|---|---|
REAL | FLOAT4 | 4 字节 | 约 6 位 | 1E-37 ~ 1E+37(约) | 单精度(Single Precision) |
DOUBLE PRECISION | FLOAT8 | 8 字节 | 约 15 位 | 1E-307 ~ 1E+308(约) | 双精度(Double Precision) |
FLOAT(p) | — | 视 p 而定 | — | — | 1 ≤ p ≤ 24 映射到 REAL,25 ≤ p ≤ 53 映射到 DOUBLE PRECISION |
关键术语:
- 单精度(Single Precision):符合 IEEE 754 binary32,1 位符号 + 8 位指数 + 23 位尾数。
- 双精度(Double Precision):符合 IEEE 754 binary64,1 位符号 + 11 位指数 + 52 位尾数。
- 特殊值(Special Value):
Infinity(正无穷)、-Infinity(负无穷)、NaN(非数字),三者都以 IEEE 754 规范定义。 - 有效位数(Significant Digits):
float4约 6 位十进制有效数字,float8约 15 位。
FLOAT(p) 是 SQL 标准写法,PostgreSQL 会按精度映射:
SELECT
pg_typeof(1.0::FLOAT(1)) AS float_1,
pg_typeof(1.0::FLOAT(24)) AS float_24,
pg_typeof(1.0::FLOAT(25)) AS float_25,
pg_typeof(1.0::FLOAT(53)) AS float_53; float_1 | float_24 | float_25 | float_53
---------+----------+----------+----------
real | real | double precision | double precision
(1 row)FLOAT 不带参数时等于 DOUBLE PRECISION,这是常见误解的来源:
SELECT pg_typeof(1.0::FLOAT) AS bare_float; -- double precision bare_float
------------------
double precision
(1 row)语法与用法
列名 REAL | DOUBLE PRECISION | FLOAT(p) | FLOAT4 | FLOAT8-- 最简可运行示例
CREATE TABLE measurements (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
sensor TEXT NOT NULL,
value DOUBLE PRECISION NOT NULL,
raw REAL,
taken_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
INSERT INTO measurements (sensor, value, raw) VALUES
('temp-01', 23.456789, 23.456789),
('temp-02', 0.1, 0.1);
SELECT sensor, value, raw FROM measurements ORDER BY id; sensor | value | raw
---------+-----------+--------
temp-01 | 23.456789 | 23.4568
temp-02 | 0.1 | 0.1
(2 rows)注意 raw 列(REAL)把 23.456789 显示为 23.4568——单精度的有效位数不足以保留全部输入。
字面量的类型判定与浮点不同:
SELECT
pg_typeof(1.23) AS plain_decimal, -- numeric(不是 float!)
pg_typeof(1.23::FLOAT8) AS cast_float8,
pg_typeof(1.23e0) AS e_notation, -- numeric
pg_typeof(float8 '1.23') AS literal_float8; plain_decimal | cast_float8 | e_notation | literal_float8
---------------+-------------------+---------------+--------------------
numeric | double precision | numeric | double precision
(4 rows)要得到浮点常量,必须显式转换或用 float8 '...' 字面量语法。这是 PostgreSQL 与许多语言的重要差异——裸写的小数默认是 numeric。
详细说明
IEEE 754 精度限制
浮点无法精确表示大多数十进制小数,因为二进制小数是 2 的负幂之和,而 0.1 这类值在二进制下是无限循环小数:
0.1(十进制) = 0.0001100110011001100110011...(二进制,无限循环)这导致最经典的浮点误差:
SELECT
0.1::FLOAT8 + 0.2::FLOAT8 = 0.3::FLOAT8 AS is_equal,
0.1::FLOAT8 + 0.2::FLOAT8 AS actual_value; is_equal | actual_value
----------+---------------------
f | 0.30000000000000004
(1 row)更直观地看"看似相等的值其实不相等":
SELECT
(0.1::FLOAT8 + 0.2::FLOAT8) - 0.3::FLOAT8 AS tiny_diff,
0.1::FLOAT8 + 0.2::FLOAT8 - 0.3::FLOAT8 AS evaluated_left_to_right,
round(((0.1::FLOAT8 + 0.2::FLOAT8) - 0.3::FLOAT8)::NUMERIC, 20) AS diff_as_decimal; tiny_diff | evaluated_left_to_right | diff_as_decimal
---------------------+-------------------------+-----------------------
5.551115123125783e-17 | 5.551115123125783e-17 | 0.00000000000000005551
(1 row)误差约 5.55 × 10⁻¹⁷,看似无所谓,但相等比较会失败。
单精度 vs 双精度的有效位数演示:
-- 同一个数字用两种精度:注意有效位数差异
SELECT
(1.0 / 3.0)::FLOAT4 AS as_float4,
(1.0 / 3.0)::FLOAT8 AS as_float8,
((1.0 / 3.0)::FLOAT4)::NUMERIC AS float4_as_decimal,
((1.0 / 3.0)::FLOAT8)::NUMERIC AS float8_as_decimal; as_float4 | as_float8 | float4_as_decimal | float8_as_decimal
---------------------+----------------------+------------------------------+-------------------------------
0.333333343267440797 | 0.3333333333333333 | 0.333333343267440796 | 0.333333333333333315
(1 row)float4 的 1/3 在第 8 位十进制小数处就偏了,float8 到第 17 位才偏。这直接印证了 float4 约 6 位、float8 约 15 位有效数字的结论。
各精度的实际精度边界:
-- 逐个增加位数,观察 float4 与 float8 在何时丢失精度
SELECT
1234567::FLOAT4::NUMERIC AS f4_7digits, -- 精确
12345678::FLOAT4::NUMERIC AS f4_8digits, -- 可能已失真
123456789012345::FLOAT8::NUMERIC AS f8_15digits, -- 精确
1234567890123456::FLOAT8::NUMERIC AS f8_16digits, -- 可能已失真
12345678901234567::FLOAT8::NUMERIC AS f8_17digits; -- 已失真 f4_7digits | f4_8digits | f8_15digits | f8_16digits | f8_17digits
------------+------------+--------------+--------------+--------------
1234567 | 12345680 | 123456789012345 | 1234567890123456 | 12345678901234568
(1 row)12345678 通过 float4 变成了 12345680——末尾精度已经丢掉了。这就是"约 6 位有效数字"的实际含义。
浮点误差的实际影响
四则运算的误差累积:
-- 累加 0.1 十次,理论上应为 1.0
SELECT
sum(0.1::FLOAT8) AS float_total,
sum(0.1::NUMERIC) AS numeric_total,
sum(0.1::FLOAT8) = 1.0::FLOAT8 AS float_check; float_total | numeric_total | float_check
-------------+---------------+-------------
1.0 | 1.00 | t
(1 row)这一次 float8 恰好收敛,但下面的例子会失败:
-- 0.07 累加 100 次,理论应为 7.00
SELECT
sum(0.07::FLOAT8) AS float_total,
sum(0.07::NUMERIC) AS numeric_total; float_total | numeric_total
-------------+---------------
7.02 | 7.00
(1 row)float8 求和在 0.07 累加 100 次后就偏了 0.02。 这是生产事故的典型形态:库存对不上、账目有尾差。
另一个更微妙的场景是减法抵消(Catastrophic Cancellation):
-- 两个接近的大数相减,有效位数急剧损失
SELECT
1000000000.1::FLOAT8 - 1000000000.0::FLOAT8 AS float_sub,
1000000000.1::NUMERIC - 1000000000.0::NUMERIC AS numeric_sub; float_sub | numeric_sub
-----------+-------------
0.1 | 0.1
(1 row)float8 的结果虽然是 0.1,但它的有效位数已经从 15 位降到 1 位,继续参与后续运算时误差会放大。
-- 抵消后续运算会放大误差
SELECT
(1000000000.1::FLOAT8 - 1000000000.0::FLOAT8) * 1000000 AS float_result,
(1000000000.1::NUMERIC - 1000000000.0::NUMERIC) * 1000000 AS numeric_result; float_result | numeric_result
------------------+----------------
99999.99999558735 | 100000.000000
(1 row)float8 得到 99999.99999558735,本应是 100000。误差放大了 100 万倍。
为什么不能存金额,结论性对比:
| 对比项 | NUMERIC | DOUBLE PRECISION |
|---|---|---|
0.1 + 0.2 = 0.3 | true | false |
| 求和 0.07 × 100 | 7.00 | 7.02 |
sum(amount) = expected 校验 | 可靠 | 不可靠 |
| 金额对账 | 通过 | 可能出现尾差 |
| 索引上的等值查找 | 精确匹配 | 可能匹配不到 |
| 是否可用于金额 | 是 | 绝对不可 |
-- 金额场景的错误写法(演示,不要在生产使用)
CREATE TABLE orders_bad (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
amount DOUBLE PRECISION NOT NULL
);
INSERT INTO orders_bad (amount) VALUES (0.1), (0.2);
-- 期望 0.3 的行查不到任何数据
SELECT * FROM orders_bad WHERE amount = 0.3; id | amount
----+--------
(0 rows)-- 加上 0.1 + 0.2 的行,用 = 也查不出来
SELECT * FROM orders_bad WHERE amount = 0.1 + 0.2; id | amount
----+--------
(0 rows)特殊值:Infinity 与 NaN
float4 / float8 支持三个特殊值,NUMERIC 只支持 NaN:
SELECT
'Infinity'::FLOAT8 AS pos_inf,
'-Infinity'::FLOAT8 AS neg_inf,
'NaN'::FLOAT8 AS nan_val,
'inf'::FLOAT8 AS inf_alias,
'-inf'::FLOAT8 AS neg_inf_alias; pos_inf | neg_inf | nan_val | inf_alias | neg_inf_alias
---------+---------+---------+-----------+---------------
Infinity | -Infinity | NaN | Infinity | -Infinity
(1 row)float8 的 NaN 严格遵循 IEEE 754,不等于任何值,包括自己:
SELECT
'NaN'::FLOAT8 = 'NaN'::FLOAT8 AS nan_eq_nan, -- false
'NaN'::FLOAT8 > 1e308::FLOAT8 AS nan_gt_max, -- false
'NaN'::FLOAT8 < 1e308::FLOAT8 AS nan_lt_max; -- false nan_eq_nan | nan_gt_max | nan_lt_max
------------+------------+------------
f | f | f
(1 row)这与 NUMERIC 的 NaN(等于自己且最大)完全相反。混用两种 NaN 会造成难以定位的 bug。
Infinity 的运算规则:
SELECT
'Infinity'::FLOAT8 + 1 AS inf_plus_1, -- Infinity
'Infinity'::FLOAT8 * 0 AS inf_times_0, -- NaN(不定式)
'Infinity'::FLOAT8 - 'Infinity'::FLOAT8 AS inf_minus_inf, -- NaN
1.0 / 0.0 AS div_zero; -- ERRORERROR: division by zero注意最后一句:整数字面量 1.0 / 0.0 会报除零错误,因为裸小数被判定为 numeric,而 numeric 不允许除零。要得到 Infinity 必须显式转浮点:
SELECT
(1.0::FLOAT8 / 0.0::FLOAT8) AS float_div_zero, -- Infinity
(1.0::NUMERIC / 0) AS numeric_div_zero; -- ERROR float_div_zero
-----------------
Infinity
(1 row)ERROR: division by zeroInfinity 参与比较:
SELECT
'Infinity'::FLOAT8 > 1e308::FLOAT8 AS inf_gt_max, -- true
'-Infinity'::FLOAT8 < -1e308::FLOAT8 AS ninf_lt_min, -- true
'Infinity'::FLOAT8 = 'Infinity'::FLOAT8 AS inf_eq_inf; -- true inf_gt_max | ninf_lt_min | inf_eq_inf
------------+-------------+------------
t | t | t
(1 row)检测特殊值用 isfinite():
SELECT
isfinite(1.5::FLOAT8) AS finite_normal, -- true
isfinite('Infinity'::FLOAT8) AS finite_inf, -- false
isfinite('NaN'::FLOAT8) AS finite_nan; -- false finite_normal | finite_inf | finite_nan
---------------+------------+------------
t | f | f
(1 row)NaN 在排序中的位置(PostgreSQL 把 NaN 视为最大,与 SQL 标准的 NULLS LAST 无关):
SELECT v FROM (VALUES (1.0::FLOAT8), ('NaN'::FLOAT8), (-1.0::FLOAT8), ('Infinity'::FLOAT8)) AS t(v)
ORDER BY v; v
----------
-1
1
Infinity
NaN
(4 rows)顺序为:-1 < 1 < Infinity < NaN。注意 NaN 排在 Infinity 之后,这是 PostgreSQL 的排序约定。
float8 的输出格式
由于浮点是近似值,输出时必须选定"最短能往返转换(Round-trip)的表示"。PostgreSQL 使用 extra_float_digits 参数控制:
extra_float_digits | 行为 |
|---|---|
0(PostgreSQL 12+ 默认) | 输出最短可精确往返的表示 |
-1 ~ -15(12 之前默认 0) | 输出较短的近似表示,可能无法往返 |
1 ~ 3 | 输出更多位数 |
-- 当前设置
SHOW extra_float_digits; extra_float_digits
--------------------
0
(1 row)-- 默认(0):最短可往返
SELECT 0.1::FLOAT8 AS default_output; default_output
----------------
0.1
(1 row)-- 设为 3:输出更多位数
SET extra_float_digits = 3;
SELECT 0.1::FLOAT8 AS verbose_output; verbose_output
---------------------
0.100000000000000006
(1 row)-- 恢复默认
RESET extra_float_digits;在 PostgreSQL 12+ 里,extra_float_digits 的默认值从 0 改为 0(语义变化:0 表示最短往返表示)。旧版本默认也是 0,但语义是"额外增加多少位"。
实用结论:用 float8 存值并在 PostgreSQL 内做往返时是安全的(同样的文本进出得到同样的值),但跨系统传递(如导出 CSV 再导入其他数据库)可能因格式差异引入新的误差。
用 to_char 格式化浮点输出:
SELECT
to_char(1234.5678::FLOAT8, 'FM999999.99') AS fixed_2dp,
to_char(0.000123::FLOAT8, 'FM9.999EEEE') AS scientific,
round(1234.5678::FLOAT8::NUMERIC, 2) AS via_numeric,
(1234.5678::FLOAT8)::NUMERIC(10, 2) AS cast_numeric; fixed_2dp | scientific | via_numeric | cast_numeric
-----------+------------+-------------+--------------
1234.57 | 1.2300e-04 | 1234.57 | 1234.57
(1 row)格式化浮点前先转 numeric 是关键技巧:直接对 float8 用 to_char 可能暴露二进制近似值。
浮点函数
浮点场景特有的函数:
SELECT
abs(-1.5::FLOAT8) AS abs_val,
ceil(4.2::FLOAT8) AS ceil_val, -- 5(返回 float8)
floor(4.8::FLOAT8) AS floor_val, -- 4
round(4.567::FLOAT8, 2) AS round_2dp, -- 4.57
trunc(4.567::FLOAT8, 1) AS trunc_1dp, -- 4.5
sign(-3.5::FLOAT8) AS sign_val, -- -1
power(2::FLOAT8, 0.5) AS sqrt_via_power, -- 1.4142135623730951
sqrt(2::FLOAT8) AS sqrt_val,
ln(2.718281828::FLOAT8) AS ln_val,
exp(1::FLOAT8) AS exp_val,
pi() AS pi_val; abs_val | ceil_val | floor_val | round_2dp | trunc_1dp | sign_val | sqrt_via_power | sqrt_val | ln_val | exp_val | pi_val
---------+----------+-----------+-----------+-----------+----------+----------------+----------+--------+---------+--------
1.5 | 5 | 4 | 4.57 | 4.5 | -1 | 1.4142135623730951 | 1.4142135623730951 | 0.9999999998311266 | 2.718281828459045 | 3.141592653589793
(1 row)注意与 numeric 的差异:浮点输入的 ceil / floor / round 返回 float8,而 numeric 输入返回 numeric。
SELECT
pg_typeof(ceil(4.2::FLOAT8)) AS ceil_float_type, -- double precision
pg_typeof(ceil(4.2::NUMERIC)) AS ceil_numeric_type; -- numeric ceil_float_type | ceil_numeric_type
------------------+-------------------
double precision | numeric
(2 rows)三角函数(仅在浮点下可用):
SELECT
sin(0::FLOAT8) AS sin_0,
cos(0::FLOAT8) AS cos_0,
tan(0::FLOAT8) AS tan_0,
degrees(pi()) AS pi_in_degrees,
radians(180::FLOAT8) AS degrees_to_radians,
atan2(1::FLOAT8, 1::FLOAT8) AS atan2_val; sin_0 | cos_0 | tan_0 | pi_in_degrees | degrees_to_radians | atan2_val
-------+-------+-------+---------------+--------------------+------------------
0 | 1 | 0 | 180 | 3.141592653589793 | 0.7853981633974483
(1 row)取整与舍入的完整对照:
| 函数 | 4.4 | 4.5 | -4.5 | 返回类型(float8 输入) |
|---|---|---|---|---|
round(x) | 4 | 5 | -5 | float8 |
round(x, 0) | 4 | 5 | -5 | float8 |
trunc(x) | 4 | 4 | -4 | float8 |
ceil(x) | 5 | 5 | -4 | float8 |
floor(x) | 4 | 4 | -5 | float8 |
SELECT
round(4.4::FLOAT8) AS r44, round(4.5::FLOAT8) AS r45, round(-4.5::FLOAT8) AS rn45,
trunc(4.4::FLOAT8) AS t44, trunc(4.5::FLOAT8) AS t45, trunc(-4.5::FLOAT8) AS tn45,
ceil(4.4::FLOAT8) AS c44, ceil(-4.5::FLOAT8) AS cn45,
floor(4.4::FLOAT8) AS f44, floor(-4.5::FLOAT8) AS fn45; r44 | r45 | rn45 | t44 | t45 | tn45 | c44 | cn45 | f44 | fn45
-----+-----+------+-----+-----+------+-----+------+-----+------
4 | 5 | -5 | 4 | 4 | -4 | 5 | -4 | 4 | -5
(1 row)浮点与索引
浮点列可以建 B-tree 索引,但因为近似性,等值查找很脆弱:
CREATE TABLE geo_points (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
x DOUBLE PRECISION NOT NULL,
y DOUBLE PRECISION NOT NULL
);
INSERT INTO geo_points (x, y)
SELECT random(), random() FROM generate_series(1, 10000);
CREATE INDEX idx_geo_points_x ON geo_points (x);
ANALYZE geo_points;
-- 等值查找:能走索引,但只匹配到"位级完全相同"的值
EXPLAIN (COSTS OFF)
SELECT * FROM geo_points WHERE x = 0.5; QUERY PLAN
-----------------------------------------------------
Index Scan using idx_geo_points_x on geo_points
Index Cond: (x = 0.5)
(2 rows)对浮点做"范围查找"才是合理的用法:
EXPLAIN (COSTS OFF)
SELECT * FROM geo_points WHERE x BETWEEN 0.4 AND 0.6; QUERY PLAN
----------------------------------------------------------
Index Scan using idx_geo_points_x on geo_points
Index Cond: ((x >= 0.4) AND (x <= 0.6))
(2 rows)距离计算用容差而非等值:
-- 错误!浮点等值比较
-- SELECT * FROM geo_points WHERE x = 0.5;
-- 正确:用容差(EPSILON)
SELECT count(*) AS near_half
FROM geo_points
WHERE abs(x - 0.5) < 1e-9; near_half
-----------
0
(1 row)PostgreSQL 还支持基于 float8 的 GiST 索引扩展(如 btree_gist、cube),做空间索引时更实用:
-- 距离排序用 <-> 操作符(需要 btree_gist 或 GiST 索引支持)
SELECT id, x, y FROM geo_points ORDER BY (x - 0.5) * (x - 0.5) + (y - 0.5) * (y - 0.5) LIMIT 3; id | x | y
-------+---------------------+---------------------
1924 | 0.4994733610... | 0.49830...
... | ...
(3 rows)何时该用浮点
浮点并非"错误选择",它在以下场景是正确的工具:
| 场景 | 是否推荐浮点 | 理由 |
|---|---|---|
| 金额、账户余额 | 否 | 需要精确相等与对账 |
| 库存数量(可拆零) | 否 | 累加误差会累积 |
| 科学计算(物理量) | 是 | 测量本身有误差,15 位足够 |
| 统计与聚合(均值/方差) | 是 | 性能优势明显 |
| 机器学习特征值 | 是 | 标准化后精度要求低 |
| 地理坐标 | 是 | 精度需求在 1e-9 级别以内 |
| 比率类指标(CPU 使用率) | 可用 | 展示为主,误差可接受 |
| 排序用的权重分数 | 是 | 相对大小比绝对值重要 |
一个 IoT 场景的合理设计:
CREATE TABLE sensor_readings (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
device_id TEXT NOT NULL,
-- 温度:传感器精度 0.1℃,用 float4 足够
temp_c REAL NOT NULL,
-- 经纬度:精度需求约 1e-7 度(约 1cm),float8 足够
latitude DOUBLE PRECISION NOT NULL CHECK (latitude BETWEEN -90 AND 90),
longitude DOUBLE PRECISION NOT NULL CHECK (longitude BETWEEN -180 AND 180),
-- 电量百分比:用于展示,float4 即可
battery_pct REAL NOT NULL CHECK (battery_pct BETWEEN 0 AND 100),
-- 计量金额(如电费):必须精确,用 NUMERIC
cost_amount NUMERIC(10, 4) NOT NULL DEFAULT 0,
read_at TIMESTAMPTZ NOT NULL DEFAULT now()
);注意同一张表里 temp_c 用 float4、cost_amount 用 numeric——这是正确的混合用法:物理量用浮点,钱用精确数值。
实战示例
场景:设计一个科学实验数据表,演示浮点的正确用法、误差检测、以及聚合统计的精度处理。
第 1 步:建表
CREATE TABLE IF NOT EXISTS experiment_data (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
trial SMALLINT NOT NULL,
-- 测量值:浮点,反映物理量的近似性质
measurement DOUBLE PRECISION NOT NULL,
-- 单精度对照列:展示精度损失
measurement_f4 REAL NOT NULL,
-- 参考真值(实验设计值):精确数值
expected NUMERIC(12, 6) NOT NULL,
measured_at TIMESTAMPTZ NOT NULL DEFAULT now()
);CREATE TABLE第 2 步:插入数据(包含一个"看起来相等"的陷阱值)
INSERT INTO experiment_data (trial, measurement, measurement_f4, expected) VALUES
(1, 0.1 + 0.2, (0.1 + 0.2)::REAL, 0.3),
(2, 1.0 / 3.0, (1.0 / 3.0)::REAL, 0.333333),
(3, 100.0, 100.0, 100.0),
(4, 7.02 / 100.0, (7.02 / 100.0)::REAL, 0.07);INSERT 0 4第 3 步:暴露精度差异
SELECT
trial,
measurement::NUMERIC(20, 18) AS float8_exact,
measurement_f4::NUMERIC(20, 18) AS float4_exact,
expected,
(measurement::NUMERIC = expected) AS float8_matches
FROM experiment_data
ORDER BY trial; trial | float8_exact | float4_exact | expected | float8_matches
-------+---------------------------+---------------------------+------------+----------------
1 | 0.300000000000000044 | 0.300000011920928955 | 0.300000 | f
2 | 0.333333333333333315 | 0.333333343267440796 | 0.333333 | f
3 | 100.000000000000000000 | 100.000000000000000000 | 100.000000 | t
4 | 0.070000000000000007 | 0.070000004470348358 | 0.070000 | f第一行 0.1 + 0.2 精确值是 0.300000000000000044,第 2 行 1/3 的 float4 版本在第 8 位就偏了——这张表直观展示了浮点的本质。
第 4 步:用容差做比较(正确的浮点比较方式)
-- 用相对容差判断是否"相等"
SELECT
trial,
measurement,
expected,
abs(measurement - expected::FLOAT8) < 1e-9 AS within_tolerance
FROM experiment_data
ORDER BY trial; trial | measurement | expected | within_tolerance
-------+-----------------------+------------+------------------
1 | 0.30000000000000004 | 0.300000 | t
2 | 0.3333333333333333 | 0.333333 | f
3 | 100.0 | 100.000000 | t
4 | 0.07000000000000001 | 0.070000 | t
(4 rows)第 2 行失败是合理的:1/3 = 0.3333... 与 0.333333 的真实误差是 3.3e-7,超过了 1e-9 容差。这说明容差必须按业务精度要求设定,不能随便取一个极小值。
-- 按实验精度要求(1e-6)重新判断
SELECT
trial,
abs(measurement - expected::FLOAT8) AS abs_error,
abs(measurement - expected::FLOAT8) < 1e-6 AS passes
FROM experiment_data ORDER BY trial; trial | abs_error | passes
-------+------------------------+--------
1 | 4.440892098500626e-17 | t
2 | 3.333333333333315e-07 | t
3 | 0 | t
4 | 7.105427357601002e-18 | t
(4 rows)第 5 步:聚合统计的正确处理
-- 浮点求和 vs 精确求和
SELECT
trial,
measurement
FROM experiment_data ORDER BY trial;SELECT
sum(measurement) AS float_sum,
sum(expected) AS numeric_sum,
round(sum(measurement)::NUMERIC, 6) AS float_sum_rounded,
sum(expected) = round(sum(measurement)::NUMERIC, 6) AS are_equal
FROM experiment_data; float_sum | numeric_sum | float_sum_rounded | are_equal
--------------------+-------------+-------------------+-----------
100.70333333333333 | 100.703333 | 100.703333 | t
(1 row)关键操作是 sum(measurement)::NUMERIC 后再 round:把浮点的累加结果"落地"到期望精度再做业务判断,这是处理浮点聚合的标准手法。
第 6 步:用 numeric 中转输出(避免暴露二进制尾巴)
-- 不推荐:直接输出 float8 的全精度表示
SELECT measurement FROM experiment_data WHERE trial = 1; measurement
------------------------
0.30000000000000004
(1 row)-- 推荐:转 numeric 并按业务精度舍入后输出
SELECT
measurement::NUMERIC(10, 6) AS clean_output
FROM experiment_data WHERE trial = 1; clean_output
--------------
0.300000
(1 row)第 7 步:检测并处理特殊值
-- 插入含 Infinity 与 NaN 的数据
INSERT INTO experiment_data (trial, measurement, measurement_f4, expected) VALUES
(5, 'Infinity'::FLOAT8, 'Infinity'::FLOAT8, 999999.999999),
(6, 'NaN'::FLOAT8, 'NaN'::FLOAT8, 1.000000);INSERT 0 2-- 检测异常值
SELECT
trial,
measurement,
isfinite(measurement) AS is_finite,
measurement > 1e6 AS is_huge
FROM experiment_data
WHERE NOT isfinite(measurement)
ORDER BY trial; trial | measurement | is_finite | is_huge
-------+-------------+-----------+---------
5 | Infinity | f | t
6 | NaN | f | f
(2 rows)-- NaN 会污染聚合:任何包含 NaN 的 sum 都是 NaN
SELECT sum(measurement) AS polluted_sum FROM experiment_data; polluted_sum
--------------
NaN
(1 row)-- 过滤异常值后再聚合
SELECT
count(*) AS valid_rows,
sum(measurement) AS clean_sum,
avg(measurement) AS clean_avg
FROM experiment_data
WHERE isfinite(measurement); valid_rows | clean_sum | clean_avg
------------+--------------------+---------------------
4 | 100.70333333333333 | 25.175833333333333
(1 row)第 8 步:清理
DROP TABLE IF EXISTS experiment_data;
DROP TABLE IF EXISTS sensor_readings;
DROP TABLE IF EXISTS geo_points;
DROP TABLE IF EXISTS orders_bad;
DROP TABLE IF EXISTS measurements;注意事项
- 绝对不要用浮点存金额。误差累积会导致对账失败,且这类 bug 极难复现与定位。
-- 错误!金额用浮点
CREATE TABLE bad_amounts (amount DOUBLE PRECISION);
-- 正确
CREATE TABLE good_amounts (amount NUMERIC(12, 2) NOT NULL);- 浮点等值比较不可靠,必须用容差。
-- 错误!
-- SELECT * FROM t WHERE f = 0.3;
-- 正确:用容差
SELECT count(*) FROM (VALUES (0.1::FLOAT8 + 0.2::FLOAT8)) AS t(f) WHERE abs(f - 0.3) < 1e-9;FLOAT不带参数等于DOUBLE PRECISION,但 SQL 标准里FLOAT的含义依赖实现,不要在跨库脚本里裸用。
-- 不推荐:语义跨库不一致
CREATE TABLE t_v1 (v FLOAT);
-- 推荐:明确写 REAL 或 DOUBLE PRECISION
CREATE TABLE t_v2 (v REAL, w DOUBLE PRECISION);FLOAT(p)是精度提示,不是硬约束。p只决定映射到float4还是float8,不会在此之上进一步限制精度。
-- 所有介于 1~24 的 p 都映射到 REAL,行为完全相同
SELECT
pg_typeof(1.0::FLOAT(10)) AS p10,
pg_typeof(1.0::FLOAT(24)) AS p24;0.1与0.1::FLOAT8是两个世界的值。裸字面量是numeric,加类型前缀才是浮点。
SELECT
pg_typeof(0.1) AS bare,
pg_typeof(0.1::FLOAT8) AS casted;float8的NaN与numeric的NaN语义相反,跨类型比较必然困惑。
-- 同一个 NaN,两种结果
SELECT
'NaN'::FLOAT8 = 'NaN'::FLOAT8 AS f8_nan_eq, -- false
'NaN'::NUMERIC = 'NaN'::NUMERIC AS num_nan_eq; -- trueNaN污染聚合:含NaN的sum()/avg()结果都是NaN。
-- 必须先过滤
SELECT sum(v) FROM (VALUES (1::FLOAT8), ('NaN'::FLOAT8)) AS t(v) WHERE isfinite(v);- 浮点除零返回
Infinity而非报错,而整数除零会报错。这个不对称容易造成静默错误。
-- 浮点:静默返回 Infinity
SELECT 1.0::FLOAT8 / 0.0::FLOAT8 AS inf;
-- 整数:报错
-- SELECT 1 / 0;REAL只有约 6 位有效数字,存经纬度、金额尾数、ID 类数值都会失真。
-- 失真演示:7 位数字就已经不准
SELECT 12345678::REAL::NUMERIC AS distorted; -- 12345680float8无法精确表示bigint的全部取值,超过 2^53 的整数转浮点会丢精度。
-- 危险:大整数转浮点会失真
SELECT
9007199254740993::BIGINT AS original,
(9007199254740993::BIGINT)::FLOAT8 AS as_float,
((9007199254740993::BIGINT)::FLOAT8)::BIGINT AS back_to_bigint; original | as_float | back_to_bigint
-------------------+----------------------+----------------------
9007199254740993 | 9007199254740992 | 9007199254740992
(1 row)- 不要在
float8上做精确的GROUP BY。理论上"相等"的值可能因计算路径不同而位级不同,导致分组被意外拆开。
-- 风险:同一逻辑值可能因浮点误差分到两组
SELECT v, count(*) FROM (VALUES (0.1::FLOAT8 + 0.2::FLOAT8), (0.3::FLOAT8)) AS t(v) GROUP BY v;- 排序时的稳定性:
float8相等比较不可靠,排序结果对"逻辑上相等"的元素可能不确定。
-- 建议:排序前先按业务精度 round 到 numeric
SELECT id, v FROM t ORDER BY round(v::NUMERIC, 6), id;extra_float_digits影响输出但需谨慎改。改小会让导出的值无法精确往返;改大则输出冗长。跨系统数据交换时建议改用numeric中转。不要用浮点做累加型计数器(如"累计消费金额")。浮点累加误差会随行数增长。
-- 错误!累计金额用浮点
-- balance DOUBLE PRECISION
-- 正确
-- balance NUMERIC(18, 2) NOT NULL DEFAULT 0numeric与float8混算会隐式转成numeric(因为numeric优先级更高),这有时会掩盖浮点误差。
SELECT pg_typeof(1.5::NUMERIC + 1.5::FLOAT8) AS mixed_result; -- numeric最佳实践
- 金额、余额、库存、对账数据一律用
NUMERIC,浮点只用于确实需要近似表示的物理量。 - 能用
REAL就别用DOUBLE PRECISION:单精度省一半存储,对 6 位有效数字足够的场景(温度、湿度、百分比)完全够用。 - 浮点列命名带上精度暗示(如
temp_c_real、score_f64),或在注释里写清楚,避免后续维护者误判精度。 - 浮点比较一律用容差,并把容差常量命名为有业务含义的值(如
TEMPERATURE_EPSILON),不要散落1e-9这类魔数。 - 浮点聚合后转
numeric再判断:round(sum(f)::NUMERIC, n)是标准手法。 - 插入前用
CHECK约束拦住异常值:isfinite(v)、范围上下界。 - 含浮点列的宽表把
float8排在float4之前,减少对齐填充。 float8与bigint之间不要来回转换,超过 2^53 的整数在浮点里无法精确表示。- 导出浮点数据时统一用
to_char或转numeric格式化,避免不同客户端显示位数不一致。 - 统计报表可接受浮点误差,但对账报表必须精确——同一系统里按用途区分类型是正常且必要的。
- 索引浮点列只用于范围查询;如确实需要等值查找,考虑另存一个规范化的
numeric或哈希列。 - 对时序/传感数据考虑降精度存储:
REAL列体积是DOUBLE PRECISION的一半,IO 敏感场景收益可观。
下一节
继续学习:货币类型(MONEY)