简介:面向地域信息开发与数据分析场景的全国五级行政区划数据包,覆盖省、市、区县、乡镇及村委会层级,适合需要地址库、区域筛选和地理维度统计的开发者与数据分析人员使用。压缩包共1个文件,为单个SQL脚本,体积约7.97MB,内含建表语句与近77万条行政区划记录,可在MySQL中直接导入并快速构建区域表。已有1074人学习下载,说明其在地址库搭建、地图服务、天气定位、物流配送和电商本地化等场景具有一定参考价值。导入后可获得省市区县乡镇村的层级字段与完整记录,便于做级联选择、区域汇总、路径规划和统计报表;结合人口经济等外部指标还能扩展多维分析,并可按行政区划调整定期维护更新。
1. 77万条行政区划数据拿到手先别入库:五级链路和「六级地区数据」的错位
做地址库、CRM 区域字段、物流分单的人,搜「六级地区数据」时十有八九会落到这份全国5级行政区划数据上:省、市、区县、乡镇、村委会,总量接近 77 万条。它最适合解决三类事:地址下拉联动、按乡镇/村居做覆盖统计、历史订单地址回填。不适合直接当法定区划权威库用,因为区划调整有滞后,村居级编码在不同数据源里长度也不完全统一。很多人以为拿到的是六级,实际落库是五级,第六级通常是村居下的网格或组,得看数据源有没有带。先想清楚这一点,再谈建表和导入,后面能少返工。
2. 拆表结构:省市区县乡镇村委会怎么落成一张节点表和一张路径表
2.1 字段先定死:编码、名称、层级、父级、全路径
这份数据最怕上来就建一张大宽表,后面查联动和做搜索都别扭。我一般会先落一张节点表,字段控制在十个以内:code、name、level、parent_code、full_name、urban_rural_code、version。level用 1 到 5 表示省、市、区县、乡镇、村居,不要用中文层级名当关联键,后面排序和过滤都麻烦。code一律按字符串处理,数据库字段用TEXT或VARCHAR,不要用整型,前导零丢一次就够你查半天。parent_code存直接上级编码,省级留空。full_name是物化路径,比如「某省/A市/B县/C镇/D村」,展示和搜索都用它,但不要拿它做外键。
节点表的主键可以用自增id,业务唯一键用(code, level)联合唯一索引。原因很实际:村居级编码在部分数据源里存在跨层级复用或补齐长度的情况,单靠code唯一约束会误杀。索引先建三个:parent_code用于联动查询,level用于按层级统计,name用于前缀搜索。别一上来给name建全文索引,77 万条数据量不大,但索引维护成本要先算清楚。
2.2 宽表和树表怎么选:查询场景决定主键与索引
树表适合「按上级查下级」和「逐级联动」,宽表适合「导出报表」和「地址补全」。我的做法是两张都留:region_node做树表,region_path做宽表,宽表由树表递归生成,只跑一次,不参与写入。宽表字段包括province_code、city_code、county_code、town_code、village_code,再加一个full_name。这样前端要三级联动时查树表,后台要按省汇总时查宽表,不用在接口里反复递归。
提示:宽表不要手工维护,每次数据版本更新后整表重建,避免中间层级改名导致路径不一致。
2.3 从原始文件到可查询表的导入步骤
原始文件常见是 CSV 或 Excel,表头可能是中文,也可能带合并单元格。第一步不是写 INSERT,而是把编码列强制读成字符串,统一去空格,再看层级分布。下面这段用 pandas 和 SQLite 做本地落库,换成 MySQL 或 PostgreSQL 时只需要改连接和建表语句。
import pandas as pd import sqlite3 # 编码、父级编码全部按字符串读,防止前导零被吃掉 dtype = { "code": str, "parent_code": str, "name": str, "level": str, "urban_rural_code": str, } df = pd.read_csv("regions.csv", dtype=dtype, keep_default_na=False) # 去空格、去不可见字符,层级转整数 for col in ["code", "parent_code", "name"]: df[col] = df[col].str.strip() df["level"] = df["level"].astype(int) df["urban_rural_code"] = df["urban_rural_code"].str.strip() # 省级父级统一置空,避免原始文件用 0 或空字符串混用 df.loc[df["level"] == 1, "parent_code"] = "" conn = sqlite3.connect("region.db") conn.execute(""" CREATE TABLE IF NOT EXISTS region_node ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT NOT NULL, name TEXT NOT NULL, level INTEGER NOT NULL, parent_code TEXT, full_name TEXT, urban_rural_code TEXT, version TEXT, UNIQUE(code, level) ); CREATE INDEX IF NOT EXISTS idx_region_parent ON region_node(parent_code); CREATE INDEX IF NOT EXISTS idx_region_name ON region_node(name); """) # 先删旧版本再灌,避免重复导入触发唯一约束 conn.execute("DELETE FROM region_node WHERE version = ?", ("v1",)) df["version"] = "v1" df.to_sql("region_node", conn, if_exists="append", index=False) conn.commit() conn.close()这段代码的关键参数只有三个:dtype强制字符串、keep_default_na=False防止空名称变成NaN、level转整数用于排序。to_sql的if_exists="append"配合前面的DELETE做版本替换,不要用replace,否则索引会一起丢掉。导入后先跑一句SELECT level, COUNT(*) FROM region_node GROUP BY level ORDER BY level;,看五级数量级是否合理:省级几十、市级几百、区县级几千、乡镇级几万、村居级几十万,总量接近 77 万。如果某一级数量明显偏少,先别继续,回第 5 章看断链问题。
3. 编码规则与校验:行政区划代码、统计用区划代码、村委会编码不是一回事
3.1 12位、9位、6位编码的层级含义
这份数据里最容易混的是三套编码:行政区划代码、统计用区划代码、村居级扩展码。常见做法是省级 2 位、市级 4 位、县级 6 位、乡镇级 9 位、村居级 12 位,但村居级有的数据源会带 15 位扩展码,把城乡分类或组码拼在后面。你不要一上来写死「村居必须 12 位」,先跑长度分布。下面这条 SQL 能看到每个层级下编码长度的分布,跑完再决定校验规则。
SELECT level, LENGTH(code) AS code_len, COUNT(*) AS cnt FROM region_node GROUP BY level, LENGTH(code) ORDER BY level, code_len;如果村居级同时出现 12 位和 15 位,不要急着判脏数据,先看 15 位是不是带了组码或城乡分类。处理方式有两种:要么保留原码,额外加base_code存前 12 位;要么统一截断到 12 位,但截断前要确认后 3 位不是有效层级。我一般选前者,后悔药留足。
3.2 校验脚本:长度、父子前缀、重复、空名称
校验顺序有讲究:先看长度分布,再看重复,再看断链,最后看前缀。反过来做会白跑很多行。下面这段 Python 直接读 CSV 做离线校验,不依赖数据库,适合拿到数据包先跑一遍。
import pandas as pd df = pd.read_csv("regions.csv", dtype=str, keep_default_na=False) df["code"] = df["code"].str.strip() df["parent_code"] = df["parent_code"].str.strip() df["name"] = df["name"].str.strip() df["level"] = df["level"].astype(int) # 1. 编码长度分布:先看数据源编码体系 print(df.groupby("level")["code"].apply(lambda s: s.str.len().value_counts())) # 2. 业务唯一键重复:code + level dup = df[df.duplicated(subset=["code", "level"], keep=False)] print("重复编码条数:", len(dup)) # 3. 空名称和空父级 print("空名称:", (df["name"] == "").sum()) print("非省级空父级:", ((df["level"] > 1) & (df["parent_code"] == "")).sum()) # 4. 断链检查:子级找不到父级 child = df[df["level"] > 1].copy() parent = df[df["level"] > 0][["code", "level"]].rename( columns={"code": "parent_code", "level": "parent_level"} ) merged = child.merge(parent, on="parent_code", how="left") missing = merged[merged["parent_level"].isna()] print("断链节点数:", len(missing)) # 5. 前缀检查:只对父级存在且编码为纯数字的行做 merged = merged.dropna(subset=["parent_level"]).copy() merged["prefix_ok"] = merged.apply( lambda r: r["code"].startswith(r["parent_code"]) if r["code"].isdigit() else True, axis=1, ) print("前缀不匹配:", (~merged["prefix_ok"]).sum())这段脚本里parent取的是level > 0的全部节点,不是只取上一级,因为省直辖县、直筒子市这类特殊体制会让父级跨级。断链结果不要只看总数,抽前 50 条看名称和层级,判断是数据缺失还是父级编码格式不一致。前缀检查用startswith对 77 万行会慢,生产环境建议改成向量化写法或直接下推到 SQL。参数上keep_default_na=False很重要,否则空名称会变成NaN,后面字符串比较全是坑。
3.3 用 SQL 自连接检查断链和孤儿节点
入库之后再用 SQL 复核一遍,重点看「子级有、父级无」的孤儿节点。下面这条 SQL 加了p.level = c.level - 1,防止跨级误匹配;LIMIT 50是先看样本,不要一上来全量导出。
SELECT c.code, c.name, c.level, c.parent_code FROM region_node c LEFT JOIN region_node p ON c.parent_code = p.code AND p.level = c.level - 1 WHERE c.level > 1 AND p.id IS NULL LIMIT 50;如果断链集中在乡镇级和村居级,常见原因是原始文件把省直辖县挂在了省级,或者直筒子市没有区县级。这种不是脏数据,是行政体制差异。处理方式不是硬补一个假父级,而是在查询层做兼容:父级为空且层级大于 1 的节点,单独归到「省直辖」虚拟分组里展示,编码仍保留原值。这样既不影响统计,也不会把树结构改乱。
4. 查询与接口落地:从「省市区三级联动」到「村级模糊搜索」的四种写法
4.1 三级联动:按父级编码查子级
三级联动是最常见的接口,输入父级编码,返回子级列表。不要在接口里递归查整棵树,按parent_code查一层就够。返回字段带has_children,前端用来决定是否显示下一级箭头。下面这条 SQL 用EXISTS判断有没有子级,比COUNT(*)轻。
SELECT r.code, r.name, r.level, EXISTS ( SELECT 1 FROM region_node c WHERE c.parent_code = r.code ) AS has_children FROM region_node r WHERE r.parent_code = :parent_code ORDER BY r.code LIMIT 500;参数:parent_code传上级编码,省级传空字符串或单独走level = 1的查询。LIMIT 500是防御性写法,正常一个市下面的区县不会超过几百,但如果父级编码传错导致命中省级,没有限制会一次吐太多。ORDER BY code比按名称排序稳定,编码本身带层级顺序。
4.2 村级搜索:前缀匹配与全文检索的取舍
村居级搜索是 77 万条数据里最容易翻车的地方。用户输入「D村」,你用name LIKE '%D村%',前置通配符直接全表扫,数据量上来后三秒起步。常见做法是先做前缀匹配name LIKE 'D村%',能走name索引;如果业务一定要中间匹配,再上 FTS5 的 trigram 分词,别在 MySQL 里硬扛。
-- 前缀搜索:能走 name 索引,适合大多数村居输入 SELECT code, name, full_name FROM region_node WHERE level = 5 AND name LIKE :kw || '%' ORDER BY name LIMIT 20;参数:kw传用户输入,拼接'%'放在后面,不要放前面。如果确实需要「包含」匹配,SQLite 可以建 FTS5 虚拟表,用trigram分词对中文子串更稳:
CREATE VIRTUAL TABLE region_fts USING fts5( name, full_name, code UNINDEXED, tokenize='trigram' ); INSERT INTO region_fts(name, full_name, code) SELECT name, full_name, code FROM region_node WHERE level = 5; SELECT code, name, full_name FROM region_fts WHERE region_fts MATCH :kw LIMIT 20;code UNINDEXED是避免编码被分词,trigram适合中文子串搜索,但索引体积会比普通索引大,先评估磁盘再上。换到其他数据库时,思路一样:前缀匹配优先,全文检索兜底,别用%kw%硬扫。
4.3 缓存与分页:77万条不要一次性塞给前端
三级联动的数据量小,省级几十、市级几百、区县级几千,可以全量缓存到 Redis,按parent_code做 key。村居级不要全量缓存,77 万条序列化后体积不小,缓存热点关键词就够了。分页不要用大偏移量OFFSET,翻到后面会越来越慢,用编码游标:
SELECT code, name, full_name FROM region_node WHERE level = 5 AND code > :last_code ORDER BY code LIMIT 50;参数:last_code传上一页最后一条的编码,第一页传空字符串。这样每页都走索引范围扫描,不会随着页码变深而变慢。前端如果要做「上一页」,把当前页第一条编码传回来做反向查询即可,不要在服务端存会话。
5. 避坑与排查:77万条行政区划数据最常见的五个翻车点
5.1 导入阶段的三个坑
第一个坑是编码前导零丢失。现象是导入后某些区县编码从 6 位变成 5 位,和父级编码对不上。原因多半是 Excel 把编码列识别成数字,或者 pandas 默认推断类型时把010000读成了10000。解决方式很直接:原始文件另存为 CSV,读取时dtype=str,数据库字段用TEXT,导入后跑一次长度分布,看到某层级出现异常长度就停下来查源文件。
第二个坑是乡镇数量和预期对不上。现象是某个省下面乡镇级少了十几个,村居级大片断链。原因通常不是数据缺失,而是省直辖县、直筒子市的父级编码为空或挂在省级,你用固定「市查区县、区县查乡镇」的层级去推,自然推不出来。解决方式是先跑第 3 章的断链 SQL,把parent_code为空的非省级节点单独列出来,查询层做兼容分组,不要硬补假父级。
第三个坑是同名村居导致关联重复。现象是按名称关联两张表,结果出现笛卡尔积,一个「D村」匹配出几十条。原因是村居重名非常普遍,光靠名称做外键一定出事。解决方式是所有关联一律用code,展示用full_name,搜索接口返回full_name而不是裸名称。历史数据里如果只存了名称,回填时也要用「省+市+区县+乡镇+村居」做联合匹配,匹配不上的挂人工,不要批量猜。
5.2 查询与关联的两个坑
第四个坑是用名称做外键。现象是区划调整后,历史订单的「B县」关联不到新数据,或者关联到了同名的另一个县。原因是改名、撤并、重名三件事同时存在。解决方式是业务表存编码不存名称,展示时再 join 节点表取当前名称;如果业务要求保留历史名称,就在订单表里存一份地址快照,不要指望区划表永远不变。
第五个坑是村级搜索用了前置通配符。现象是搜索框输入两个字,接口响应三秒以上,数据库 CPU 飙高。原因是LIKE '%kw%'无法走索引,77 万条全表扫。解决方式是改成前缀匹配LIKE 'kw%',或者上 FTS5 trigram;如果产品一定要中间匹配,就在前端加「至少输入两个字符」的限制,后端加LIMIT和超时,别让一个搜索框拖垮整个库。
6. 把77万条压到可维护:版本快照、增量比对和上线前自检
这份数据不是导入一次就完事,区划调整每年都有,村居级撤并更频繁。我的习惯是每次更新都保留两个版本表:region_node当前版、region_node_old上一版,用version字段区分也行,但比对时用两张表更直观。上线前先跑增量比对,看新增、删除、改名三类变化,再决定是全量替换还是打补丁。
-- 新增节点:新版本有、旧版本无 SELECT n.code, n.name, n.level FROM region_node n LEFT JOIN region_node_old o ON n.code = o.code AND n.level = o.level WHERE o.id IS NULL; -- 改名节点:编码相同但名称变化 SELECT n.code, o.name AS old_name, n.name AS new_name FROM region_node n JOIN region_node_old o ON n.code = o.code AND n.level = o.level WHERE n.name <> o.name;比对前先把两边的code都转成字符串,别一边整型一边文本,否则新增结果会多出一大片假阳性。改名结果不要直接覆盖历史订单,先导出一份人工确认,尤其是区县和乡镇级改名,往往伴随管辖范围调整。
上线前自检我一般固定看六个数:总行数、五级分布、断链数、重复编码数、空名称数、编码长度异常数。下面这张表贴在发布单里,每次更新填一遍,比拍脑袋可靠。
| 检查项 | 预期 | 异常处理 |
|---|---|---|
| 总行数 | 接近 77 万 | 偏差超过 5% 先查源文件 |
| 五级分布 | 省级几十、市级几百、区县级几千、乡镇级几万、村居级几十万 | 某一级骤降查导入截断 |
| 断链数 | 非省级节点为 0 或集中在特殊体制 | 逐条看父级编码 |
| 重复编码 | (code, level)无重复 | 去重前先看是否跨版本 |
| 空名称 | 0 | 回源文件补 |
| 编码长度异常 | 各层级长度分布集中 | 查前导零和扩展码 |
从那以后我每次拿到区划数据,都先跑一遍长度分布和断链检查,再谈导入和接口,哪怕数据包看起来再干净也不跳过。希望帮到你。
本文还有配套的精品资源,点击获取