3. 空间数据类型与几何对象建模
在 PostgreSQL 中启用 PostGIS 扩展后,最核心的收获是两类空间数据类型:geometry 与 geography。它们都能存储点、线、面等几何对象,但底层假设完全不同。geometry 类型基于平面直角坐标系,所有距离、面积、长度计算都在投影平面上按欧氏几何进行,速度快、函数丰富,是绝大多数空间分析的首选。geography 类型则基于椭球模型,坐标以经纬度(通常是 WGS84,SRID=4326)存储,距离与面积计算会自动考虑地球曲率,结果以米为单位,适合跨大范围、对真实距离敏感的场景,例如全球配送范围、航空航线长度估算。
选择哪一种,取决于数据尺度与精度要求。若数据集中在一个城市或省份,且已使用投影坐标系(如 UTM 或高斯-克吕格),用 geometry 即可,计算快且结果稳定。若数据覆盖全国乃至全球,又需要直接得到米制距离,用 geography 更省心,但代价是部分函数不可用、计算开销更大。实践中常见做法是:存储用 geography 保证语义正确,分析时用 ST_Transform 转到合适的投影再转成 geometry 计算。
几何对象的输入输出依赖三种格式。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 在其中的作用位置。
建表时,几何列的定义方式直接决定后续可用函数与数据一致性。推荐写法是显式声明类型与 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 的距离单位是米。若误把经纬度当平面坐标做面积统计,结果会严重偏小,这是初学者最常见的错误之一。
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 等范围查询会从全表扫描变为索引扫描,数据量越大收益越明显。建表方案确定后,应尽早补上索引,而不是等到查询变慢再回头添加。