多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

2023全国五级行政区划SQL:12位编码、层级查询与避坑指南

2023全国五级行政区划SQL:12位编码、层级查询与避坑指南 简介全国五级行政区域数据库表及配套SQL文件覆盖省级、地级、县级、乡级、村级五级行政区划名称与区域代码适合需要准确行政区划数据的开发人员、数据分析师及GIS应用构建者解决业务系统中数据时效性与代码对照问题。资源包共3个文件包含2个SQL脚本与1个CSV表格压缩包大小18.8MBSQL脚本分别对应完整数据表与层级关联表便于直接导入MySQL等主流数据库CSV表格可作数据查阅或二次加工。数据更新至2023年3月细分至行政村、社区一级并涵盖民族乡、苏木等特殊区划类型可用于地址匹配、业绩统计、地图可视化等场景也可为省市县乡层级治理提供基准数据。目前已有5013人学习下载数据表结构清晰配合SQL可快速完成建库与初始化。如需原始表格或JSON格式可联系作者获取便于不同环境灵活使用。1. 全国五级行政区划 SQL它解决的不只是“查地址”而是把层级关系一次对齐做业务系统的时候地址类字段是最容易翻车的地方之一。不少项目用省市区手工三级下拉遇到要选到村镇级的需求就只能临时找数据拼结果要么层级不全要么更新滞后代码也散落在各种 SQL 片段里换个环境就跑不起来。这份 2023 年最新版全国五级行政区域数据库表和 SQL 文件补的正是这个短板——它不是一个普通的地名清单而是带 12 位编码、父级关系、五级层级标记的一套完整数据表导入后可以直接当成字典表、校验表甚至作为地址子系统的底座。适合正在做电商物流、政务系统、网格化管理的开发者尤其是被地址数据折腾过一轮、想少踩几个坑的人。2. 五级数据怎么分12 位编码和层级口径先对齐2.1 五级不是“省市区县乡”这么简单而是统计口径的五级很多人在第一次看这张表的时候会问不是只有省、市、县、乡四级吗哪来的第五级这里说的五级是按统计用区划代码的口径划分的第一级省级包括省、自治区、直辖市第二级地市级包括地级市、地区、自治州、盟第三级县区级包括县、区、县级市、旗第四级乡镇街道级包括镇、乡、街道、苏木第五级村居委员会级包括村委会、居委会以及部分类似村级的功能区代码民间习惯里“省市区县”只到第四级第五级日常很少用所以普通项目往往只维护到乡镇。但真做网格化管理、物流末端派送、农村电商的时候缺了第五级订单和区域绑定就会露馅。2023 年更新版的价值正是把这一级也补齐了并且处理了一批近两年撤乡设镇、街道拆分后的代码变动。需要留意的是第五级并不是每个地区都有。像某些功能区、农场、兵团团场代码体系不完全按常规的“乡镇下面挂村居”来走后面避坑章节会专门说。2.2 12 位编码的分段规则每一段对应哪一级这份 SQL 表里的核心字段是 12 位区划代码分段规则和身份证前 6 位有重叠但含义不完全一样。分段如下代码位对应级别说明1-2 位省级表示省、自治区、直辖市3-4 位地市级表示地级市、地区、自治州5-6 位县区级表示区、县、县级市7-9 位乡镇级表示镇、乡、街道10-12 位村居级表示村委会、居委会以一段虚构代码为例比如990102123001拆开就是99某省级行政区01该省下的第一地市02该市下的第一区县123该区县下的一个乡镇或街道001该乡镇下的第一个村或社区前 6 位大体能和身份证号里的地区代码对上但注意这只是“大体”。统计口径的区划代码和公安户籍使用的行政区划代码偶尔有偏差尤其是乡镇以后的部分很多是统计部门为城乡划分单独编码的不能用身份证前 6 位反推村级代码。做数据清洗的时候最忌讳的事就是拿身份证号去推测用户所在村居级代码推出来大概率对不上。另外这份表里除了代码和名称通常还带一个明显的“级别”字段用来快速过滤某一层级的全部记录。查询时优先用级别字段不要在名称里做关键字匹配否则会有大量重名干扰。3. 把表建起来DDL 结构、导入命令和字段设计思路3.1 五级一张表还是五张表为什么选单表加 level 标记拿到这份数据资源以后第一步不是急着导入而是先想清楚表结构。网上流传的做法有好几种有人说省、市、县、乡、村各建一张表每张表外键关联很像标准的关系建模也有人说没必要一张表自关联就够了。我一般选单表加一个 level 字段标记层级。原因是五级行政区划的层级虽然固定但实际业务里经常要处理“这个直辖市的第几级缺失了”“那个省直辖县跨级了”这类情况。单表自关联的好处在于接口层不需要为每一级单独写一套查询逻辑只要传父级代码就能往下钻数据有异常一眼就能从 parent_code 字段看出来。此外多表方案在更新数据时非常痛苦。行政区划每年都有调整单表更新只需要导入新数据多表则要按层级分别更新还要处理外键顺序稍不留神就会因为先插子表后插父表报外键错误。建表语句可以这样写DROP TABLE IF EXISTS region; CREATE TABLE region ( code CHAR(12) NOT NULL COMMENT 12位区划代码, name VARCHAR(100) NOT NULL COMMENT 区划名称, parent_code CHAR(12) DEFAULT NULL COMMENT 父级代码顶级为NULL, level TINYINT NOT NULL COMMENT 1省 2市 3县区 4乡镇街道 5村居委, region_type VARCHAR(20) DEFAULT COMMENT 区/县/镇/乡/街道/社区/村委会等, PRIMARY KEY (code), KEY idx_parent_code (parent_code), KEY idx_level (level) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci COMMENT全国五级行政区划表;这里有几个关键点code用CHAR(12)而不是VARCHAR(12)因为代码长度固定前导 0 很重要。如果用整数类型990010000000这类代码会被去掉前导位父级匹配直接失效。parent_code允许为空顶级省级记录没有父级不要为了结构完整给它塞一个假的“中国”父节点否则查询链路会多出一层无意义的目录。索引只加了parent_code和level。code是主键天然有索引。这样设计覆盖了绝大多数查询场景按父级查子级、按级别过滤、按代码精确匹配。如果业务经常按名称模糊搜索可以再加一个name前缀索引但一般不建议直接对名称建全列索引字符串很长索引膨胀明显。3.2 导入三步走字符集、导入命令、数据校验建好表以后导入这份 SQL 文件有标准流程顺序错了会出乱码或者主键冲突。第一步确认文件字符集。用文本编辑器打开 SQL 文件或者查看文件头确认里面写的是utf8mb4还是gbk。这份资源标注为 2023 年最新版大概率是 UTF-8 格式但稳妥起见还是要确认。检查方法简单file region.sql看到输出里带 UTF-8 字样就可以放心用。如果是 GBK之后需要转码再导入。第二步命令行导入。直接执行mysql -uroot -p --default-character-setutf8mb4 mydb region.sql这里--default-character-setutf8mb4不能省。它指定客户端发送 SQL 语句时使用的字符集不指定的话MySQL 会按系统默认字符集解析含生僻字的村名很容易变成问号。mydb是你自己的目标库名导入前先建好库并且确认库也是 utf8mb4否则表结构里写了 utf8mb4 也会被库的默认字符集覆盖。第三步导入后做条数校验SELECT level, COUNT(*) FROM region GROUP BY level;正常情况是第 1 级数量在 30 到 35 之间第 5 级数量最多通常有几十万条。如果查出来第 5 级一条都没有说明导入的 SQL 不完整或者表的 level 字段映射有问题。不要只看总条数分级统计更可靠。导入过程建议用一个事务包住整份 SQL尤其是更新已有数据的时候。常见做法是在导入前执行START TRANSACTION导入完成后检查数据无误再COMMIT一旦发现数据异常可以直接ROLLBACK不用重新清洗一遍文件。4. 从村到省拉一条路径递归 CTE、五层 JOIN 和级联接口的取舍4.1 固定层级的五层自连接简单直接导入完成之后最常用的需求是给定一个村居代码查出它的完整路径——村、乡镇、县区、地市、省。因为层级固定是五级最直接的方式就是五层自连接。这种写法思路简单适合数据量几十万行的表加上索引后性能并不差SELECT c.name AS village, t.name AS town, d.name AS district, s.name AS city, p.name AS province FROM region c LEFT JOIN region t ON t.code c.parent_code LEFT JOIN region d ON d.code t.parent_code LEFT JOIN region s ON s.code d.parent_code LEFT JOIN region p ON p.code s.parent_code WHERE c.level 5 AND c.code 990102123001;这里用了LEFT JOIN而不是INNER JOIN是有意的。如果某条数据的父级链断了比如某个乡镇的parent_code指向了一个已停用的县用INNER JOIN会把整条记录丢掉只剩 NULL用LEFT JOIN则至少能看到村和乡镇名称查出来哪一级是 NULL顺便就发现了脏数据。参数说明最内层的c表代表第五级t、d、s、p分别是乡镇、县区、地市、省级。条件里的c.code换成实际要查询的村居代码。如果只想查某个乡镇下全部村的路径可以把WHERE c.code 改成WHERE c.parent_code 某乡镇代码。这种写法的缺陷也很明显层级一旦变成六层甚至七层就要继续加 JOIN维护成本上升。但它有一个递归写法比不上的优势——不依赖 MySQL 版本5.7 和 MariaDB 10.1 都能跑生产环境如果还在用老版本优先用它。4.2 用递归 CTE 支持任意深度适合做动态层级接口如果项目用的是 MySQL 8.0 以上版本推荐用递归 CTE 替代多层 JOIN代码更简洁而且天然支持“从任意节点向上回溯”或“向下展开”WITH RECURSIVE region_path (code, name, parent_code, level) AS ( SELECT code, name, parent_code, level FROM region WHERE code 990102123001 UNION ALL SELECT r.code, r.name, r.parent_code, r.level FROM region r INNER JOIN region_path rp ON r.code rp.parent_code ) SELECT * FROM region_path ORDER BY level;递归部分拆开看就是两步第一步查出起始节点的信息第二步用INNER JOIN把当前节点的父级一层层接上来。r.code rp.parent_code这个条件表示向上追溯想要向下钻取把条件反过来写即可。参数说明WHERE code 990102123001是递归的种子节点可以是任意层级的代码。ORDER BY level保证路径从村到省排列也可以改成ORDER BY level DESC得到从省到村的路径。由于递归可能产生重复节点建议在最终结果上加上SELECT DISTINCT防止异常父级链条导致死循环。要注意的是递归 CTE 在 MySQL 8.0 和 MariaDB 10.2 上支持但很多云数据库默认关闭了cte_max_recursion_depth数据没问题也可能会报错。使用前先确认变量状态不然上线之后夜里突然报错处理起来很被动。4.3 级联下拉接口后端怎么设计不卡顿行政区划最常见的应用是省市区乡村级联下拉。前端每选一级就向后端要下一级列表接口实现不能全量返回几十万条记录一次性出完页面直接卡死。推荐接口设计是接收一个parent_code参数返回该节点下所有直接子级。SELECT code, name, level FROM region WHERE parent_code ? ORDER BY code;前端第一级传空或传固定标识后端返回省级选中某个省后把省的code传回来继续查下一级。这个接口用到的就是之前建的idx_parent_code索引单次查询返回几百条记录性能没有任何压力。需要注意的一点是直辖市的区下面没有地市这一层用户选完“北京市-海淀区”之后第三级直接就是乡镇街道。接口层对层级变化要宽容按level字段判断当前到达第几级而不是假定“省下面一定是市”。5. 避坑行政区划数据最常见的五个翻车点5.1 直辖市的层级链缺一级乡镇的父级不是区现象用户选择直辖市的某个街道前端联动的“地市”一栏没有数据或者查询村到省路径时中间有一级显示为 NULL。原因直辖市的行政区划本身就是“省-区-乡镇”结构没有地市级。这份表里直辖市的区县节点其parent_code直接指向省级代码再往下挂乡镇街道层级比普通省份少一级。解决业务代码里不要默认“省下面一定有市”。查询路径时把“某级没有父级”当作正常情况处理不要强行给直辖市的区找一个虚假的地市父节点。前端级联组件也要支持跳级否则用户选完直辖市后下拉框就断档。5.2 省直辖县、兵团和功能区代码不在常规父子链上现象查某个县的parent_code发现它指向的是省而不是地市导致五层 JOIN 出来的城市字段为空。原因全国有一批省直辖县级单位它们不归属任何一个地市行政上直接由省管理。另外兵团、农场、林场等功能区的代码虽然挂在省级节点下面但自身既不是标准镇也不是标准乡region_type字段和普通区划不同。解决先接受数据本身有多样性不要用单一规则去校验所有记录。平时做统计报表时按 “省-省直辖县” 的路径建立一张映射表把这类特殊情况单独维护避免每次查询都走一遍完整五级链路。兵团这类节点建议把parent_code为空的分支抽出来做特殊标记。5.3 代码回收与更名旧业务数据关联不上新表现象业务表里存着 2021 年的乡镇代码2023 年去 join 新导入的行政区划表大量记录匹配不上订单区域的统计数字凭空少了一大截。原因行政区划代码不是永久不变的。撤乡并镇、街道拆分、县改区都会导致代码被回收或重新分配。老代码在 2023 年版表里已经不存在自然关联不上。解决不要用最新表直接覆盖历史上所有业务数据。建议保留三类数据当前最新区划表、历史区划表、区划变更对照表。业务查询先按当前表关联匹配不上的再用历史表兜底并把匹配结果记录到日志里。每次导入新版 SQL 之前先拿旧表和新表做一次 diff生成变更对照再决定是否需要更新业务表里的冗余地区字段。5.4 生僻地名和繁体名乱码导入就错后面全错现象SQL 导入后部分村名显示为问号或乱码比如某个字显示成“锟斤拷”直接把地址文本打废。原因SQL 文件是 UTF-8 编码但 MySQL 客户端连接字符集不是 utf8mb4。导入时--default-character-set没指定或者建库的时候用了latin1让数据在写入中文时发生了不可逆的编码错误。解决导入前检查两处一是文件编码二是建库语句里的DEFAULT CHARSET。两者都确认是 utf8mb4 后再执行导入。如果已经导入到一半发现乱码不要试图用ALTER TABLE改字符集来恢复编码错误是字节级别的问题改字符集救不回来只能清空表重新导入。血泪教训导入后第一件事就是随机查几条含生僻字的记录别等业务跑起来才发现。5.5 重复导入导致主键冲突表清理顺序不能省现象同一份 SQL 文件执行两次第二次导入时报Duplicate entry错误导入中断在中间位置。原因区划代码是主键重复导入时后一批数据与已有数据撞主键。很多开发者以为“再导入一遍会覆盖”实际上默认INSERT遇到主键冲突会直接报错不会自动更新。解决如果要全量刷新先清空表再导入TRUNCATE TABLE region;然后重新执行导入命令。如果只想增量更新建议导入前把 SQL 里的INSERT INTO改成INSERT IGNORE INTO或者在导入前先执行SET FOREIGN_KEY_CHECKS0;不过更稳妥的方案是维护一张快照表把新旧数据做对比只更新有变化的记录。全量清空虽然简单但会把历史数据的血统抹掉线上环境不建议这么干。6. 把区划表当字典和校验工具来用导入完成只是第一步行政区划表真正的价值在于把它当成一套基础字典持续使用。我一般会在项目里做两件事一是地址文本校验二是增量更新对比。地址文本校验用来判断用户填写的收货地址疑似错别字或非法地名。做法是把区划名称加载到内存字典匹配地址字符串。核心逻辑是拆出关键词先在区划表里精确匹配匹配不上的再尝试模糊匹配。from pymysql import connect conn connect(hostlocalhost, userroot, password, dbmydb, charsetutf8mb4) cur conn.cursor() cur.execute(SELECT code, name, level FROM region) region_map {row[1]: row for row in cur.fetchall()} def check_address(address): for name, row in region_map.items(): if name and name in address: return row[0] return None这段代码的作用是把所有区划名称放到内存里逐条检查地址文本中是否包含区划名称。优点是不需要引入外部分词库对五级表这种几万行级别的数据完全够用。缺点是名称可能有重名比如“城关镇”很多省份都有只靠名称反查代码不可靠真实场景里还需要结合地址上下文去消歧。增量更新对比比直接全量覆盖安全得多。思路很简单把上一次导入的数据保留在快照表region_old里用NOT IN查出新增和停用代码再决定业务表怎么处理。-- 查找新增的区划代码 SELECT code, name FROM region WHERE code NOT IN (SELECT code FROM region_old); -- 查找停用的区划代码 SELECT code, name FROM region_old WHERE code NOT IN (SELECT code FROM region);输出结果后把新增代码插入业务映射表把停用代码标记为失效而不是直接删除。用户已经绑定的旧地址宁可显示“已失效”也不要去匹配一个新的同名地区否则历史订单归属会被改动。从那以后我每次接手地址类项目第一件事就是确认行政区划版本和父级链完整率运行时跑一遍完整性检查再动业务表推进度之前先堵坑。这份 2023 年版的全国五级行政区划表解决的不只是拉一个下拉菜单的问题而是让地址字段从“看起来有数据”变成“真正能对上关系”希望能帮到你。本文还有配套的精品资源点击获取
返回列表