3. 空间数据类型与几何对象建模

在 PostgreSQL 中启用 PostGIS 扩展后,最核心的收获是两类空间数据类型:geometry 与 geography。它们都能存储点、线、面等几何对象,但底层假设完全不同。geometry 类型基于平面直角坐标系,所有距离、面积、长度计算都在投影平面上按欧氏几何进行,速度快、函数丰富,是绝大多数空间分析的首选。geography 类型则基于椭球模型,坐标以经纬度(通常是 WGS84,SRID=4326)存储,距离与面积计算会自动考虑地球曲率,结果以米为单位,适合跨大范围、对真实距离敏感的场景,例如全球配送范围、航空航线长度估算。

选择哪一种,取决于数据尺度与精度要求。若数据集中在一个城市或省份,且已使用投影坐标系(如 UTM 或高斯-克吕格),用 geometry 即可,计算快且结果稳定。若数据覆盖全国乃至全球,又需要直接得到米制距离,用 geography 更省心,但代价是部分函数不可用、计算开销更大。实践中常见做法是:存储用 geography 保证语义正确,分析时用 ST_Transform 转到合适的投影再转成 geometry 计算。

SRID(Spatial Reference Identifier)是坐标参考系统的数字编号。SRID=4326 表示 WGS84 经纬度;SRID=3857 是 Web 墨卡托,常用于瓦片地图;SRID=4490 是 CGCS2000 经纬度。同一张表中所有 geometry 列的 SRID 应保持一致,跨 SRID 运算前必须显式转换,否则 PostGIS 会报错或给出错误结果。

几何对象的输入输出依赖三种格式。WKT(Well-Known Text)是可读文本,如 POINT(116.39 39.91),适合人工编写与调试。WKB(Well-Known Binary)是二进制形式,体积小、解析快,适合程序间传输。EWKB 是 PostGIS 对 WKB 的扩展,额外携带 SRID 与 Z/M 维度信息,是数据库内部存储与网络传输的默认格式。三者可通过 ST_AsText、ST_AsBinary、ST_GeomFromText、ST_GeomFromWKB 等函数互相转换。

下面这张图梳理了 geometry 与 geography 的差异,以及 WKT/WKB/EWKB 三种格式在“文本—二进制—数据库内部”之间的转换路径,同时标出 SRID 在其中的作用位置。

geometry 与 geography 类型及 WKT/WKB/EWKB 转换关系 geometry 平面直角坐标系 · 欧氏几何 • 距离/面积按平面计算,速度快 • 函数最全,支持所有空间分析 • 需指定投影 SRID(如 3857/4490) • 适合城市、省域、局部工程 • 单位随坐标系变化(度/米) geography 椭球模型 · 经纬度(WGS84) • 距离/面积自动考虑地球曲率 • 结果单位为米,语义直观 • 部分函数不可用,计算开销大 • 适合全国、全球、跨洲场景 • 默认 SRID=4326,不可随意改 ST_Transform + 类型转换 WKT / WKB / EWKB 格式转换链 WKT 文本 POINT(116.39 39.91) WKB 二进制 紧凑、无 SRID EWKB 扩展 含 SRID + Z/M 维度 ST_AsBinary ST_GeomFromText 附加 SRID ST_SetSRID 数据库内部存储默认使用 EWKB;网络传输常用 WKB;人工调试用 WKT

建表时,几何列的定义方式直接决定后续可用函数与数据一致性。推荐写法是显式声明类型与 SRID,例如 geom geometry(Point, 4326),这样插入不符合类型的几何会被拒绝,避免脏数据。若需要同时支持多种几何类型,可用 geometry(Geometry, 4326),但会失去类型约束。geography 列则写作 geog geography(Point, 4326),SRID 通常固定为 4326。

下面给出一个覆盖点、线、面、多几何与几何集合的建表示例,并插入示例数据。所有坐标均为教学示例,不代表真实地理位置。

CREATE TABLE spatial_objects (
    id          serial PRIMARY KEY,
    name        text NOT NULL,
    geom        geometry(Geometry, 4326),
    geog        geography(Geometry, 4326),
    created_at  timestamptz DEFAULT now()
);

-- 点:一个城市中心
INSERT INTO spatial_objects (name, geom, geog) VALUES
('city_center',
 ST_GeomFromText('POINT(116.39 39.91)', 4326),
 ST_GeogFromText('POINT(116.39 39.91)'));

-- 线:一条道路
INSERT INTO spatial_objects (name, geom, geog) VALUES
('road_line',
 ST_GeomFromText('LINESTRING(116.30 39.90, 116.35 39.92, 116.40 39.91)', 4326),
 ST_GeogFromText('LINESTRING(116.30 39.90, 116.35 39.92, 116.40 39.91)'));

-- 面:一个行政区域
INSERT INTO spatial_objects (name, geom, geog) VALUES
('district_poly',
 ST_GeomFromText('POLYGON((116.30 39.88, 116.45 39.88, 116.45 39.95, 116.30 39.95, 116.30 39.88))', 4326),
 ST_GeogFromText('POLYGON((116.30 39.88, 116.45 39.88, 116.45 39.95, 116.30 39.95, 116.30 39.88))'));

-- 多几何:多个点组成的集合
INSERT INTO spatial_objects (name, geom, geog) VALUES
('multi_point',
 ST_GeomFromText('MULTIPOINT((116.39 39.91), (116.40 39.92), (116.38 39.90))', 4326),
 ST_GeogFromText('MULTIPOINT((116.39 39.91), (116.40 39.92), (116.38 39.90))'));

-- 几何集合:点 + 线 + 面混合
INSERT INTO spatial_objects (name, geom, geog) VALUES
('geom_collection',
 ST_GeomFromText('GEOMETRYCOLLECTION(
   POINT(116.39 39.91),
   LINESTRING(116.30 39.90, 116.35 39.92),
   POLYGON((116.30 39.88, 116.45 39.88, 116.45 39.95, 116.30 39.95, 116.30 39.88))
 )', 4326),
 ST_GeogFromText('GEOMETRYCOLLECTION(
   POINT(116.39 39.91),
   LINESTRING(116.30 39.90, 116.35 39.92),
   POLYGON((116.30 39.88, 116.45 39.88, 116.45 39.95, 116.30 39.95, 116.30 39.88))
 )'));

插入后可用以下查询验证类型与 SRID 是否正确,并观察 geometry 与 geography 在距离计算上的差异:

SELECT name,
       ST_GeometryType(geom) AS geom_type,
       ST_SRID(geom)         AS srid,
       ST_AsText(geom)       AS wkt
FROM spatial_objects;

-- 比较同一对点的距离:geometry 按度,geography 按米
SELECT
  ST_Distance(
    ST_GeomFromText('POINT(116.39 39.91)', 4326),
    ST_GeomFromText('POINT(116.40 39.92)', 4326)
  ) AS geom_dist_deg,
  ST_Distance(
    ST_GeogFromText('POINT(116.39 39.91)'),
    ST_GeogFromText('POINT(116.40 39.92)')
  ) AS geog_dist_m;

示例结果中,geom_dist_deg 约为 0.014 度,而 geog_dist_m 约为 1500 米左右(具体数值随椭球参数略有浮动)。这个对比直观说明:geometry 的距离单位是坐标系单位,geography 的距离单位是米。若误把经纬度当平面坐标做面积统计,结果会严重偏小,这是初学者最常见的错误之一。

不要在同一列中混用不同 SRID 的几何。若必须存储多坐标系数据,应增加一列记录 SRID,或在插入前统一转换。跨 SRID 调用 ST_Distance、ST_Intersects 等函数会直接报错,而不是自动转换。

几何集合(GEOMETRYCOLLECTION)与多几何(MULTIPOINT、MULTILINESTRING、MULTIPOLYGON)容易混淆。多几何要求所有子对象类型一致,例如 MULTIPOINT 只能装点;几何集合则允许点、线、面混合。建表时若用 geometry(Geometry, 4326),两者都能存;若用 geometry(Point, 4326),则只能存单点,插入 MULTIPOINT 会失败。因此,类型约束的宽严需要在“数据干净”与“灵活扩展”之间权衡。

最后,为几何列建立 GiST 索引是空间查询性能的关键。对 geometry 列执行 CREATE INDEX idx_geom ON spatial_objects USING GIST (geom);,对 geography 列同理。索引生效后,ST_DWithin、ST_Intersects 等范围查询会从全表扫描变为索引扫描,数据量越大收益越明显。建表方案确定后,应尽早补上索引,而不是等到查询变慢再回头添加。