强曰为道
与天地相似,故不违。知周乎万物,而道济天下,故不过。旁行而不流,乐天知命,故不忧.
文档目录

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 constraintSRID 或类型约束违反确保数据符合列约束
Coordinate values are out of range [-1e+308, 1e+308]坐标值无效检查 NaN 或 Infinity 值
lwgeom_intersection: GEOS ErrorGEOS 不支持的操作检查几何是否有效
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() 修复函数
查询错误类型不匹配、维度混合检查约束、统一维度

扩展阅读