Skip to content

数学常量与三角函数 ​

数学常量与三角函数是数值计算里"看起来冷门、用起来离不开"的一类:计算两个坐标点的距离、求方位角、做周期性建模、把弧度换成角度展示给用户——都绕不开它们。本节覆盖 PI()、DEGREES()、RADIANS() 与全部三角/反三角/双曲函数,包括 sind/cosd/tand 这一套角度版函数。重点讲清三个高频错误:pi 是函数不是常量、sin/cos/tan 的输入是弧度而非角度,以及官方推荐优先用角度版函数、而不是 radians() 转换。

前置知识

阅读本节前,建议先了解:RANDOM / SETSEED

基础概念 ​

概念术语说明
圆周率π / PI圆周长与直径之比,pi() 返回 double precision 近似值
弧度radian角度的一种度量,180° = π 弧度;PostgreSQL 三角函数的输入单位
角度degree常见的角度度量,360° 为一整圈
度弧度转换DEGREES / RADIANSdegrees(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。

语法与用法 ​

sql
-- 数学常量: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() 函数获取:

sql
SELECT pi() AS ok;   -- 3.141592653589793
sql
SELECT pi AS wrong;
-- 错误!ERROR:  column "pi" does not exist
-- LINE 1: SELECT pi AS wrong;
--                ^
-- 注意:不带括号会被当成列名,报"列不存在"而不是"函数不存在",更容易误导排查

pi() 返回 double precision,精度约 15~17 位有效数字:

sql
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() 最常见的用途是圆面积与周长计算:

sql
-- 圆面积 π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);
text
  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):

sql
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.718281828459045
sql
SELECT e AS wrong;
-- 错误!ERROR:  column "e" does not exist

exp(1) 返回 double precision 的极限精度,若需要更高精度可传 numeric 入参。

DEGREES / RADIANS:单位换算 ​

degrees(x) 把弧度转成角度,radians(x) 把角度转成弧度。两者互为反函数:

sql
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

互逆性是排查单位错误的好工具:

sql
SELECT degrees(radians(37)) AS roundtrip;  -- 37(转过去再转回来)

记住换算方向:radians() 是"角度 → 弧度",degrees() 是"弧度 → 角度"。名字与目标单位一致,记起来最不容易混。

常用的角度-弧度对照表:

角度弧度表达式弧度值
0°00
30°pi() / 60.5235987755982988
45°pi() / 40.7853981633974483
60°pi() / 31.0471975511965976
90°pi() / 21.5707963267948966
180°pi()3.141592653589793
270°3 * pi() / 24.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 起提供:

sql
-- 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。

sql
-- 对照:角度版更精确
SELECT tand(45)          AS degree_based,   -- 1(精确)
       tan(radians(45))  AS radian_based;   -- 0.9999999999999999(有舍入误差)

如果运行环境早于 PostgreSQL 9.6(或需要与旧版保持一致),只能用 radians() 转换,或者手工写 sin(30 * pi() / 180)。

这是本节最高频的错误来源——忘记转换单位:

sql
-- 错误!以为 sin 接收角度
SELECT sin(30) AS wrong_sin_30;
-- 结果:-0.9880316240928618
-- 它算的实际是 sin(30 弧度),不是 sin(30°)
sql
-- 正确写法一:用角度版函数(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

用一组对照把差异钉死:

sql
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

三个基本三角函数的定义域是无限制的,任何实数都能算:

sql
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——这是浮点近似的必然结果,比较时应使用容差:

sql
SELECT abs(sin(pi())) < 1e-15 AS sin_pi_is_zero;  -- true

反三角函数:定义域与返回值 ​

asin(x) 与 acos(x) 的定义域是 [-1, 1],超出范围直接报错:

sql
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.141592653589793
sql
SELECT asin(2);
-- 错误!ERROR:  input is out of range
sql
SELECT acos(1.0000001);
-- 错误!ERROR:  input is out of range

这个报错在真实场景里非常容易触发。比如用点积公式算夹角时,浮点误差可能让本该等于 1 的结果变成 1.0000000000000002,直接让查询失败:

sql
-- 假设两个单位向量的点积理论上是 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;  -- 0

atan(x) 的定义域是整个实数轴,返回值范围是 (-π/2, π/2):

sql
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) 更强的地方在于:能区分象限,并且返回值范围是 (-π, π],也就是覆盖了完整的一圈。

sql
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),无法区分"第一象限"和"第三象限":

sql
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:

sql
-- 把 atan2 结果归一化到 [0, 360)
SELECT (degrees(atan2(-1, -1)) + 360) % 360 AS azimuth;  -- 225

四象限完整对照表(用角度版 atan2d 更直观):

坐标 (x, y)atan2d(y, x)归一化到 [0, 360)象限
(1, 1)4545第一象限
(-1, 1)135135第二象限
(-1, -1)-135225第三象限
(1, -1)-45315第四象限
(0, 1)9090正 Y 轴
(1, 0)00正 X 轴
(0, -1)-90270负 Y 轴
(-1, 0)180180负 X 轴
sql
-- 用一条 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);
text
  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),所以这个归一化写法可以直接套用而无需判断符号。

坐标系旋转:用旋转矩阵做坐标变换 ​

平面坐标绕原点旋转 θ 角(逆时针为正)由旋转矩阵给出:

text
x' = x·cos θ - y·sin θ
y' = x·sin θ + y·cos θ

用 sind/cosd 直接写角度版的旋转,可读性最好:

sql
-- 把一个图形的所有顶点绕原点逆时针旋转 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;
text
 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() 混用是隐蔽错误:

sql
-- 错误!角度版与弧度版混用,旋转角完全错乱
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):

sql
-- 用取模把任意角度归一到 [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);
text
   a   | normalized
-------+------------
   370 |         10
   -30 |        330
   720 |          0
    45 |         45
  -400 |        320
(5 rows)

注意必须先 +360 再取模:-30 % 360 在 PostgreSQL 里返回 -30(余数符号跟随被除数),直接取模得不到 330。

归一化到 (-180, 180](很多坐标系与地图 API 的口径):

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

sql
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]"的平滑函数:

sql
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端点与越界报错
sql
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.5493061443340549
sql
SELECT 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:

sql
SELECT pg_typeof(sin(0.5))          AS t1,   -- double precision
       pg_typeof(sin(0.5::numeric)) AS t2;   -- double precision(隐式转换后仍是 double)

如果把 numeric 表达式直接塞进去,可能会遇到类型解析问题;显式转换更稳妥:

sql
-- 推荐:显式转换,语义清晰
SELECT sin((pi() / 6)::double precision) AS explicit_cast,  -- 0.49999999999999994
       sin(radians(30::numeric)::double precision) AS via_radians;  -- 0.5

需要把结果保存为 numeric 时,记得在最后一步转换:

sql
SELECT round(sin(radians(30))::numeric, 10) AS sin_numeric;  -- 0.5000000000

NaN 与 Infinity 的传播 ​

三角函数遇到 NaN 或 Infinity 时,结果会按浮点规则传播:

sql
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)

这些特殊值在聚合时同样会传播,需要留意:

sql
SELECT round(avg(v), 4) AS avg_with_nan
FROM (VALUES (1.0::float8), (2.0), ('NaN'::float8)) AS t(v);
text
 avg_with_nan
--------------
          NaN
(1 row)

一旦有 NaN 混入,聚合结果整体变成 NaN。如果业务上 NaN 应当作缺失值,先过滤或转 NULL:

sql
SELECT round(avg(v), 4) AS avg_clean
FROM (VALUES (1.0::float8), (2.0), ('NaN'::float8)) AS t(v)
WHERE v <> 'NaN'::float8;
text
 avg_clean
-----------
    1.5000
(1 row)

性能:三角函数调用比四则运算贵 ​

三角函数是对 C 标准库的调用,成本明显高于加减乘除。在逐行调用的场景(大表全表扫描)里,这会成为瓶颈:

sql
-- 不推荐:对每一行都调用三角函数
SELECT id, sin(lat) FROM locations;

-- 推荐:如果输入是常量,先算好常量
SELECT id, lat * 0.017453292519943295 FROM locations;  -- 预计算的弧度系数

如果三角函数只在 WHERE 里做常量判断,PostgreSQL 会在计划阶段折叠成常量,代价可以忽略:

sql
EXPLAIN SELECT * FROM locations WHERE lat < degrees(pi() / 4);
text
                                   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 公里内的门店,并给出方位角方向。

sql
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;
text
 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 公里内并加上方向描述:

sql
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;
text
 store_id |    name     | distance_km | direction
----------+-------------+-------------+-----------
        1 | 人民广场店  |        0.70 | 西
        2 | 陆家嘴店    |        1.93 | 北
(2 rows)

再演示一个周期性建模的例子:用 sin 生成模拟的日客流曲线。

sql
-- 模拟一天的客流:以中午 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;
text
 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 会被解析成列名,报"列不存在"。
sql
SELECT pi();
-- 3.141592653589793

SELECT pi;
-- 错误!ERROR:  column "pi" does not exist
  • 不带 d 后缀的三角函数输入是弧度,不是角度。 想算 30 度,优先用 sind(30);若用弧度版,必须写 sin(radians(30))。
sql
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],越界直接报错。 浮点误差导致的"略微越界"必须用夹紧处理。

sql
SELECT acos(1.0000000000000002);
-- 错误!ERROR:  input is out of range

SELECT acos(LEAST(GREATEST(1.0000000000000002, -1), 1)) AS safe;  -- 0
  • atan2 的参数顺序是 (y, x),不是 (x, y)。 写反了会把方位角整体旋转 90 度。
sql
SELECT degrees(atan2(1, 0)) AS north_like,   -- 90(y=1, x=0)
       degrees(atan2(0, 1)) AS east_like;    -- 0
  • atan 无法区分象限,atan2 可以。 求方位角必须用 atan2。
sql
SELECT degrees(atan(-1.0 / -1.0)) AS ambiguous,   -- 45(错,丢失象限信息)
       degrees(atan2(-1, -1))     AS correct;     -- -135
  • 返回 [0, 360) 的方位角需要手工归一化。
sql
-- 不推荐:直接使用,会得到负值
SELECT degrees(atan2(-1, -1)) AS negative_bearing;   -- -135

-- 推荐:加 360 再取模
SELECT (degrees(atan2(-1, -1)) + 360) % 360 AS normalized;  -- 225
  • acosh(x) 要求 x >= 1,atanh(x) 要求 -1 < x < 1,端点与越界都报错。
sql
SELECT acosh(0.5);
-- 错误!ERROR:  input is out of range

SELECT atanh(1);
-- 错误!ERROR:  input is out of range
  • 三角函数没有 numeric 重载,返回值一定是 double precision。 需要 numeric 时在最后一步转换。
sql
SELECT pg_typeof(sin(0.5::numeric)) AS t;  -- double precision
  • sin(pi()) 不会返回精确的 0。 浮点近似导致结果是 1.22e-16 量级,比较时必须用容差。
sql
SELECT sin(pi()) = 0               AS naive,   -- false
       abs(sin(pi())) < 1e-15      AS epsilon; -- true
  • NaN 会污染聚合结果。 一行 NaN 就能让 avg、sum 全部变成 NaN。
sql
-- 不推荐:直接把 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 的参数必须夹紧。 两个相同点之间计算距离时,浮点误差极易触发越界报错。
sql
-- 不推荐:可能报 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 列表逐行求值。

下一节

继续学习:NOW / CURRENT_DATE / CURRENT_TIME

参考链接 ​