广告位 · 页头(后台-模块管理-新建模块后替换此区域)

PostgreSQL的earthdistance扩展如何计算球面距离?

在业务系统里,只要涉及基于经纬度的距离判断,例如查找用户附近的门店、计算配送费用或按距离排序地点,球面距离就是一个绕不开的问题。PostgreSQL 提供了一个名为 earthdistance 的扩展,它不追求 PostGIS 那种完整的空间分析能力,但足够轻量,能直接把经纬度坐标转成可索引的 earth 类型,并用一个大圆距离操作符返回米制距离。如果项目只需要基础距离计算,earthdistance 可能比引入 PostGIS 更合适。

PostgreSQL的earthdistance扩展如何计算球面距离?

一、earthdistance 的安装与基础查询

earthdistance 扩展依赖 cube 扩展,因此启用时必须两个扩展一起创建。cube 提供多维立方体类型,earthdistance 利用它把地球表面坐标表达为三维空间中的点。安装过程非常简单,在 PostgreSQL 客户端执行以下 SQL 即可。

CREATE EXTENSION IF NOT EXISTS cube;
CREATE EXTENSION IF NOT EXISTS earthdistance;

安装完成后,可以调用 earth() 函数查看扩展内置的地球半径,默认值约为 6378168 米。这个常量已经考虑了地球的近似椭球形状,适合大多数业务场景。接着创建一张存储地点的表,坐标列使用 earth 类型。插入数据时不能直接写经纬度数字,而要使用 ll_to_earth(latitude, longitude) 函数进行转换,注意第一个参数是纬度,第二个参数是经度。

CREATE TABLE locations (
    id integer GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name text NOT NULL,
    coord earth NOT NULL
);
INSERT INTO locations (name, coord) VALUES
('纽约示例点', ll_to_earth(40.7128, -74.0060)),
('洛杉矶示例点', ll_to_earth(34.0522, -118.2437)),
('芝加哥示例点', ll_to_earth(41.8781, -87.6298));

计算距离时,使用 <@> 操作符连接两个 earth 值,返回结果是以米为单位的浮点数。下面的查询会计算每个地点到纽约示例点的距离,并按距离升序排列。

SELECT
    name,
    (coord <@> ll_to_earth(40.7128, -74.0060)) AS distance_meters
FROM locations
ORDER BY distance_meters;

这段查询的结果会显示纽约示例点自身距离为 0,洛杉矶约 3935 公里,芝加哥约 1144 公里。虽然这些数值与常见地图服务的结果有微小差异,但用于按距离筛选或排序已经足够。如果只需要判断某个半径内有哪些点,还可以进一步使用索引优化,而不是逐行计算全表距离。

二、球面距离的计算原理与精度

earthdistance 并不是简单地把经纬度当作平面坐标做欧几里得距离。它先把经纬度转换为三维空间中的单位向量,再通过向量夹角反推地球表面的大圆距离。这个思路与导航和航空中常用的大圆距离公式一致。对于两个点 P1 和 P2,转换后的三维向量夹角越小,实际地表距离就越短。earthdistance 底层依赖 cube 类型存储这个三维向量,并用扩展中的距离操作符一步完成点积和反余弦计算。

如果希望验证 earthdistance 的结果,可以用 SQL 手动实现大圆距离公式。PostgreSQL 提供 radians() 和三角函数,能够直接写出以下查询。

WITH params AS (
    SELECT
        40.7128 AS lat1, -74.0060 AS lon1,
        34.0522 AS lat2, -118.2437 AS lon2,
        6371000 AS R
)
SELECT R * acos(
    sin(radians(lat1)) * sin(radians(lat2)) +
    cos(radians(lat1)) * cos(radians(lat2)) *
    cos(radians(lon2 - lon1))
) AS great_circle_meters
FROM params;

这个查询使用 6371000 米作为地球半径,计算出的纽约到洛杉矶距离约为 3936 公里。earthdistance 默认半径为 6378168 米,结果会略有不同。差异主要来自半径常量的选取,而不是公式本身有错误。对于一般业务,几百米以内的差距通常不会影响附近门店展示或配送范围判断。如果业务要求极高精度,例如测绘或导航应用,应当改用 PostGIS 的 geography 类型,它能根据椭球模型给出更精确的距离。

除操作符外,earthdistance 还提供 earth_distance() 函数,二者计算结果一致,使用函数可以让 SQL 可读性更好。在理解原理时,还要注意 earth 类型在存储层面表现为 cube 类型,直接查询坐标列会看到类似三维坐标的表示。这个特性让 earthdistance 可以复用 cube 扩展的索引和操作符,但同时也带来一个限制:earth 类型本身不保存经纬度语义,排序或比较时不能把坐标当作普通数字处理。因此凡是涉及经纬度转换的场合,都要显式调用 ll_to_earth() 函数。

三、范围查询与 GIST 索引优化

当表中地点数量增长到几十万甚至百万级时,直接使用 <@> 对每一行做距离计算会导致全表扫描,查询性能会快速下降。earthdistance 通过 earth_box() 函数生成一个以目标点为中心、边长为指定半径两倍的立方体范围,再利用 cube 的 GIST 索引快速过滤掉大部分不相干的记录。对候选集进行精确距离计算,可以避免全表排序。

要启用索引,需要使用 GIST 索引,因为 earth 类型底层依赖 cube 的操作符类。创建索引的语句如下。

CREATE INDEX locations_coord_idx ON locations USING gist (coord);

执行范围查询时,把 earth_box() 条件放进 WHERE 子句,PostgreSQL 会优先走索引扫描。下面的语句查询距离纽约示例点 100 公里以内的地点。

SELECT
    name,
    (coord <@> ll_to_earth(40.7128, -74.0060)) AS distance_meters
FROM locations
WHERE earth_box(ll_to_earth(40.7128, -74.0060), 100000) @> coord
ORDER BY distance_meters;

这里 @> 表示 cube 的包含操作符,用来判断地点坐标是否落在目标立方体内。需要明确的是,earth_box() 只是一个粗糙的过滤条件,它过滤出的候选集可能包含一些实际距离超过 100 公里的点,因为立方体边界大于球面边界。因此最终仍然要计算精确距离,并用 ORDER BY 和条件完成准确筛选。对于大规模数据,这种先粗筛后精算的模式可以显著降低 CPU 开销。

还有一个经常被忽略的优化点:如果查询固定返回前 N 条最近地点,可以在外层使用 SELECT ... LIMIT n 配合距离排序。但 PostgreSQL 在没有索引助手的条件下仍可能对大量候选点排序,因此 earth_box() 的半径参数要结合业务合理设置,不宜设置过大。半径过大会让候选集膨胀,过小则可能漏掉边界地点,通常需要根据业务半径加上适当的缓冲值,例如目标 10 公里可设置 12 公里的过滤框。

四、earthdistance 与 PostGIS 的适用边界

在考虑地理位置功能时,PostGIS 常常是第一个被提起的方案。PostGIS 是 PostgreSQL 空间扩展的事实标准,支持点、线、多边形、坐标系转换、空间连接等复杂能力。如果项目里已经安装了 PostGIS,或者需要处理行政区划、轨迹分析、地理围栏等复杂场景,那么直接使用 PostGIS 是更合理的选择。但 PostGIS 也意味着更复杂的依赖、更大的安装体积和更高的学习成本,对于一些只做简单距离计算的轻量服务来说未必划算。

earthdistance 的优势集中在两点:一是依赖少,只需要 cube 扩展,不需要地理库;二是接口简单,两个函数加一个操作符就能覆盖多数距离查询。它特别适合已经使用 PostgreSQL 但不想引入额外地理组件的项目,例如创业项目的后台管理、内部数据分析、候选门店筛选等场景。对于需要按经纬度范围查询的 SQL,earthdistance 的 earth_box() 和 GIST 索引也能提供不错的性能。

但 earthdistance 的局限同样明显。它不支持真正的几何类型,无法处理多边形或路径,也没有坐标系转换能力。如果数据来自 GPS 设备,通常使用的是 WGS84 坐标,earthdistance 默认半径能够匹配;如果未来需要接入国内地图坐标系统,例如 GCJ-02 或 BD-09,则必须自己先做坐标纠偏,再写入 earth 列。另外,earthdistance 的索引基于 cube,不是专门为球面优化,海量数据下的性能与 PostGIS 的 geography 索引相比仍有差距。因此是否使用 earthdistance,本质上是一个复杂度与功能之间的权衡。

总结起来,如果业务只需要点对点距离、半径内筛选、距离排序这三件事,earthdistance 是一个体积小、上手快的方案;如果需求会扩展到复杂空间分析,那么从一开始选择 PostGIS 会更稳妥。对大多数中小型 PostgreSQL 应用来说,先把 earthdistance 用起来,后续确有需要再迁移到 PostGIS 也是可行的路径。

广告位 · 文章底部(后台-模块管理替换此区域)