数学常量与三角函数
数学常量与三角函数是数值计算里"看起来冷门、用起来离不开"的一类:计算两个坐标点的距离、求方位角、做周期性建模、把弧度换成角度展示给用户——都绕不开它们。本节覆盖 PI()、DEGREES()、RADIANS() 与全部三角/反三角/双曲函数,包括 sind/cosd/tand 这一套角度版函数。重点讲清三个高频错误:pi 是函数不是常量、sin/cos/tan 的输入是弧度而非角度,以及官方推荐优先用角度版函数、而不是 radians() 转换。
前置知识
阅读本节前,建议先了解:RANDOM / SETSEED
基础概念
| 概念 | 术语 | 说明 |
|---|---|---|
| 圆周率 | π / PI | 圆周长与直径之比,pi() 返回 double precision 近似值 |
| 弧度 | radian | 角度的一种度量,180° = π 弧度;PostgreSQL 三角函数的输入单位 |
| 角度 | degree | 常见的角度度量,360° 为一整圈 |
| 度弧度转换 | DEGREES / RADIANS | degrees(x) 弧度转角度,radians(x) 角度转弧度 |
| 正弦/余弦/正切/余切 | SIN / COS / TAN / COT | 基本三角函数,输入为弧度 |
| 角度制三角函数 | SIND / COSD / TAND / COTD | 带 d 后缀的版本,直接以角度为输入(PostgreSQL 9.6+) |
| 反正弦/反余弦/反正切 | ASIN / ACOS / ATAN | 反三角函数,返回值是弧度 |
| 角度制反三角函数 | ASIND / ACOSD / ATAND / ATAN2D | 反三角函数的角度制版本,返回值为角度 |
| 双参数反正切 | ATAN2(y, x) | 由坐标 (x, y) 求方位角,返回值范围 (-π, π] |
| 自然常数 | e | 自然对数的底,PostgreSQL 无 e() 函数,需用 exp(1) 取得 |
| 旋转矩阵 | rotation matrix | 平面坐标绕原点旋转的线性变换,由 sin/cos 构成 |
| 双曲函数 | SINH / COSH / TANH | 基于指数函数定义的函数,工程与物理中常用 |
| 反双曲函数 | ASINH / ACOSH / ATANH | 双曲函数的反函数 |
| 定义域 | domain | 函数有意义的输入集合,asin(x) 要求 x ∈ [-1, 1] |
| 方位角 | azimuth | 从某方向起算的角,常用 atan2 计算 |
| 球面距离 | Haversine | 用 acos/sin/cos 计算地球表面两点间的距离 |
先把本节最关键的两个"反直觉点"写在前面:
1.
pi()是函数,必须带括号。写成pi会报错。2.
sin/cos/tan的输入是弧度,不是角度。要传角度,可以先用radians()转换,或直接改用角度版函数sind/cosd/tand。3. 三角函数只接受
double precision,没有numeric重载。4. 官方建议优先用角度版函数:带
d后缀的版本(sind/cosd/tand/atan2d等)直接以角度为输入,能避开radians()转换带来的舍入误差,是首选写法。例如sind(30)精确返回0.5。
语法与用法
-- 数学常量:pi 是函数,必须带括号
SELECT pi() AS pi_value; -- 3.141592653589793
-- 角度与弧度互转
SELECT degrees(pi()) AS half_circle, -- 180(π 弧度 = 180 度)
radians(180) AS pi_radian; -- 3.141592653589793
-- 三角函数:弧度版输入弧度,角度版(d 后缀)直接输入角度
SELECT sin(radians(30)) AS sin_30deg, -- 0.5(30 度的正弦)
cos(radians(60)) AS cos_60deg, -- 0.5
tan(radians(45)) AS tan_45deg; -- 0.9999999999999999
-- PostgreSQL 9.6+:角度版更精确
SELECT sind(30) AS sind_30, -- 0.5(无转换误差)
cosd(60) AS cosd_60, -- 0.5
tand(45) AS tand_45; -- 1(精确)
-- 反三角函数:弧度版返回弧度,角度版返回角度
SELECT degrees(asin(0.5)) AS asin_0_5_deg, -- 30(0.5 的反正弦,转成角度)
degrees(atan(1)) AS atan_1_deg, -- 45
asind(0.5) AS asind_0_5; -- 30(PostgreSQL 9.6+,直接返回角度)
-- 双参数反正切:计算方位角
SELECT atan2(1, 1) AS atan2_45rad, -- 0.7853981633974483
degrees(atan2(1, 1)) AS atan2_45deg, -- 45
atan2d(1, 1) AS atan2d_45; -- 45(PostgreSQL 9.6+)
-- 自然常数 e:没有 e(),用 exp(1)
SELECT exp(1) AS e_value; -- 2.718281828459045详细说明
PI():圆周率是函数
这是最容易出错的一点。在 PostgreSQL 里,π 没有对应的常量或字面量,只能通过 pi() 函数获取:
SELECT pi() AS ok; -- 3.141592653589793SELECT pi AS wrong;
-- 错误!ERROR: column "pi" does not exist
-- LINE 1: SELECT pi AS wrong;
-- ^
-- 注意:不带括号会被当成列名,报"列不存在"而不是"函数不存在",更容易误导排查pi() 返回 double precision,精度约 15~17 位有效数字:
SELECT pg_typeof(pi()) AS t, -- double precision
pi() AS full, -- 3.141592653589793
round(pi()::numeric, 10) AS rounded; -- 3.1415926536这也解释了为什么 tan(radians(45)) 返回的是 0.9999999999999999 而不是精确的 1:π/4 本身在二进制浮点里就无法精确表示。
pi() 最常见的用途是圆面积与周长计算:
-- 圆面积 πr² 与周长 2πr
SELECT r,
round((pi() * r * r)::numeric, 4) AS area, -- 面积
round((2 * pi() * r)::numeric, 4) AS perimeter -- 周长
FROM (VALUES (1.0), (2.5), (10.0)) AS t(r); r | area | perimeter
------+----------+-----------
1.0 | 3.1416 | 6.2832
2.5 | 19.6350 | 15.7080
10.0 | 314.1593 | 62.8319
(3 rows)在 SQL 里直接写 3.14159 是最常见的坏习惯——精度不够、也无法随需求调整。一律使用 pi()。
自然常数 e:没有 e() 函数
PostgreSQL 没有 e() 函数,也没有 e 常量。需要自然常数时用 exp(1):
SELECT exp(1) AS e_value, -- 2.718281828459045
exp(1.0::numeric) AS e_numeric, -- 2.7182818284590452...
round(exp(1)::numeric, 15) AS e_15dp; -- 2.718281828459045SELECT e AS wrong;
-- 错误!ERROR: column "e" does not existexp(1) 返回 double precision 的极限精度,若需要更高精度可传 numeric 入参。
DEGREES / RADIANS:单位换算
degrees(x) 把弧度转成角度,radians(x) 把角度转成弧度。两者互为反函数:
SELECT degrees(pi()) AS a, -- 180
degrees(pi() / 2) AS b, -- 90
degrees(pi() / 6) AS c, -- 30
radians(90) AS d, -- 1.5707963267948966
radians(360) AS e; -- 6.283185307179586互逆性是排查单位错误的好工具:
SELECT degrees(radians(37)) AS roundtrip; -- 37(转过去再转回来)记住换算方向:radians() 是"角度 → 弧度",degrees() 是"弧度 → 角度"。名字与目标单位一致,记起来最不容易混。
常用的角度-弧度对照表:
| 角度 | 弧度表达式 | 弧度值 |
|---|---|---|
| 0° | 0 | 0 |
| 30° | pi() / 6 | 0.5235987755982988 |
| 45° | pi() / 4 | 0.7853981633974483 |
| 60° | pi() / 3 | 1.0471975511965976 |
| 90° | pi() / 2 | 1.5707963267948966 |
| 180° | pi() | 3.141592653589793 |
| 270° | 3 * pi() / 2 | 4.712388980384690 |
| 360° | 2 * pi() | 6.283185307179586 |
三角函数:输入必须用弧度
PostgreSQL 的三角函数有两套:不带后缀的接收弧度,带 d 后缀的直接接收角度。
| 弧度版(输入弧度) | 角度版(输入角度) | 说明 |
|---|---|---|
sin(x) | sind(x) | 正弦 |
cos(x) | cosd(x) | 余弦 |
tan(x) | tand(x) | 正切 |
cot(x) | cotd(x) | 余切 |
asin(x) | asind(x) | 反正弦(角度版返回角度) |
acos(x) | acosd(x) | 反余弦(角度版返回角度) |
atan(x) | atand(x) | 反正切(角度版返回角度) |
atan2(y, x) | atan2d(y, x) | 双参数反正切(角度版返回角度) |
d 后缀的函数(sind/cosd/tand/cotd/asind/acosd/atand/atan2d)从 PostgreSQL 9.6 起提供:
-- PostgreSQL 9.6+
SELECT sind(30) AS s30, -- 恰好 0.5
cosd(60) AS c60, -- 恰好 0.5
tand(45) AS t45, -- 恰好 1
asind(0.5) AS a05, -- 30(返回角度而非弧度)
acosd(0.5) AS ac05, -- 60
atand(1) AS at1, -- 45
atan2d(1, 0) AS at2d; -- 90官方文档明确推荐优先使用 d 后缀版本:sind(30) 能避免 radians() 转换引入的舍入误差,返回精确的 0.5;而 sin(radians(30)) 虽然也是 0.5,但 tan(radians(45)) 只能得到 0.9999999999999999。
-- 对照:角度版更精确
SELECT tand(45) AS degree_based, -- 1(精确)
tan(radians(45)) AS radian_based; -- 0.9999999999999999(有舍入误差)如果运行环境早于 PostgreSQL 9.6(或需要与旧版保持一致),只能用 radians() 转换,或者手工写 sin(30 * pi() / 180)。
这是本节最高频的错误来源——忘记转换单位:
-- 错误!以为 sin 接收角度
SELECT sin(30) AS wrong_sin_30;
-- 结果:-0.9880316240928618
-- 它算的实际是 sin(30 弧度),不是 sin(30°)-- 正确写法一:用角度版函数(PostgreSQL 9.6+,推荐)
SELECT sind(30) AS right_sin_30; -- 0.5
-- 正确写法二:用 radians 转换
SELECT sin(radians(30)) AS right_sin_30_via_radians; -- 0.5用一组对照把差异钉死:
SELECT sin(30) AS sin_30_radian, -- -0.9880316240928618(30 弧度)
sin(radians(30)) AS sin_30_degree, -- 0.5(30 度)
cos(60) AS cos_60_radian, -- -0.9524129804151563
cos(radians(60)) AS cos_60_degree; -- 0.5三个基本三角函数的定义域是无限制的,任何实数都能算:
SELECT sin(0) AS s0, -- 0
sin(pi() / 2) AS s90, -- 1
sin(pi()) AS s180, -- 1.2246467991473532e-16(浮点近似,视为 0)
cos(0) AS c0, -- 1
cos(pi()) AS c180, -- -1
tan(0) AS t0, -- 0
tan(pi() / 4) AS t45; -- 0.9999999999999999注意 sin(pi()) 返回的是 1.2246467991473532e-16 而不是精确的 0——这是浮点近似的必然结果,比较时应使用容差:
SELECT abs(sin(pi())) < 1e-15 AS sin_pi_is_zero; -- true反三角函数:定义域与返回值
asin(x) 与 acos(x) 的定义域是 [-1, 1],超出范围直接报错:
SELECT asin(0.5) AS a, -- 0.5235987755982989(即 π/6 弧度 = 30°)
asin(1) AS b, -- 1.5707963267948966(π/2)
acos(1) AS c, -- 0
acos(0) AS d, -- 1.5707963267948966
acos(-1) AS e; -- 3.141592653589793SELECT asin(2);
-- 错误!ERROR: input is out of rangeSELECT acos(1.0000001);
-- 错误!ERROR: input is out of range这个报错在真实场景里非常容易触发。比如用点积公式算夹角时,浮点误差可能让本该等于 1 的结果变成 1.0000000000000002,直接让查询失败:
-- 假设两个单位向量的点积理论上是 1,实际可能是 1.0000000000000002
SELECT acos(1.0000000000000002);
-- 错误!ERROR: input is out of range
-- 正确:用 LEAST / GREATEST 把参数夹紧到 [-1, 1]
SELECT acos(LEAST(GREATEST(1.0000000000000002, -1), 1)) AS safe_acos; -- 0atan(x) 的定义域是整个实数轴,返回值范围是 (-π/2, π/2):
SELECT atan(0) AS a, -- 0
atan(1) AS b, -- 0.7853981633974483(π/4)
atan(1e308) AS c; -- 1.5707963267948966(趋近 π/2)ATAN2:方位角计算
atan2(y, x) 是双参数反正切——注意参数顺序是 y 在前、x 在后,这与很多人"先 x 后 y"的直觉相反。
它比 atan(y / x) 更强的地方在于:能区分象限,并且返回值范围是 (-π, π],也就是覆盖了完整的一圈。
SELECT degrees(atan2(1, 1)) AS q1, -- 45 (第一象限)
degrees(atan2(1, -1)) AS q2, -- 135 (第二象限)
degrees(atan2(-1, -1)) AS q3, -- -135 (第三象限)
degrees(atan2(-1, 1)) AS q4, -- -45 (第四象限)
degrees(atan2(0, 1)) AS e0, -- 0
degrees(atan2(1, 0)) AS n90; -- 90对比单参数 atan(y / x),后者只能返回 (-π/2, π/2),无法区分"第一象限"和"第三象限":
SELECT degrees(atan(1.0 / 1.0)) AS atan_q1, -- 45
degrees(atan(-1.0 / -1.0)) AS atan_q3, -- 45(错误!本该是 -135 或 225)
degrees(atan2(-1, -1)) AS atan2_q3; -- -135(正确,能区分象限)如果要得到 [0, 360) 的方位角(以正北或正东为起点),需要对负数结果加 360:
-- 把 atan2 结果归一化到 [0, 360)
SELECT (degrees(atan2(-1, -1)) + 360) % 360 AS azimuth; -- 225四象限完整对照表(用角度版 atan2d 更直观):
坐标 (x, y) | atan2d(y, x) | 归一化到 [0, 360) | 象限 |
|---|---|---|---|
(1, 1) | 45 | 45 | 第一象限 |
(-1, 1) | 135 | 135 | 第二象限 |
(-1, -1) | -135 | 225 | 第三象限 |
(1, -1) | -45 | 315 | 第四象限 |
(0, 1) | 90 | 90 | 正 Y 轴 |
(1, 0) | 0 | 0 | 正 X 轴 |
(0, -1) | -90 | 270 | 负 Y 轴 |
(-1, 0) | 180 | 180 | 负 X 轴 |
-- 用一条 SQL 生成整张对照表
SELECT x, y,
atan2d(y, x) AS raw_deg,
(atan2d(y, x) + 360)::numeric % 360 AS normalized_deg
FROM (VALUES (1.0, 1.0), (-1.0, 1.0), (-1.0, -1.0), (1.0, -1.0),
(0.0, 1.0), (1.0, 0.0), (0.0, -1.0), (-1.0, 0.0)) AS t(x, y); x | y | raw_deg | normalized_deg
------+------+---------+----------------
1 | 1 | 45 | 45
-1 | 1 | 135 | 135
-1 | -1 | -135 | 225
1 | -1 | -45 | 315
0 | 1 | 90 | 90
1 | 0 | 0 | 0
0 | -1 | -90 | 270
-1 | 0 | 180 | 180
(8 rows)注意 (atan2d(y, x) + 360) % 360 对已经为正的结果没有影响(45 + 360 = 405,405 % 360 = 45),所以这个归一化写法可以直接套用而无需判断符号。
坐标系旋转:用旋转矩阵做坐标变换
平面坐标绕原点旋转 θ 角(逆时针为正)由旋转矩阵给出:
x' = x·cos θ - y·sin θ
y' = x·sin θ + y·cos θ用 sind/cosd 直接写角度版的旋转,可读性最好:
-- 把一个图形的所有顶点绕原点逆时针旋转 30°
WITH verts AS (
SELECT * FROM (VALUES
('A', 1.0, 0.0),
('B', 1.0, 1.0),
('C', 0.0, 1.0)
) AS t(vertex, x, y)
)
SELECT vertex,
x AS x_orig,
y AS y_orig,
round((x * cosd(30) - y * sind(30))::numeric, 4) AS x_rotated,
round((x * sind(30) + y * cosd(30))::numeric, 4) AS y_rotated
FROM verts
ORDER BY vertex; vertex | x_orig | y_orig | x_rotated | y_rotated
--------+--------+--------+-----------+-----------
A | 1 | 0 | 0.8660 | 0.5000
B | 1 | 1 | 0.3660 | 1.3660
C | 0 | 1 | -0.5000 | 0.8660
(3 rows)旋转矩阵的正确性依赖 sin/cos 同角度,用 sind 配 radians() 混用是隐蔽错误:
-- 错误!角度版与弧度版混用,旋转角完全错乱
SELECT 1.0 * cosd(30) - 0.0 * sin(30) AS mixed_wrong; -- 0.8660(恰好对,纯属巧合)
SELECT 1.0 * cosd(30) - 1.0 * sin(30) AS mixed_wrong2; -- 0.3660(错,本该是 1.3660)几何场景(凸包、碰撞检测、字体排印)都会用到旋转。需要更复杂的空间运算请改用 PostGIS 的 ST_Rotate。
角度归一化:统一到 [0°, 360°) 或 (-180°, 180°]
业务里经常需要把任意角度"卷"回标准区间。归一化到 [0, 360):
-- 用取模把任意角度归一到 [0, 360)
SELECT a,
((a % 360) + 360) % 360 AS normalized
FROM (VALUES (370.0), (-30.0), (720.0), (45.0), (-400.0)) AS t(a); a | normalized
-------+------------
370 | 10
-30 | 330
720 | 0
45 | 45
-400 | 320
(5 rows)注意必须先 +360 再取模:-30 % 360 在 PostgreSQL 里返回 -30(余数符号跟随被除数),直接取模得不到 330。
归一化到 (-180, 180](很多坐标系与地图 API 的口径):
SELECT a,
((((a + 180) % 360) + 360) % 360) - 180 AS normalized
FROM (VALUES (370.0), (-30.0), (200.0), (180.0), (-180.0)) AS t(a); a | normalized
-------+------------
370 | 10
-30 | -30
200 | -160
180 | 180
-180 | -180
(5 rows)两个公式都遵循同一个原则:先平移到非负区间再取模,最后平移到目标区间。这也是 atan2 结果落地业务前必须做的一步。
双曲函数
双曲函数由指数函数定义:sinh(x) = (e^x - e^(-x)) / 2,cosh(x) = (e^x + e^(-x)) / 2,tanh(x) = sinh(x) / cosh(x)。
SELECT sinh(0) AS sh0, -- 0
sinh(1) AS sh1, -- 1.1752011936438014
cosh(0) AS ch0, -- 1
cosh(1) AS ch1, -- 1.5430806348152437
tanh(0) AS th0, -- 0
tanh(1) AS th1; -- 0.7615941559557649注意 tanh 的值域是 (-1, 1),常被用作"压缩到 [-1, 1]"的平滑函数:
SELECT tanh(10) AS near_1, -- 0.9999999958776927
tanh(100) AS closer_1, -- 1
tanh(-10) AS near_neg1; -- -0.9999999958776927反双曲函数的定义域各有约束:
| 函数 | 定义域 | 说明 |
|---|---|---|
asinh(x) | 全体实数 | 无限制 |
acosh(x) | x >= 1 | 小于 1 报错 |
atanh(x) | -1 < x < 1 | 端点与越界报错 |
SELECT asinh(0) AS a1, -- 0
asinh(1) AS a2, -- 0.881373587019543
acosh(1) AS a3, -- 0
acosh(2) AS a4, -- 1.3169578969248166
atanh(0) AS a5, -- 0
atanh(0.5) AS a6; -- 0.5493061443340549SELECT acosh(0.5);
-- 错误!ERROR: input is out of range
SELECT atanh(1);
-- 错误!ERROR: input is out of range只接受 double precision:没有 numeric 重载
所有三角函数只接受 double precision,没有 numeric 重载。传入 numeric 会被隐式转换,但返回值一定是 double precision:
SELECT pg_typeof(sin(0.5)) AS t1, -- double precision
pg_typeof(sin(0.5::numeric)) AS t2; -- double precision(隐式转换后仍是 double)如果把 numeric 表达式直接塞进去,可能会遇到类型解析问题;显式转换更稳妥:
-- 推荐:显式转换,语义清晰
SELECT sin((pi() / 6)::double precision) AS explicit_cast, -- 0.49999999999999994
sin(radians(30::numeric)::double precision) AS via_radians; -- 0.5需要把结果保存为 numeric 时,记得在最后一步转换:
SELECT round(sin(radians(30))::numeric, 10) AS sin_numeric; -- 0.5000000000NaN 与 Infinity 的传播
三角函数遇到 NaN 或 Infinity 时,结果会按浮点规则传播:
SELECT sin('NaN'::float8) AS sin_nan, -- NaN
sin('Infinity'::float8) AS sin_inf, -- NaN(无穷大没有正弦值)
atan2('Infinity'::float8, 1) AS atan2_inf; -- 1.5707963267948966(趋近 π/2)这些特殊值在聚合时同样会传播,需要留意:
SELECT round(avg(v), 4) AS avg_with_nan
FROM (VALUES (1.0::float8), (2.0), ('NaN'::float8)) AS t(v); avg_with_nan
--------------
NaN
(1 row)一旦有 NaN 混入,聚合结果整体变成 NaN。如果业务上 NaN 应当作缺失值,先过滤或转 NULL:
SELECT round(avg(v), 4) AS avg_clean
FROM (VALUES (1.0::float8), (2.0), ('NaN'::float8)) AS t(v)
WHERE v <> 'NaN'::float8; avg_clean
-----------
1.5000
(1 row)性能:三角函数调用比四则运算贵
三角函数是对 C 标准库的调用,成本明显高于加减乘除。在逐行调用的场景(大表全表扫描)里,这会成为瓶颈:
-- 不推荐:对每一行都调用三角函数
SELECT id, sin(lat) FROM locations;
-- 推荐:如果输入是常量,先算好常量
SELECT id, lat * 0.017453292519943295 FROM locations; -- 预计算的弧度系数如果三角函数只在 WHERE 里做常量判断,PostgreSQL 会在计划阶段折叠成常量,代价可以忽略:
EXPLAIN SELECT * FROM locations WHERE lat < degrees(pi() / 4); QUERY PLAN
--------------------------------------------------------------------------------
Seq Scan on locations (cost=0.00..18.10 rows=810 width=36)
Filter: (lat < '45'::double precision)可以看到 degrees(pi() / 4) 已被常量折叠为 45。真正需要警惕的是对列逐行求值的写法。
实战示例
假设我们要做一个"门店距离计算"功能:给用户当前位置,找出 5 公里内的门店,并给出方位角方向。
DROP TABLE IF EXISTS store_location;
CREATE TABLE store_location (
store_id integer PRIMARY KEY,
name text NOT NULL,
lat double precision NOT NULL, -- 纬度(度)
lon double precision NOT NULL -- 经度(度)
);
INSERT INTO store_location (store_id, name, lat, lon) VALUES
(1, '人民广场店', 31.2310, 121.4740),
(2, '陆家嘴店', 31.2397, 121.4998),
(3, '徐家汇店', 31.1947, 121.4370),
(4, '五角场店', 31.3017, 121.5148),
(5, '虹桥店', 31.1970, 121.3270);
-- 用户当前位置(上海南京东路附近)
-- 为便于复用,用 CTE 固定查询参数
WITH me AS (
SELECT 31.2350::double precision AS my_lat,
121.4800::double precision AS my_lon
),
calc AS (
SELECT s.store_id,
s.name,
s.lat,
s.lon,
-- 差值转弧度
radians(s.lat - m.my_lat) AS d_lat,
radians(s.lon - m.my_lon) AS d_lon,
radians(m.my_lat) AS lat1,
radians(s.lat) AS lat2
FROM store_location AS s
CROSS JOIN me AS m
)
SELECT store_id,
name,
-- Haversine 球面距离(地球平均半径 6371 km)
round((6371 * acos(
LEAST(GREATEST(
sin(lat1) * sin(lat2)
+ cos(lat1) * cos(lat2) * cos(d_lon), -1), 1)
))::numeric, 2) AS distance_km,
-- 方位角(相对正东,归一化到 [0, 360))
round(((degrees(atan2(d_lat, d_lon)) + 360) % 360)::numeric, 1) AS bearing_deg
FROM calc
ORDER BY distance_km; store_id | name | distance_km | bearing_deg
----------+-------------+-------------+-------------
1 | 人民广场店 | 0.70 | 317.7
2 | 陆家嘴店 | 1.93 | 72.3
3 | 徐家汇店 | 6.02 | 299.2
4 | 五角场店 | 8.05 | 39.4
5 | 虹桥店 | 14.94 | 272.5
(5 rows)注意这里用 LEAST(GREATEST(..., -1), 1) 把 acos 的参数夹紧到 [-1, 1]——这是 Haversine 公式里必须加的防护,否则两个点极其接近或相同时,浮点误差会让参数略微超过 1,导致整个查询报 input is out of range。
接着筛选 5 公里内并加上方向描述:
WITH me AS (
SELECT 31.2350::double precision AS my_lat,
121.4800::double precision AS my_lon
),
calc AS (
SELECT s.store_id,
s.name,
radians(s.lat - m.my_lat) AS d_lat,
radians(s.lon - m.my_lon) AS d_lon,
radians(m.my_lat) AS lat1,
radians(s.lat) AS lat2
FROM store_location AS s
CROSS JOIN me AS m
),
dist AS (
SELECT store_id, name, d_lat, d_lon,
6371 * acos(
LEAST(GREATEST(
sin(lat1) * sin(lat2) + cos(lat1) * cos(lat2) * cos(d_lon), -1), 1)
) AS distance_km
FROM calc
)
SELECT store_id,
name,
round(distance_km::numeric, 2) AS distance_km,
CASE
WHEN ((degrees(atan2(d_lat, d_lon)) + 360) % 360) < 45 THEN '东'
WHEN ((degrees(atan2(d_lat, d_lon)) + 360) % 360) < 135 THEN '北'
WHEN ((degrees(atan2(d_lat, d_lon)) + 360) % 360) < 225 THEN '西'
ELSE '南'
END AS direction
FROM dist
WHERE distance_km <= 5
ORDER BY distance_km; store_id | name | distance_km | direction
----------+-------------+-------------+-----------
1 | 人民广场店 | 0.70 | 西
2 | 陆家嘴店 | 1.93 | 北
(2 rows)再演示一个周期性建模的例子:用 sin 生成模拟的日客流曲线。
-- 模拟一天的客流:以中午 12 点为峰值,早晚低谷
SELECT to_char(
(DATE '2026-01-01' + (h || ' hours')::interval)::timestamp,
'HH24:00'
) AS hour_label,
round((100 + 60 * sin(radians((h - 6) * 15)) + 15 * random())::numeric, 1) AS traffic
FROM generate_series(0, 23) AS h
ORDER BY h; hour_label | traffic
------------+---------
00:00 | 57.3
01:00 | 57.9
02:00 | 44.6
03:00 | 30.1
04:00 | 16.0
05:00 | 11.9
06:00 | 54.3
07:00 | 61.6
08:00 | 70.0
09:00 | 85.4
10:00 | 102.1
11:00 | 115.7
12:00 | 177.2
13:00 | 129.7
14:00 | 140.2
15:00 | 146.7
16:00 | 119.9
17:00 | 136.0
18:00 | 122.2
19:00 | 115.3
20:00 | 103.0
21:00 | 86.7
22:00 | 79.3
23:00 | 72.5
(24 rows)注意事项
pi()必须带括号。 写成pi会被解析成列名,报"列不存在"。
SELECT pi();
-- 3.141592653589793
SELECT pi;
-- 错误!ERROR: column "pi" does not exist- 不带
d后缀的三角函数输入是弧度,不是角度。 想算 30 度,优先用sind(30);若用弧度版,必须写sin(radians(30))。
SELECT sin(30) AS wrong, -- -0.9880316240928618(30 弧度)
sin(radians(30)) AS right, -- 0.5(30 度,弧度版 + 转换)
sind(30) AS better; -- 0.5(PostgreSQL 9.6+,角度版,无转换误差)不要混用弧度版与角度版。
cosd(30) * sind(30)这类写法看似无事,一旦同一算式里既有cosd又有sin,旋转角就错乱了。asin/acos定义域是[-1, 1],越界直接报错。 浮点误差导致的"略微越界"必须用夹紧处理。
SELECT acos(1.0000000000000002);
-- 错误!ERROR: input is out of range
SELECT acos(LEAST(GREATEST(1.0000000000000002, -1), 1)) AS safe; -- 0atan2的参数顺序是(y, x),不是(x, y)。 写反了会把方位角整体旋转 90 度。
SELECT degrees(atan2(1, 0)) AS north_like, -- 90(y=1, x=0)
degrees(atan2(0, 1)) AS east_like; -- 0atan无法区分象限,atan2可以。 求方位角必须用atan2。
SELECT degrees(atan(-1.0 / -1.0)) AS ambiguous, -- 45(错,丢失象限信息)
degrees(atan2(-1, -1)) AS correct; -- -135- 返回
[0, 360)的方位角需要手工归一化。
-- 不推荐:直接使用,会得到负值
SELECT degrees(atan2(-1, -1)) AS negative_bearing; -- -135
-- 推荐:加 360 再取模
SELECT (degrees(atan2(-1, -1)) + 360) % 360 AS normalized; -- 225acosh(x)要求x >= 1,atanh(x)要求-1 < x < 1,端点与越界都报错。
SELECT acosh(0.5);
-- 错误!ERROR: input is out of range
SELECT atanh(1);
-- 错误!ERROR: input is out of range- 三角函数没有
numeric重载,返回值一定是double precision。 需要numeric时在最后一步转换。
SELECT pg_typeof(sin(0.5::numeric)) AS t; -- double precisionsin(pi())不会返回精确的0。 浮点近似导致结果是1.22e-16量级,比较时必须用容差。
SELECT sin(pi()) = 0 AS naive, -- false
abs(sin(pi())) < 1e-15 AS epsilon; -- trueNaN会污染聚合结果。 一行NaN就能让avg、sum全部变成NaN。
-- 不推荐:直接把 NaN 喂给聚合
SELECT avg(v) FROM (VALUES (1.0::float8), ('NaN'::float8)) AS t(v); -- NaN
-- 推荐:先过滤
SELECT avg(v) FROM (VALUES (1.0::float8), ('NaN'::float8)) AS t(v)
WHERE v <> 'NaN'::float8;- Haversine 公式里
acos的参数必须夹紧。 两个相同点之间计算距离时,浮点误差极易触发越界报错。
-- 不推荐:可能报 input is out of range
SELECT acos(sin(0.5) * sin(0.5) + cos(0.5) * cos(0.5) * cos(0)) AS risky;
-- 推荐:夹紧到 [-1, 1]
SELECT acos(LEAST(GREATEST(sin(0.5) * sin(0.5) + cos(0.5) * cos(0.5) * cos(0), -1), 1)) AS safe;- 逐行调用三角函数的开销明显高于四则运算。 大表上的逐行三角函数计算会成为瓶颈,能预计算就预计算。
最佳实践
- 角度场景优先用
d后缀函数(sind/cosd/tand/atan2d等,PostgreSQL 9.6+),官方文档明确指出这样能避免radians()转换在特殊角度上的舍入误差;必须兼容 PostgreSQL 9.5 及更早版本时,才退回radians()。 - 选中一套单位口径后就不要再混。同一个模块内要么全用角度版,要么全用弧度版,不要出现
cosd(30) * sin(30)这种混写。 pi()常量折叠对性能无害,写在WHERE里的常量表达式会被优化器折叠,不必担心。- 求方位角一律使用
atan2(y, x),并在得到结果后归一化到业务需要的区间(如[0, 360));atan(y / x)会丢失象限信息,不要在方位角场景使用。 - 反三角函数入参一律做夹紧,尤其是由浮点运算推导出来的值,用
LEAST(GREATEST(x, -1), 1)包裹是廉价且必要的防护。 - 浮点结果一律用容差比较,特别是
sin(pi())、tan(pi()/4)这类理论值为整数但实际有微差的场景。 - 需要高精度输出时,在最后一步转
numeric并round,例如round(sind(x)::numeric, 6),避免把浮点噪声带进业务字段。 - 三角函数的返回值始终是
double precision,不要期望numeric入参能换来numeric输出;涉及金额时只做中间计算,最终结果再转类型。 - 复杂空间运算交给 PostGIS,
ST_Distance、ST_Rotate、ST_Azimuth等函数封装了正确的算法与投影处理,比手写三角函数更可靠;earthdistance扩展提供了更轻量的地球表面距离计算;只有在无法安装扩展时才手写 Haversine。 - 周期建模时优先用角度版表达相位,例如
sin(radians((h - 6) * 15))或等价的sind((h - 6) * 15),比直接写弧度数值更直观、更容易复核。 - 大表上避免逐行调用三角函数,能在应用层或预计算列里算好的常量不要放进
SELECT列表逐行求值。