简介:《人大金仓-KingbaseES GIS数据迁移方案》面向数据库管理员、GIS应用开发与迁移实施人员,围绕空间数据从ArcGIS、GeoScene、SuperMap及通用格式迁入KingbaseES V8.6的场景,梳理可落地的迁移路径与验证方法。压缩包内仅含1个PDF文件,约2.76MB,便于集中查阅与随项目实施归档。文档依次介绍KingbaseES对矢量、栅格数据的支撑能力,基于KDTS的抽取、转换、加载操作,ArcGIS/GeoScene软件侧迁移配置,SuperMap平台迁移及通用GIS文件入库,并给出迁移结果核验与常见问题汇总,能帮助读者理解迁移前后的对象映射、连接配置、完整性检查和排错思路。当前已有137人学习,适合需要开展国产数据库GIS数据迁移选型、实施与验证的技术人员参考。
1. GIS 数据迁到 KingbaseES V8.6:第一件该做的事不是导数据
上个月帮一个测绘类团队把一套空间库从老平台搬到 KingbaseES V8.6,对方给的时间是两天,理由是“都是 PG 系,ogr2ogr 一把梭就行”。第一天下午他们卡在几何列全空,第二天卡在面积统计差了 30%,第三天开始怀疑是坐标系定义缺失。这类 GIS 数据迁移的翻车点,几乎都不在“导”这个动作上,而在导之前的盘点:源端到底有几种几何类型、SRID 是不是混着存的、目标端的空间参考表里有没有 4490 这条记录。标题里的 kingbase_v8.6 指的就是 V8.6 这条版本线,它自带的空间扩展兼容 PostGIS 的函数体系,但兼容不等于等价,函数签名、SRID 字典、索引行为都有差异。这套方案适合手里有 PostGIS、SDE 或文件型空间数据、需要一次性或分批搬进 KingbaseES 的从业者。下面按“先盘点、再落库、后校验、最后排性能”的顺序讲,能照着复现。
2. 迁前盘点:把源端空间资产摸成一张可核对的清单
盘点的目的很朴素:让“搬完之后对不对”这件事在搬之前就有标准答案。你要摸清四样东西——几何列在哪些表、每列的几何类型和维度、SRID 是否唯一、空间索引建在哪些列上。这四样里任意一项在源端就是脏的,迁过去只会更脏,而且脏得更难查。我一般会先在源端跑一遍只读查询,把结果贴进一张表格,再拿这张表去跟目标端逐项对。
2.1 四类源端和它们的几何表达差异
不同源端的空间数据落盘方式差别很大,迁移策略也不同,先认清自己是哪一类,能省掉一半试错。
| 源端类型 | 几何存储形式 | 迁移难点 | 建议路径 |
|---|---|---|---|
| PostGIS 类库 | geometry/geography 列,带 typmod | 扩展函数签名差异、自定义函数依赖 | 结构改造 + 数据通道迁移 |
| 商业空间中间件 | ST_Geometry 自定义类型 | 无法直接 dump 成标准几何 | 先导出 WKT/WKB 或中间文件 |
| 文件型(shp/GeoJSON/GPKG) | 文件内自带几何与属性 | 编码、字段名截断、2GB 上限 | GDAL 直读或转中间格式 |
| 其他库的空间扩展 | 多为自有类型 | 函数语义不一致,SRID 字典不全 | 一律转 WKB 中转 |
表格里最容易被低估的是第二行。带中间件的库,几何列在数据库里往往是个自定义类型,直接 dump 出来目标端根本不认,必须先通过中间件或客户端把几何序列化成 WKT/WKB,再走标准通道。文件型的坑则在编码:DBF 里的中文属性一旦按错误代码页读进来,后面再改就麻烦了。
2.2 用 SQL 和 ogrinfo 把 SRID、几何类型、索引摸清
源端是 PostGIS 系的话,下面这条查询能一次把空间列的家底摊开。它读的是 geometry_columns 视图,这个视图在各家兼容实现里基本都有,是跨库盘点最稳的入口。
-- 源端盘点:哪些表有空间列、什么类型、几维、SRID 是多少 SELECT f_table_schema AS schema_name, f_table_name AS table_name, f_geometry_column AS geom_col, coord_dimension AS dims, srid, type FROM geometry_columns ORDER BY 1, 2;结果里重点看两列:srid 和 type。如果同一张表出现多个 srid,说明这列没加 typmod 约束,脏数据是历史遗留的;如果 type 是 GEOMETRY 而不是具体类型,说明这一列什么几何都能塞,迁移时必须显式指定目标类型,否则目标端会退化成通用类型,索引效率直接掉一个档。
接着统计每张表的实际数据健康状况,这条查询会告诉你几何有没有空值、有没有非法几何、SRID 是不是真的统一。
-- 健康度抽查:空几何、非法几何、SRID 混杂情况 SELECT count(*) AS total_rows, count(*) FILTER (WHERE geom IS NULL) AS null_geom, count(*) FILTER (WHERE ST_IsValid(geom) = false) AS invalid_geom, count(DISTINCT ST_SRID(geom)) AS srid_kinds FROM public.pipe_line;invalid_geom 不为 0 的话,先别急着迁,自相交的多边形在目标端建索引时可能直接报错中断,几百 G 的表迁到 90% 挂掉,那种体验一次就够了。
索引清单单独查,别指望靠肉眼翻建表语句:
-- 空间索引清单:确认哪些几何列真的建了 GiST SELECT tablename, indexname, indexdef FROM pg_indexes WHERE schemaname = 'public' AND indexdef ILIKE '%gist%';文件型源端用 GDAL 自带的 ogrinfo,一条命令把图层结构、要素数、坐标系、字段类型全打出来,比打开桌面软件点半天可靠。
# 不加载数据,只读元信息:图层名、几何类型、要素数、SRS ogrinfo -so -al ./data/city_roads.shp输出里认准两行:Geometry 和 Feature Count。Geometry 若是 3D 或带 M 值,目标端建表时维度要跟着改,否则 Z 值会被静默丢掉——静默丢失是最难查的一类问题,因为导入过程不报错。
2.3 目标端 KGIS 能力核对表
盘点完源端,反过来核目标端。KingbaseES V8.6 的空间能力由 KGIS 扩展提供,扩展名和函数命名在不同小版本上有差异,稳妥做法是先看扩展清单,不要凭记忆写 CREATE EXTENSION。
-- 目标端:看空间扩展到底叫什么、装没装 SELECT name, default_version, installed_version FROM pg_available_extensions WHERE name IN ('kgis', 'postgis'); -- 装好之后验证版本与函数可用性 SELECT postgis_version();版本号能查出来,说明扩展加载成功、函数库链接正常。查不出来就是扩展没装或者 search_path 没带进去,这一步不通,后面所有几何操作都是空谈。
再核对空间参考字典。国内业务里 4490(2000 国家大地坐标系)出现频率极高,如果目标端的 spatial_ref_sys 里没有这条记录,导入时 SRID 会被识别失败,常见的表现是几何能进去但 ST_SRID 返回 0,或者被迫重投影到 4326,整套数据的平面位置整体偏移。
-- 常用 SRID 是否在字典里:4326 经纬度、4490 国家2000、3857 墨卡托 SELECT srid, auth_name, auth_srid, left(srtext, 60) AS srtext_head FROM spatial_ref_sys WHERE srid IN (4326, 4490, 3857);缺哪条就补哪条,补的时候用标准的 WKT 定义,不要自己手写参数,投影参数错一位,几万平方公里的数据就整体漂了。这一步做完,盘点才算闭环:源端有什么、目标端能不能接、中间差在哪,三件事都有答案了,再开始搬。
3. 落库实操:从文件型和库对库两条路把几何搬进 KingbaseES
落库阶段分两条路:源端是文件就走 GDAL,源端是库就走逻辑导出。两条路的共同前提是目标库先建好、扩展先装上、几何列的类型和 SRID 先定死——先建表再灌数据,比让工具自动建表可控得多,自动建表最常见的问题是几何类型退化、SRID 写成 0、索引一个没建。
3.1 建库、扩展加载与最小验证
KingbaseES 的客户端工具是 ksql,服务端口常见为 54321,具体以实际部署为准。先连通再谈其他。
# 连通性验证:版本 + 当前库 + 扩展可用性 ksql -h 10.0.0.11 -p 54321 -U system -d gisdb \ -c "SELECT version();" \ -c "SELECT current_database();"建库时显式指定 UTF8 和 template0,避免从模板库继承来奇怪的 locale 和已装扩展。这一步看着琐碎,但迁移现场最常见的“字符集不一致导致属性乱码”,根子就在建库这一句。
-- 建库:显式编码与模板,避免继承脏配置 CREATE DATABASE gisdb WITH ENCODING = 'UTF8' LC_COLLATE = 'zh_CN.UTF-8' LC_CTYPE = 'zh_CN.UTF-8' TEMPLATE = template0; -- 切库后加载空间扩展(扩展名以 2.3 查到的为准) \c gisdb CREATE EXTENSION IF NOT EXISTS kgis; -- 最小验证:一个点、一次距离计算,能出数就说明函数链路通 SELECT ST_AsText(ST_SetSRID(ST_MakePoint(116.39, 39.90), 4326)) AS pt, ST_Distance(ST_SetSRID(ST_MakePoint(116.39, 39.90), 4326)::geography, ST_SetSRID(ST_MakePoint(116.40, 39.91), 4326)::geography) AS dist_m;这段最小验证别跳过。它同时验证了三件事:几何构造函数可用、SRID 设置可用、geography 类型的米制距离可用。三件事里任何一件不通,都说明扩展装得不完整,此时灌数据只会得到一堆无法参与空间计算的行。
3.2 文件型数据:ogr2ogr 直连与中间格式两条路
目标端兼容 PG 线协议和对应空间函数时,GDAL 的 PostgreSQL 驱动可以直连写入,这是最快的一条路。命令里几个参数值得逐个说清楚:
# 直连写入:显式指定图层名、几何列名、主键名、空间索引、几何类型 ogr2ogr -f PostgreSQL \ "PG:host=10.0.0.11 port=54321 dbname=gisdb user=system password=***" \ ./data/city_roads.shp \ -nln public.city_roads \ -lco GEOMETRY_NAME=geom \ -lco FID=gid \ -lco SPATIAL_INDEX=GIST \ -nlt PROMOTE_TO_MULTI \ -t_srs EPSG:4490 \ -progress-lco SPATIAL_INDEX=GIST 是最容易漏的一条。漏了它,数据一条不少地进去了,查询却慢十倍,因为整表就一个堆。注意它是“建索引”而不是“建对索引”,如果目标端不支持这个 layer creation option,你还得手工补 CREATE INDEX。
-nlt PROMOTE_TO_MULTI 处理的是混合几何类型:源图层里既有单线又有多线时,目标列若声明为 MULTILINESTRING,单线会被直接拒绝,提升成多线才能整体灌进去。代价是几何包装多一层,面积长度计算不受影响。
-t_srs EPSG:4490 是重投影开关,用它是把源坐标“算”成 4490,不是“标”成 4490。如果源数据本来就是 4490 只是元数据丢了,用错这一项就是二次变换,位置会整体偏移。判断方法:ogrinfo 输出里的 SRS 若为空或明显不对,而你又确信数据本身是目标坐标系,那就改用 -a_srs EPSG:4490 只打标签、不做变换。
直连驱动走不通时(比如目标端不接受某些系统表查询),退回中间格式:把几何导成 WKT 文本,属性走 CSV,再用 COPY 灌进去。慢一些,但每一步都可控、可断点续传、可肉眼检查。
# 中间格式导出:几何转 WKT,属性原样落 CSV,绕开直连驱动的兼容问题 from osgeo import ogr import csv src = ogr.Open("./data/city_roads.shp") layer = src.GetLayer(0) fields = [layer.GetLayerDefn().GetFieldDefn(i).GetName() for i in range(layer.GetLayerDefn().GetFieldCount())] with open("./out/city_roads.csv", "w", newline="", encoding="utf-8") as f: w = csv.writer(f) w.writerow(["gid", "geom_wkt"] + fields) # 几何列单独成列 for i, feat in enumerate(layer, start=1): geom = feat.GetGeometryRef() w.writeheader if False else None # 占位,避免误用 w.writerow([i, geom.ExportToWkt() if geom else ""] + [feat.GetField(n) for n in fields])这段脚本产出的是纯文本,最大的好处是几何不经过任何二进制通道,编码问题和类型兼容问题被彻底绕开。导入端用 COPY 一次推进去,几何列先用 text 类型接,再落成正式几何列:
-- 1) 先建临时表承接文本 CREATE TABLE public.city_roads_stg ( gid integer, geom_wkt text, road_name text, road_level text ); -- 2) COPY 灌文本(服务端路径,注意权限) COPY public.city_roads_stg FROM '/data/out/city_roads.csv' WITH (FORMAT csv, HEADER true, ENCODING 'UTF8'); -- 3) 转正式表:显式 SRID 与几何类型,非法几何单独记下来 CREATE TABLE public.city_roads ( gid integer PRIMARY KEY, road_name text, road_level text, geom geometry(MultiLineString, 4490) ); INSERT INTO public.city_roads (gid, road_name, road_level, geom) SELECT gid, road_name, road_level, ST_GeomFromText(geom_wkt, 4490) FROM public.city_roads_stg WHERE geom_wkt IS NOT NULL AND ST_IsValid(ST_GeomFromText(geom_wkt, 4490));第三步的 WHERE 是刻意留的:非法几何不硬塞,留在临时表里等人处理,比让它们进正式表再靠 ST_IsValid 一条条捞出来省事得多。灌完对一下两边的行数,临时表行数减去正式表行数就是被拦下来的坏几何数量,这个数字应该小且可解释。
3.3 库对库:逻辑导出与自定义函数迁移
库对库最省事的思路是结构改造 + 数据通道分离。整库 dump 直接还原到 KingbaseES,实际做下来成功率不高,原因不在数据本身,而在 dump 文件里带着扩展名、函数签名、系统表引用这些环境相关的东西。
# 只导结构,先看看目标端能接受多少 sys_dump -h 10.0.0.20 -p 5432 -U gis -d gisdb \ --schema-only -f /data/gis_schema.sql # 只导单表数据,格式用 custom,便于按表选择性还原 sys_dump -h 10.0.0.20 -p 5432 -U gis -d gisdb \ -Fc -t public.city_roads -f /data/city_roads.dmp # 目标端按表还原 ksql -h 10.0.0.11 -p 54321 -U system -d gisdb -c "TRUNCATE public.city_roads;" sys_restore -h 10.0.0.11 -p 54321 -U system -d gisdb \ -t city_roads --data-only /data/city_roads.dmp注意这里刻意用了“先建表、再只还原数据”的顺序。结构由你自己在目标端按清单建,几何类型和 SRID 显式写死;dump 只负责数据搬运,不参与结构决策。这样即使两边扩展名不同、函数签名有差异,数据通道照样通。
自定义函数和视图是另一块,必须单独盘。它们往往比表更脆弱,因为依赖的是函数签名而不是数据本身。
-- 列出源端所有非系统函数,逐个确认目标端有没有同名同参数的重载 SELECT n.nspname AS schema_name, p.proname AS func_name, pg_get_function_identity_arguments(p.oid) AS args FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname NOT IN ('pg_catalog', 'information_schema') ORDER BY 1, 2;拿到清单后按三类处理:纯 SQL 且只调用标准 ST_ 函数的,一般能直接用;调用特定版本才有的重载的,要改写成参数更宽松的等价形式;依赖中间件自定义类型的,基本只能重写。视图同理,先 SELECT 一遍看报错信息,报“函数不存在”的按名字去目标端查重载列表,报“类型不存在”的就得改结构了。
3.4 空间索引与统计信息重建
数据全部灌完之后,索引和统计信息必须重建。这一步在很多迁移清单里被放在“可选”,结果是性能问题被留到了上线之后。GiST 索引建得慢,主要吃 maintenance_work_mem,大表上把这个值临时调高,比反复重试划算。
-- 建 GiST 空间索引;大表场景先放开维护内存再建 SET maintenance_work_mem = '2GB'; CREATE INDEX idx_city_roads_geom ON public.city_roads USING gist (geom); -- 属性列的组合查询,按实际 WHERE 条件补 btree CREATE INDEX idx_city_roads_level ON public.city_roads (road_level); -- 统计信息必须跑,否则优化器对空间选择率完全没概念 ANALYZE public.city_roads;GiST 的建索引耗时和数据分布强相关:几何越复杂、重叠越多,越慢。分块迁移的场景下,每块灌完就建索引再合并,比最后一次性建要稳,因为中途失败不用从头再来。
ANALYZE 这一步的意义在于让优化器知道 && 操作符的选择率。刚灌完数据的表如果没有统计信息,优化器可能选全表扫描,你会误以为是索引没建好,其实是它不知道有索引可用。验证方法很简单,EXPLAIN 看一眼有没有走 Index Scan,这件事放在第 6 章细说。
4. 迁完怎么验:行数、SRID、面积三关对齐
验收标准要在迁移开始之前就定下来,不是迁完再想“怎么算对”。我的习惯是三关:行数与几何非空对得上、SRID 分布一致、面积或长度的相对误差在阈值内。三关全过才算迁完,任何一关不过都要回到源端找原因,别在目标端硬改数据迁就结果。
4.1 行数、几何非空与几何类型分布
第一关在目标端跑一条聚合查询,把四个关键指标一次拿出来。
-- 目标端验收第一关:行数、空几何、类型一致性、SRID 种类 SELECT count(*) AS total_rows, count(*) FILTER (WHERE geom IS NULL) AS null_geom, count(*) FILTER (WHERE GeometryType(geom) <> 'MULTILINESTRING') AS type_mismatch, count(DISTINCT ST_SRID(geom)) AS srid_kinds FROM public.city_roads;四个数对应的判断:total_rows 要等于源端行数减去被拦下的非法几何数;null_geom 应该为 0,不为 0 说明导入过程中有空几何混进来;type_mismatch 必须为 0,不为 0 说明几何列类型声明得比数据宽;srid_kinds 必须为 1,大于 1 说明有历史脏数据没清干净。
注意 GeometryType 在不同实现里返回值大小写可能不同,写成大写比较稳妥。这条查询跑得慢是正常的,因为它对所有行做了一次几何类型判定,几十万行以内可以接受,上千万行建议抽样。
4.2 面积长度误差与 SRID 引发的位置偏移
第二关是对几何数值本身。这里有个高频陷阱:4490 是地理坐标系,单位是度,直接 ST_Area 算出来的是“平方度”,没有任何业务意义,而且和源端算出来的数字对不上时会让人误以为是迁移丢精度。
-- 错误示范:地理坐标系上直接算面积,得到平方度 SELECT sum(ST_Area(geom)) FROM public.city_roads; -- 正确做法一:转投影坐标系再算,单位是米 SELECT sum(ST_Area(ST_Transform(geom, 4547))) / 1000000.0 AS area_km2 FROM public.city_roads; -- 正确做法二:geography 直接返回平方米 SELECT sum(ST_Area(geom::geography)) / 1000000.0 AS area_km2 FROM public.city_roads;4547 是与 4490 配套的 3 度带投影之一,具体带号按数据所在经度选,选错带号会导致面积系统性偏差,这个偏差通常表现为“所有面整体缩放了千分之几”,很难从单个要素上看出来。稳妥做法是先用少量要素试算,和源端同参数下的结果比,误差稳定在 0.1% 以内再全表跑。
长度同理,线要素用 ST_Length 在投影坐标下算,或转 geography。误差阈值我一般定在 0.5% 以内:低于这个数属于浮点与投影实现差异,可以接受;高于这个数就一定是坐标系或几何有问题,必须查。
位置偏移是另一类,表现为几何形状没变但整体平移。判断方法很简单:取一个已知地物的坐标,源端和目标端各查一次,差值超过几米就是重投影做错了,常见原因是把“打标签”写成了“做变换”,或者 4490 在目标端字典里缺失后被自动当成 4326 处理。
4.3 把三关写成一条可重复执行的脚本
验收要能反复跑,不能每次手工贴 SQL。下面这段脚本连两个库,按主键抽样比对几何的 WKT 哈希,是最省事的一致性检查方式——几何只要差一个坐标位,哈希就对不上。
# 抽样比对:两边取同一批主键,比较几何 WKT 的 MD5 import hashlib import psycopg2 def geom_digest(conn_info, table, key_col, geom_col, sample_ids): sql = (f"SELECT {key_col}, ST_AsText({geom_col}) " f"FROM {table} WHERE {key_col} = ANY(%s)") out = {} with psycopg2.connect(**conn_info) as conn: with conn.cursor() as cur: cur.execute(sql, (sample_ids,)) for k, wkt in cur.fetchall(): out[k] = hashlib.md5((wkt or "").encode("utf-8")).hexdigest() return out src_ids = list(range(1, 2001)) # 按主键等距抽样,覆盖头中尾 src = geom_digest(SRC, "public.city_roads", "gid", "geom", src_ids) dst = geom_digest(DST, "public.city_roads", "gid", "geom", src_ids) diff = [k for k in src_ids if src.get(k) != dst.get(k)] print(f"抽样 {len(src_ids)} 条,不一致 {len(diff)} 条,样例主键 {diff[:10]}")参数说明:sample_ids 要覆盖头中尾三段,避免只抽到某一批导入的数据;比对用 WKT 文本而不是二进制,是因为二进制在不同实现下的字节序和头部标记可能不同,会造成假报警。不一致条数为 0 说明几何本身一致;不为 0 时先看样例主键落在哪个导入批次,再回那个批次查。
三关都过之后,还有一件事常被漏掉:把验收脚本存进版本库,和建表语句放在一起。下次增量迁移或环境重建时,直接跑一遍就知道新环境对不对,比凭记忆复述“上次好像没问题”可靠得多。
5. 排查清单:GIS 数据迁移里五个高频翻车点
下面五条都是我在实际迁移里遇到过的,按“现象 → 原因 → 解决”写,顺序大致按出现频率排。看的时候可以对着自己的验收结果找对应项,对上了直接按解决那栏做。
5.1 数据正确性类:几何为空、位置偏移、数值对不上
第一条,几何列全空或大量 NULL。现象是行数对得上、属性全在,但 geom 列查出来是空的,ST_IsValid 直接报错。原因通常是源端的几何不是标准 WKB 编码,或者走了某个中间件自定义类型,导入工具读不出来就写了个 NULL 进去,而且不报错。解决是换中间格式:先把几何导成 WKT 文本,再在目标端用 ST_GeomFromText 还原,全流程文本化,任何一步都能肉眼确认。还原时用 WHERE 过滤掉 ST_IsValid 为 false 的行,把它们留在临时表里单独处理。
第二条,整体位置偏移几百米到几公里。现象是所有要素形状正确、相对位置正确,但叠到地图上整体错位。原因基本是两个:一是目标端 spatial_ref_sys 里缺 4490 这类国内常用 SRID,导入工具认不出来就按 4326 处理;二是本来只想打标签,却用成了重投影参数,数据被算了两次变换。解决是先在目标端查字典补记录,再确认导入工具的坐标系参数是“只打标签”还是“做变换”:源数据本身已是目标坐标系时用只打标签,源数据确实是另一套坐标系时才用变换。
第三条,面积或长度统计对不上。现象是几何看着没问题,业务统计结果和源端差百分之几。原因多半是单位问题:在地理坐标系上直接算面积得到的是平方度,或者两边用了不同的投影带。解决是统一在投影坐标系下计算,两边用同一个 EPSG 编码,先拿少量要素试算把误差稳定住,再全表跑。误差阈值定在 0.5% 以内,超过就查坐标系而不是查精度。
5.2 性能与对象依赖类:查询慢、视图函数失效
第四条,查询慢到不可用,EXPLAIN 显示全表扫描。现象是数据量和源端一样,同样的空间范围查询在目标端要几十秒。原因有三个层次:索引压根没建;建了索引但没跑 ANALYZE,优化器不知道索引可用;查询写法把索引废掉了,最典型的是在 WHERE 里对几何列套 ST_Transform,导致 && 操作符用不上索引。解决是按顺序排查:先查 pg_indexes 确认索引存在,再 ANALYZE,最后看执行计划里有没有 Index Scan,没有就改写查询,把变换挪到常量侧而不是列侧。
第五条,视图、物化视图、自定义函数成片失效。现象是数据全对,但业务查询一跑就报“函数不存在”或“类型不匹配”。原因是这些对象依赖的是源端特有的函数重载或自定义类型,环境一换签名就对不上。解决是先把对象清单拉出来,按依赖排序逐个重建,不要试图整库还原。视图和函数的重建顺序建议是:先建基础函数,再建视图,最后建物化视图并刷新。物化视图刷新在大表上很吃资源,安排在业务低峰做。
6. 大表与增量:分块搬运和索引生效验证
上千万行、几百 G 的空间表,一次性灌进去失败重来的成本太高,分块是必须的。分块的切法有讲究:按主键区间切最省事,但空间查询的局部性差;按空间范围切(一个经纬度网格一个块)对空间索引更友好,灌完每块还能立刻验证。我一般用后者,切块脚本按 ST_MakeEnvelope 生成范围,逐块搬运。
# 按经纬度网格切块导出,每块独立成型,失败只影响一块 for lon in 115 116 117 118; do for lat in 38 39 40; do ogr2ogr -f PostgreSQL \ "PG:host=10.0.0.11 port=54321 dbname=gisdb user=system password=***" \ ./data/city_roads.shp \ -nln public.city_roads \ -lco GEOMETRY_NAME=geom -lco SPATIAL_INDEX=NONE \ -append \ -spat $lon $lat $(($lon+1)) $(($lat+1)) \ -progress done done注意这里把 SPATIAL_INDEX 设成了 NONE:块内不建索引,全部搬完统一建一次,避免十几次索引重建的开销。如果数据量到了单次建索引都吃力的程度,就改成“每块建局部索引 + 最后合并”或者干脆分表,具体取决于业务是按什么维度查询的。
搬完之后验证索引是否真的被用上,比验证索引是否存在重要得多。下面这条 EXPLAIN 是每次迁移上线前必跑的:
-- 空间范围查询的执行计划:确认走 Index Scan 而不是 Seq Scan EXPLAIN (ANALYZE, BUFFERS) SELECT gid, road_name FROM public.city_roads WHERE geom && ST_MakeEnvelope(116.30, 39.80, 116.50, 40.00, 4490);看三处:Node 类型是不是 Index Scan 或 Bitmap Index Scan,Buffers 里的 shared read 是不是远小于表的总页数,Actual time 是不是毫秒级。如果显示 Seq Scan 而表很大,先 ANALYZE 再跑一次,还不行就检查 SRID 是否一致——ST_MakeEnvelope 的 SRID 和数据列的 SRID 不一致时,&& 会退化成不可索引的比较。
增量迁移是另一块。我的习惯是给每张表加一个统一的更新时间列,增量按时间水位线拉,水位线存在独立的元数据表里,每次迁移前后各写一次,出问题能精确定位到哪一批。增量批次不要太大,单批控制在几分钟能跑完的量级,失败了重跑成本低。这块没有通用参数,得按业务写入频率试。
最后说个自己的教训。早期做这类迁移,我总想着一次跑通,脚本写得越自动越好,结果第一次出错时连错在哪一步都定位不到——那种感觉就像面对一个黑匣子翻车现场。后来改成每步都落日志、每步都能单独重跑、每步都留一个可核对的中间结果,整体耗时反而短了。空间数据迁移这件事,盘点和验收花的时间从来不是浪费,它们决定了你在出问题时是慌还是稳。希望帮到你。
本文还有配套的精品资源,点击获取