PostGIS 完全指南 / 第 17 章:故障排查
第 17 章:故障排查
17.1 常见安装问题
问题 1:扩展不存在
-- 错误: ERROR: could not open extension control file
CREATE EXTENSION postgis;
原因: PostGIS 包未安装或版本不匹配。
解决方案:
# Ubuntu/Debian
sudo apt install postgresql-16-postgis-3
# CentOS/RHEL
sudo dnf install postgresql16-postgis3
# 验证安装
dpkg -l | grep postgis # Debian
rpm -qa | grep postgis # RHEL
问题 2:函数不存在
-- 错误: ERROR: function postgis_version() does not exist
原因: 扩展已安装但未在当前数据库启用。
解决方案:
-- 在目标数据库中执行
CREATE EXTENSION IF NOT EXISTS postgis;
问题 3:共享库加载失败
ERROR: could not load library "postgis-3.so"
原因: 依赖库缺失或版本冲突。
解决方案:
# 检查依赖
ldd /usr/lib/postgresql/16/lib/postgis-3.so
# 安装缺失依赖
sudo apt install libgeos-dev libproj-dev libgdal-dev
# 重启 PostgreSQL
sudo systemctl restart postgresql
17.2 常见几何错误
错误 1:无效几何 (Invalid Geometry)
-- 自相交多边形
SELECT ST_IsValid(
ST_GeomFromText('POLYGON((0 0, 2 2, 2 0, 0 2, 0 0))')
);
-- 输出: FALSE
-- 查看无效原因
SELECT ST_IsValidReason(
ST_GeomFromText('POLYGON((0 0, 2 2, 2 0, 0 2, 0 0))')
);
-- 输出: Self-intersection [1 1]
修复方案:
-- 使用 ST_MakeValid 修复
SELECT ST_MakeValid(
ST_GeomFromText('POLYGON((0 0, 2 2, 2 0, 0 2, 0 0))')
);
-- 输出: MULTILINESTRING((0 0,2 2),(2 0,0 2))
-- 注意: 修复后可能变成 MultiPolygon 或 GeometryCollection
-- 批量修复表中所有无效几何
UPDATE my_table
SET geom = ST_MakeValid(geom)
WHERE NOT ST_IsValid(geom);
-- 查看修复前后的类型变化
SELECT
ST_GeometryType(geom) AS original_type,
ST_GeometryType(ST_MakeValid(geom)) AS fixed_type,
ST_IsValid(geom) AS was_valid
FROM my_table
WHERE NOT ST_IsValid(geom);
错误 2:坐标顺序错误
-- 经纬度搞反了
-- 正确: ST_MakePoint(经度, 纬度) = ST_MakePoint(116.4, 39.9)
-- 错误: ST_MakePoint(39.9, 116.4)
-- 检测方法:查看坐标范围
SELECT
ST_XMin(ST_Extent(geom)) AS min_x,
ST_XMax(ST_Extent(geom)) AS max_x,
ST_YMin(ST_Extent(geom)) AS min_y,
ST_YMax(ST_Extent(geom)) AS max_y
FROM my_table;
-- WGS84 经度范围: -180 ~ 180
-- WGS84 纬度范围: -90 ~ 90
-- 如果 max_x > 90 且 max_y > 90,很可能是经纬度搞反了
修复方案:
-- 交换经纬度
UPDATE my_table
SET geom = ST_SetSRID(
ST_MakePoint(ST_Y(geom), ST_X(geom)),
4326
);
错误 3:SRID 不匹配
-- 错误: ERROR: Operation on mixed SRID geometries
SELECT ST_Intersects(
ST_SetSRID(ST_MakePoint(116.4, 39.9), 4326),
ST_SetSRID(ST_MakePoint(116.4, 39.9), 4547)
);
修复方案:
-- 统一 SRID
-- 方式 1: 转换到统一坐标系
SELECT ST_Intersects(
ST_SetSRID(ST_MakePoint(116.4, 39.9), 4326),
ST_Transform(ST_SetSRID(ST_MakePoint(447963, 4418554), 4547), 4326)
);
-- 方式 2: 确保所有数据使用相同 SRID
UPDATE my_table SET geom = ST_Transform(geom, 4326) WHERE ST_SRID(geom) != 4326;
错误 4:几何维度不匹配
-- 错误: ERROR: lwgeom_intersection: GEOS Error: UnsupportedOperationException
-- 原因: 2D 几何与 3D 几何混合操作
-- 检查维度
SELECT ST_NDims(geom), count(*) FROM my_table GROUP BY ST_NDims(geom);
-- 统一为 2D
UPDATE my_table SET geom = ST_Force2D(geom) WHERE ST_NDims(geom) > 2;
17.3 查询性能问题
问题 1:查询缓慢
-- 诊断步骤
EXPLAIN (ANALYZE, BUFFERS)
SELECT name FROM stores WHERE ST_Intersects(geom, target_geom);
常见原因与解决:
| 现象 | 原因 | 解决方案 |
|---|---|---|
| Seq Scan | 未创建索引 | CREATE INDEX USING GIST(geom) |
| Index Scan 很慢 | 查询范围太大 | 增加空间过滤条件 |
| Rows Removed by Filter 高 | 索引选择性差 | 使用部分索引或缩小范围 |
| Sort 大量行 | 未利用 KNN | 使用 <-> 操作符 |
问题 2:索引未生效
-- 检查索引是否存在
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'stores' AND indexdef LIKE '%gist%';
-- 检查统计信息
ANALYZE stores;
-- 检查查询计划
SET enable_seqscan = off; -- 仅调试
EXPLAIN ANALYZE SELECT ...;
RESET enable_seqscan;
问题 3:内存不足
-- 错误: ERROR: out of memory
-- 原因: 复杂空间操作需要大量内存
-- 增加 work_mem
SET work_mem = '1GB';
-- 或分批处理
-- 大范围 UNION 操作时分块执行
WITH chunks AS (
SELECT ST_Union(geom) AS geom
FROM districts
WHERE id BETWEEN 1 AND 100
UNION ALL
SELECT ST_Union(geom)
FROM districts
WHERE id BETWEEN 101 AND 200
)
SELECT ST_Union(geom) FROM chunks;
17.4 数据质量问题
检测无效几何
-- 统计无效几何数量
SELECT
count(*) FILTER (WHERE ST_IsValid(geom)) AS valid_count,
count(*) FILTER (WHERE NOT ST_IsValid(geom)) AS invalid_count,
count(*) FILTER (WHERE ST_IsEmpty(geom)) AS empty_count,
count(*) FILTER (WHERE geom IS NULL) AS null_count
FROM my_table;
-- 查看无效几何的详细信息
SELECT id, ST_IsValidReason(geom) AS reason
FROM my_table
WHERE NOT ST_IsValid(geom)
LIMIT 20;
检测退化几何
-- 空几何
SELECT id FROM my_table WHERE ST_IsEmpty(geom);
-- 零长度线
SELECT id FROM roads WHERE ST_Length(geom) = 0;
-- 零面积面
SELECT id FROM districts WHERE ST_Area(geom::geography) < 1;
-- 线太短(可能是噪声)
SELECT id FROM roads WHERE ST_Length(geom::geography) < 1; -- < 1 米
检测拓扑错误
-- 重叠检测
SELECT a.id AS id_a, b.id AS id_b
FROM districts a, districts b
WHERE a.id < b.id
AND ST_Overlaps(a.geom, b.geom);
-- 间隙检测
WITH union_geom AS (
SELECT ST_Union(geom) AS geom FROM districts
)
SELECT ST_Difference(
ST_Envelope(geom), geom
) AS gaps
FROM union_geom
WHERE NOT ST_Equals(ST_Envelope(geom), geom);
-- 悬挂点检测(线要素)
SELECT id
FROM roads
WHERE NOT EXISTS (
SELECT 1 FROM roads r2
WHERE r2.id != roads.id
AND ST_Intersects(ST_StartPoint(roads.geom), r2.geom)
)
OR NOT EXISTS (
SELECT 1 FROM roads r2
WHERE r2.id != roads.id
AND ST_Intersects(ST_EndPoint(roads.geom), r2.geom)
);
17.5 数据修复工具箱
修复无效几何
-- 通用修复函数
CREATE OR REPLACE FUNCTION fix_geometry(
input_geom GEOMETRY,
tolerance DOUBLE PRECISION DEFAULT 0.0
) RETURNS GEOMETRY AS $$
DECLARE
result GEOMETRY;
BEGIN
result := input_geom;
-- 步骤 1: 尝试 MakeValid
IF NOT ST_IsValid(result) THEN
result := ST_MakeValid(result);
END IF;
-- 步骤 2: 如果还有问题,尝试缓冲区修复
IF NOT ST_IsValid(result) THEN
result := ST_Buffer(result, 0);
END IF;
-- 步骤 3: 如果指定了容差,简化几何
IF tolerance > 0 AND ST_IsValid(result) THEN
result := ST_SimplifyPreserveTopology(result, tolerance);
END IF;
-- 步骤 4: 确保结果有效
IF NOT ST_IsValid(result) THEN
RAISE WARNING 'Cannot fix geometry: %', ST_IsValidReason(result);
RETURN NULL;
END IF;
RETURN result;
END;
$$ LANGUAGE plpgsql IMMUTABLE;
-- 批量修复
UPDATE my_table
SET geom = fix_geometry(geom, 0.00001)
WHERE NOT ST_IsValid(geom);
修复坐标精度
-- 降低坐标精度(去除浮点噪声)
UPDATE my_table
SET geom = ST_GeomFromText(ST_AsText(geom, 8), ST_SRID(geom));
-- 统一坐标精度
CREATE OR REPLACE FUNCTION normalize_precision(
geom GEOMETRY,
precision INTEGER DEFAULT 8
) RETURNS GEOMETRY AS $$
BEGIN
RETURN ST_GeomFromText(ST_AsText(geom, precision), ST_SRID(geom));
END;
$$ LANGUAGE plpgsql IMMUTABLE;
修复多边形方向
-- OGC 标准:外环逆时针,内环顺时针
-- PostGIS 自动处理,但某些数据源可能方向不对
-- 强制外环逆时针
UPDATE my_table
SET geom = ST_ForceRHR(geom)
WHERE ST_GeometryType(geom) = 'ST_Polygon';
17.6 调试技巧
开启详细日志
-- 查看 GEOS 版本(影响几何操作)
SELECT PostGIS_GEOS_Version();
-- 开启详细错误信息
SET client_min_messages = 'debug';
-- 执行有问题的查询
SELECT ST_Intersects(geom_a, geom_b) FROM problem_table;
RESET client_min_messages;
逐步排查复杂查询
-- 复杂查询拆分为多个步骤
-- 原始查询
SELECT * FROM a JOIN b ON ST_Intersects(a.geom, b.geom) WHERE ...;
-- 拆分排查
-- 步骤 1: 确认数据存在
SELECT count(*) FROM a WHERE ...;
SELECT count(*) FROM b WHERE ...;
-- 步骤 2: 测试边界框过滤
SELECT count(*) FROM a, b WHERE a.geom && b.geom AND ...;
-- 步骤 3: 测试精确空间关系
SELECT count(*) FROM a, b WHERE a.geom && b.geom AND ST_Intersects(a.geom, b.geom) AND ...;
-- 步骤 4: 测试完整查询
EXPLAIN ANALYZE SELECT * FROM a JOIN b ON ST_Intersects(a.geom, b.geom) WHERE ...;
检查几何有效性
-- 创建诊断视图
CREATE OR REPLACE VIEW geometry_diagnostics AS
SELECT
'my_table' AS table_name,
id,
ST_GeometryType(geom) AS geom_type,
ST_SRID(geom) AS srid,
ST_NDims(geom) AS ndims,
ST_NPoints(geom) AS npoints,
ST_IsValid(geom) AS is_valid,
ST_IsValidReason(geom) AS validity_reason,
ST_IsEmpty(geom) AS is_empty,
ST_IsSimple(geom) AS is_simple,
ST_XMin(geom) AS xmin,
ST_YMin(geom) AS ymin,
ST_XMax(geom) AS xmax,
ST_YMax(geom) AS ymax
FROM my_table;
-- 快速定位问题数据
SELECT * FROM geometry_diagnostics
WHERE NOT is_valid OR is_empty OR xmin < -180 OR xmax > 180;
17.7 常见错误消息
| 错误消息 | 原因 | 解决方案 |
|---|---|---|
Geometry type does not match column | 几何类型不匹配 | 检查列定义与数据类型 |
new row violates geometry column constraint | SRID 或类型约束违反 | 确保数据符合列约束 |
Coordinate values are out of range [-1e+308, 1e+308] | 坐标值无效 | 检查 NaN 或 Infinity 值 |
lwgeom_intersection: GEOS Error | GEOS 不支持的操作 | 检查几何是否有效 |
transform: could not form projection | 坐标转换失败 | 检查 SRID 是否存在 |
column "geom" is of type geometry but expression is of type text | 类型转换错误 | 使用 ST_GeomFromText() |
column "geom" does not exist | 列名错误 | 检查表结构 |
-- 检查约束
SELECT conname, pg_get_constraintdef(oid)
FROM pg_constraint
WHERE conrelid = 'my_table'::regclass
AND contype = 'c';
17.8 灾难恢复
从损坏的数据库恢复
# 1. 尝试导出数据
pg_dump -U postgres -d gisdb --data-only -Fc -f recovery.dump
# 2. 如果 pg_dump 失败,尝试 COPY
docker exec postgis psql -U postgres -d gisdb -c \
"COPY (SELECT * FROM my_table) TO STDOUT WITH (FORMAT csv)" > recovery.csv
# 3. 如果数据库无法启动
# 使用 pg_resetwal 重置 WAL(最后手段)
pg_resetwal /var/lib/postgresql/data
# 4. 从备份恢复
pg_restore -U postgres -d gisdb_new recovery.dump
数据一致性检查
-- 检查表和索引的一致性
SELECT
schemaname, tablename,
pg_size_pretty(pg_relation_size(schemaname || '.' || tablename)) AS table_size,
pg_size_pretty(pg_indexes_size(schemaname || '.' || tablename)) AS index_size,
n_dead_tup,
last_vacuum,
last_autovacuum
FROM pg_stat_user_tables
WHERE schemaname = 'public'
ORDER BY n_dead_tup DESC;
17.9 诊断脚本集
全面诊断脚本
-- PostGIS 诊断报告
DO $$
DECLARE
rec RECORD;
BEGIN
RAISE NOTICE '=== PostGIS 诊断报告 ===';
-- 版本信息
RAISE NOTICE 'PostgreSQL: %', version();
RAISE NOTICE 'PostGIS: %', PostGIS_Full_Version();
-- 空间表统计
FOR rec IN
SELECT f_table_name, f_geometry_column, srid, type, count
FROM geometry_columns gc
JOIN (
SELECT f_table_name, count(*)
FROM geometry_columns
GROUP BY f_table_name
) c ON gc.f_table_name = c.f_table_name
LOOP
RAISE NOTICE '表: %.%, SRID: %, 类型: %',
rec.f_table_name, rec.f_geometry_column, rec.srid, rec.type;
END LOOP;
-- 无效几何统计
FOR rec IN
SELECT
table_name,
(xpath('/row/cnt/text()', query_to_xml(
format('SELECT count(*) AS cnt FROM %I WHERE NOT ST_IsValid(geom)', table_name),
false, true, ''
)))[1]::text::int AS invalid_count
FROM information_schema.tables
WHERE table_schema = 'public'
LOOP
IF rec.invalid_count > 0 THEN
RAISE WARNING '表 % 存在 % 个无效几何!', rec.table_name, rec.invalid_count;
END IF;
END LOOP;
END $$;
17.10 本章小结
| 问题类别 | 常见原因 | 排查方法 |
|---|---|---|
| 安装问题 | 包未安装、版本不匹配 | apt list, CREATE EXTENSION |
| 几何错误 | 无效几何、SRID 不匹配 | ST_IsValid, ST_IsValidReason |
| 性能问题 | 缺少索引、统计过时 | EXPLAIN ANALYZE, ANALYZE |
| 数据质量 | 自相交、退化几何 | fix_geometry() 修复函数 |
| 查询错误 | 类型不匹配、维度混合 | 检查约束、统一维度 |