在开发涉及地理位置的功能时,我们经常需要回答一个看似简单的问题:两个经纬度坐标点之间到底相距多少米?如果把这个计算放到应用层做,数据量大时性能会很糟糕,而且要先把所有候选数据查出来再逐条过滤,逻辑也不优雅。PostgreSQL提供的earthdistance扩展把这个能力直接下推到数据库层,配合索引可以实现高效的附近查询。本文围绕这个扩展的安装、核心函数和实战用法展开讲解。

一、earthdistance扩展的安装与启用
earthdistance在PostgreSQL中不是默认启用的,它依赖cube扩展提供底层的多维立方体数据结构。启用过程很简单,以超级用户或数据库所有者身份连接到目标数据库,执行下面的语句即可。
-- 先安装基础扩展cube CREATE EXTENSION cube; -- 再安装earthdistance,它依赖cube CREATE EXTENSION earthdistance;
注意两条语句的顺序不能颠倒,因为earthdistance内部使用cube类型来表示地球上的三维坐标点,如果cube没有先创建,第二条语句会直接报错。如果你使用的是云数据库服务,通常这两个扩展都在支持列表里,直接执行即可;如果是自编译安装的PostgreSQL,需要确认contrib模块已经编译,否则在文件系统里根本找不到对应的控制文件。
安装成功后可以通过系统视图确认:
SELECT extname FROM pg_extension ORDER BY extname; -- 结果中应能看到 cube 和 earthdistance 两条记录
另外要说明一点,earthdistance默认使用英里作为距离单位,但它也提供了以米为单位的函数版本。这一点在后面讲函数时会具体展开,是很多初学者容易踩坑的地方——算出来的数字差了1600多倍,多半就是单位搞混了。
二、核心函数详解:从坐标转换到距离计算
1. ll_to_earth:把经纬度转成地球坐标
earthdistance内部并不直接用经纬度计算,而是把地球近似为一个球体,先将经纬度转换为以地球中心为原点的三维笛卡尔坐标,存储为cube类型。转换函数就是ll_to_earth,它接收纬度和经度两个参数:
SELECT ll_to_earth(39.9042, 116.4074); -- 北京的坐标,返回一个cube类型的点,形如 (-2174.28, 4390.83, 4069.10)
这里第一个参数是纬度,第二个是经度,顺序不能写反。很多数据的字段习惯是先经度后纬度,直接传进去得到的距离完全是错的,而且不会报任何错误,排查起来很隐蔽。返回的cube点配合GiST索引,是后面做范围查询提速的关键。
2. earth_distance:计算两点球面距离
earth_distance有两种用法。第一种传入两个earth值也就是cube类型的坐标点,返回米为单位的距离;第二种传入四个浮点数,分别是两点的纬度和经度,直接返回英里为单位的大圆距离。
-- 用法一:传入两个cube点,单位是米
SELECT earth_distance(
ll_to_earth(39.9042, 116.4074), -- 北京
ll_to_earth(31.2304, 121.4737) -- 上海
);
-- 结果约为 1068300 左右,即约1068公里
-- 用法二:传入四个经纬度数值,单位是英里
SELECT earth_distance(39.9042, 116.4074, 31.2304, 121.4737);
-- 结果约为 663.7 英里同一个城市对,两种写法单位不同,这一点务必注意。实战中推荐用法一,因为它可以和earth_box配合使用,并且能走索引。
3. earth_box与earth_distance的组合查询
单独用earth_distance计算距离需要逐行比较,数据量大时是全表扫描。正确的提速姿势是先用earth_box把范围缩小:它返回一个以指定点为中心、给定半径的立方体包围盒。判断某点是否在这个盒内,可以使用cube类型自带的cube_contains操作符,也可以用earthdistance提供的<@相关操作。下面的例子演示查询北京10公里范围内的所有地点:
-- 建表并插入示例数据
CREATE TABLE places (
id serial PRIMARY KEY,
name text,
lat float8,
lng float8
);
CREATE INDEX idx_places_earth ON places USING gist (ll_to_earth(lat, lng));
-- 查询北京(39.9042, 116.4074)周围10公里内的地点
SELECT name,
earth_distance(
ll_to_earth(39.9042, 116.4074),
ll_to_earth(lat, lng)
) AS distance_m
FROM places
WHERE earth_box(ll_to_earth(39.9042, 116.4074), 10000) @> ll_to_earth(lat, lng)
ORDER BY distance_m
LIMIT 20;这里的查询分两层:WHERE条件利用GiST索引快速过滤出大致在10公里范围内的候选行,SELECT中的earth_distance再精确计算球面距离并排序。为什么两个都要写?因为earth_box判断的是立方体包围盒,盒子角落的点可能超出半径范围,必须用精确距离再过滤一次。更严谨的写法是把精确距离条件也放进WHERE:
SELECT name,
earth_distance(ll_to_earth(39.9042, 116.4074), ll_to_earth(lat, lng)) AS distance_m
FROM places
WHERE earth_box(ll_to_earth(39.9042, 116.4074), 10000) @> ll_to_earth(lat, lng)
AND earth_distance(ll_to_earth(39.9042, 116.4074), ll_to_earth(lat, lng)) <= 10000
ORDER BY distance_m;这个模式在附近的人、附近门店等场景中非常经典,理解了包围盒粗筛加精确距离复筛的思路,迁移到其他地理方案时也是通用的。
三、earthdistance与PostGIS的选择建议
earthdistance的优点是轻量,不需要安装庞大的PostGIS,两个CREATE EXTENSION就能跑起来,对于只有距离计算和简单附近查询需求的应用完全够用。但它有两个明显局限:第一,它把地球当成完美的球体,而真实地球是扁球体,赤道半径和极半径相差约21公里,因此计算结果存在大约0.5%以内的误差,对于打车计费这类对精度敏感的场景可能不够;第二,它只支持距离和范围查询,无法处理多边形、路径相交、地理编码等复杂空间运算。
PostGIS则是专业的地理信息系统扩展,支持椭球体模型(Geography类型配合KNN索引),精度更高,功能覆盖面也广得多,代价是安装配置复杂、学习曲线陡峭。如果项目未来可能涉及行政区域判断、轨迹分析、地图可视化等需求,建议一开始就选PostGIS;如果只是做附近几公里的简单查询,earthdistance反而是性价比更高的选择。
还有一点容易被忽略:earthdistance的纬度参数如果超出正负90度、经度超出正负180度,会直接抛出错误,因此写入数据前最好在应用层做校验,或者在数据库端加CHECK约束,避免脏数据导致查询中途失败。整体来说,earthdistance是一个小而美的工具,理解它的球体近似原理和索引配合方式,就能在绝大多数轻量级地理查询场景中游刃有余。
earthdistancePostgreSQL地球距离计算修改时间:2026-09-14 07:42:40