简介:MySQL省市区表完整版文件是一份面向Web开发、后台管理系统与地理信息相关项目开发者的基础数据资源,可用于快速补齐中国省、市、区县行政区划数据,减少手工整理成本。压缩包内共1个文件,为sql脚本,包体约126KB,导入MySQL后即可生成省市区表并填充记录,字段通常覆盖行政名称、区号、拼音及经纬度等,便于地址选择、区域检索、拼音匹配和地图定位等场景调用。该资源已有2939人学习或下载,说明其在同类基础数据中具有一定参考价值。读者可获得较完整的行政区划表结构与数据脚本,用于初始化数据库、做地址级联查询、按拼音或坐标检索,也能作为开发测试环境中的区域基础数据,帮助提升应用的功能完整性与数据录入效率。
1. 省市区数据不是「一张表」那么简单:先看清这份 MySQL 资源能解决什么
做过电商收货地址、门店配送范围、发票抬头、实名认证属地校验的人,多半都在省市区数据上栽过跟头。看起来只是「省、市、区」三级联动,真做起来才发现:直辖市怎么分层、地级市下面挂着县级市、某些区县改名后新旧编码并存、还有开发区这种「非标准行政区」要不要收录。我见过最典型的翻车,是前端三级下拉用得很顺,结果订单一导出,同一个城市出现了三种拼写,财务对账时对着 Excel 干瞪眼。
这份 MySQL 省市区表完整版文件,解决的就是这个最底层也最容易被低估的问题:提供一份可直接导入 MySQL 的行政区划基础数据,字段结构清晰,省市区层级关系明确,附带编码信息,能直接支撑地址选择、区域统计、配送范围匹配、数据看板下钻这类需求。它适合三类人:正在搭电商或 SaaS 系统、需要一份能长期维护的地址字典的后端;手里有一堆脏地址、需要做清洗和归属归并的数据同学;以及做 BI 报表、动辄要按大区省份聚合的分析岗。
需要提前说清楚边界:省市区数据不是一劳永逸的静态资产,行政区划本身会调整,街道乡镇级别的变动更频繁。这份资源给的是省市区三级这一层相对稳定的骨架,够用、好用,但它不会替你做业务侧的地址标准化,那部分要在应用层自己兜住。下面从表结构设计讲到导入验证,再讲几个真会让人半夜爬起来改数据的坑。
2. 表结构与字段设计:为什么省市区不建议一张表拍平
拿到一份省市区 SQL 文件,第一反应往往是「打开看看、导进去、写个 select」。但如果字段设计没吃透,后面无论做级联查询还是做归属统计都会别扭。这份数据的价值不只在数据本身,还在于它的组织方式贴合了行政区划的树形本质。
2.1 自关联单表与三表拆分的选型
省市区数据的存储有两派做法,我在不同项目里都用过,结论是:中小体量业务用自关联单表,重统计场景才考虑三表拆分或宽表冗余。
自关联单表的结构大致是这样:一张region表,每条记录代表一个行政单位,通过parent_id指向父级,通过level或type标记它是省、市还是区县。这种设计的优势很直接——层级扩展成本低,将来如果要加「街道/乡镇」第四级,不需要新建表,只加数据不改结构;查询联动也简单,查某个省下面的市,就是where parent_id = ?,一条 SQL 搞定。
三表拆分(province、city、district)的好处是每张表字段少、语义直白,新人接手一眼就懂,某些老系统还喜欢在 city 表里冗余 province_id 方便联查。代价是新增层级要动表结构,跨级查询要 join 两次,数据一致性维护也更麻烦——改个名字得同步三张表。
我一般的判断标准是:如果你的业务只需要「省市区三级下拉 + 存一个编码」,自关联单表足够了;如果你要做「按大区聚合销售额」「按城市等级分组」这类分析,可以在单表基础上加一张维度宽表做冗余,而不是把主数据拆散。
| 设计方式 | 适用场景 | 主要代价 |
|---|---|---|
| 自关联单表 | 地址选择、级联下拉、编码存储 | 递归查询需要应用层拼装 |
| 三表拆分 | 结构简单、团队熟悉、字段差异大 | 扩展层级要改表、联查多 |
| 单表 + 宽表冗余 | 报表聚合、多维分析 | 冗余数据需同步机制 |
2.2 关键字段的语义与索引
不管哪种设计,几个字段的语义必须先对齐,否则导入之后全是暗坑。常见字段包括:主键 id、行政区划编码 code、名称 name、父级 parent_id、层级 level、排序 sort,有的还会带拼音或首字母用于搜索。
行政区划编码是这套数据里最有价值的字段,它天然携带层级信息,国家标准编码一般是 6 位,前两位代表省级、中间两位代表市级、后两位代表区县。这意味着你可以不依赖 parent_id,仅凭编码前缀就能判断归属,做数据校验时非常有用——比如检查一条区县记录的编码前四位,是否真能对上它声称的父级市。
索引方面,parent_id必须有索引,否则每次下拉联动都全表扫;code建议加唯一索引,防止重复导入;如果要做名称模糊搜索,name上可以加普通索引,但更推荐用前缀匹配或者干脆上全文检索,like '%xx%'在大表上基本等于自废武功。
提示:编码的唯一索引不只是防重复,它还是你做增量更新时的匹配键。有了它,新版本数据导入时才能用
on duplicate key update做幂等覆盖,而不是先清表再插入。
2.3 建表语句与导入前的结构确认
下面这段建表语句是自关联单表的典型形态,字段命名和类型可以直接照抄,注意code用char(12)而不是int,因为编码存在前导零,用整型会把010000变成10000,这是个经典血泪教训。
CREATE TABLE `region` ( `id` int unsigned NOT NULL AUTO_INCREMENT COMMENT '自增主键', `code` char(12) NOT NULL COMMENT '行政区划编码,保留前导零', `name` varchar(64) NOT NULL COMMENT '行政区划名称', `short_name` varchar(32) DEFAULT NULL COMMENT '简称,如“京”“沪”', `parent_id` int unsigned NOT NULL DEFAULT '0' COMMENT '父级ID,省级为0', `level` tinyint unsigned NOT NULL COMMENT '层级:1省 2市 3区县', `sort` int NOT NULL DEFAULT '0' COMMENT '同级排序值', `status` tinyint NOT NULL DEFAULT '1' COMMENT '状态:1有效 0停用', PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_parent` (`parent_id`), KEY `idx_level` (`level`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='省市区行政区划表';这段语句里几个点值得单独说。code用char(12)而不是 6 位,是给未来留余地——有些统计口径会用 9 位或 12 位的扩展编码,字段留长不吃亏,MySQL 的 char 是定长存储,比 varchar 少一层长度前缀开销。parent_id默认 0 表示省级,这样查询省级列表就是where parent_id = 0,比where level = 1语义更明确,也便于将来挂虚拟根节点。
status字段是很多人会漏掉的一环。行政区划调整时,被撤销的区县不应该直接从表里删掉——历史订单里还存着它的编码呢,删了就对不上。正确做法是保留记录、置status = 0,前端下拉过滤掉,历史数据照常能 join 出名称。
导入之前,先把文件结构过一遍,确认是「建表 + 批量 insert」还是纯数据文件。常见做法是先建表再导数据,中间用事务包一层,失败就整体回滚,避免导一半留下半张脏表:
# 先建库建表 mysql -u root -p your_db < schema.sql # 再导数据,用单事务减少提交开销 mysql -u root -p --default-character-set=utf8mb4 your_db < region_data.sql参数上要注意--default-character-set=utf8mb4,如果源文件里带生僻字或少数民族地区名称,字符集不对就会出现乱码或导入中断,这个错误报得还很隐晦,往往只提示某行语法错误,实际是编码问题。
3. 级联查询与地址标准化:把数据真正用起来的几条 SQL
数据导进去只是开始,能不能撑住业务才是关键。省市区数据的高频用法集中在三块:级联下拉、编码反查、地址归属归并。这三块写不顺,说明表结构或索引有问题,早发现早改。
3.1 三级联动的查询写法与 N+1 陷阱
最朴素的做法是每选一级发一次请求,前端点省份 → 查市 → 点市 → 查区县。功能上没问题,但如果你的页面一加载就要渲染多个地址表单(比如批量导入地址、多收货人管理),就会变成几十次小查询,这就是典型的 N+1。
改进思路有两条。一是一次性懒加载整棵树:省市区总量在几千条量级,一个接口返回全部数据,前端在内存里做级联过滤,用户体验最顺滑,代价是首次加载稍慢,但对现代网络来说可接受。二是按层预取:进页面时只取省列表,同时用一条 SQL 把每个省下的市数量带上,用户展开时再按需加载。
按层查询的写法很直接:
-- 查所有省级 SELECT id, code, name FROM region WHERE parent_id = 0 AND status = 1 ORDER BY sort, id; -- 查某省下的所有市 SELECT id, code, name FROM region WHERE parent_id = ? AND status = 1 ORDER BY sort, id; -- 一次查出多个省的市,避免循环单查 SELECT id, code, name, parent_id FROM region WHERE parent_id IN (?, ?, ?) AND status = 1 ORDER BY parent_id, sort, id;第三条是关键优化点。很多人写循环,for一遍省列表逐个查市,数据库连接池直接被小查询打满。改成IN批量查之后,在应用层按parent_id分组塞回各个省下面,一次往返解决。参数说明:IN里的占位符数量要控制,一次几十个没问题,上千个就要考虑分批,MySQL 对超长IN列表的解析成本会明显上升。
ORDER BY sort, id这个细节别省。行政区划的展示顺序有讲究,直辖市、省会往往要排在前面,sort字段就是干这个的;没有sort值时用id兜底保证顺序稳定,否则同样的查询两次可能出不同顺序,前端渲染会闪。
3.2 编码前缀反查归属:不依赖 parent_id 的校验手段
前面提到编码自带层级,这在数据校验和脏数据修复时特别有用。假设你手里有一批历史订单,只存了区县编码,要反推它属于哪个市、哪个省,最稳的做法就是按编码前缀匹配,而不是依赖可能已经错乱的parent_id。
-- 已知区县编码 110105,反查所属市和省份 SELECT d.name AS district_name, c.name AS city_name, p.name AS province_name FROM region d LEFT JOIN region c ON c.code = CONCAT(LEFT(d.code, 4), '00') LEFT JOIN region p ON p.code = CONCAT(LEFT(d.code, 2), '0000') WHERE d.code = '110105';这段 SQL 的逻辑是纯字符串截取:区县编码前 4 位补00得到市级编码,前 2 位补0000得到省级编码,然后直接按编码 join 回本表。好处是完全不依赖parent_id是否正确,只要编码规范就能查出来;坏处是要求编码严格遵循 6 位国标结构,遇到直辖市这种「省和市编码特殊处理」的情况要单独验证。
参数上提醒一点:LEFT()和CONCAT()用在 join 条件里会导致索引失效,数据量小无所谓,如果真的到了几十万行,建议在导入阶段就把province_code、city_code冗余成独立字段,用的时候直接等值匹配。
注意:直辖市、特别行政区的编码结构有特殊约定,部分数据源里直辖市的市级记录可能缺失或编码不等长。用前缀法之前,先跑一遍抽样校验,看省级记录数、市级记录数是否符合预期,别直接上生产。
3.3 地址文本归并与模糊匹配的边界
业务里更常见的不是编码,而是一段自由文本:「北京市朝阳区某某路」「广东深圳南山区」。要把这类文本归到标准区划上,靠 SQL 的like顶一阵子,但很快就会撞墙——「北京」和「北京市」、「内蒙」和「内蒙古」、简称全称混用,匹配结果时好时坏。
我的常用套路是三级回退匹配:先用完整名称精确匹配,命中就结束;没命中就去掉「省/市/区/县」后缀再匹配一次;还不行就用编码前缀或拼音首字母辅助。伪代码大致是:
import re SUFFIX = re.compile(r'(省|市|区|县|自治州|地区|盟)$') def normalize_region_name(raw: str) -> str: """去掉行政区划后缀,便于宽松匹配""" return SUFFIX.sub('', raw.strip()) def match_region(raw: str, cursor) -> dict | None: name = raw.strip() # 第一级:精确匹配 cursor.execute( "SELECT id, code, name, level FROM region WHERE name = %s AND status = 1", (name,)) row = cursor.fetchone() if row: return row # 第二级:去后缀后匹配,限定在市级及以上,降低误伤 stripped = normalize_region_name(name) cursor.execute( "SELECT id, code, name, level FROM region " "WHERE name = %s AND level <= 2 AND status = 1", (stripped,)) return cursor.fetchone()这里的关键不是代码多漂亮,而是把匹配策略分层,并且限定每层的可信范围。第二级限定level <= 2,是因为去掉后缀后「朝阳」既是区也是别的地名,不限定层级容易匹配到错误记录。实际生产里我还会给匹配结果打一个置信度标记,精确匹配记高分,宽松匹配记低分,低分的进人工复核队列,而不是直接写库。
这套逻辑落地之后,清洗十万级地址数据大概能做到八成以上自动归并,剩下的长尾要么是错别字,要么是「某某开发区」这类非标准行政区,确实得人工兜。
4. 增量维护与数据校验:省市区表长期不腐的关键动作
省市区数据最容易出问题的时候,不是第一次导入,而是半年后的一次区划调整。某个县撤县设区,编码变了、名称变了,而你的表还停在旧版本上,前端选不到、订单对不上,问题才暴露出来。所以这一节讲的不是导入,是让它活得久。
4.1 用编码做幂等增量更新
核心思路一句话:永远以 code 为匹配键,做 upsert,不要清表重导。清表重导看着干净,但会破坏id的稳定性——如果业务表里引用的是 region 的id而不是code,一次重导所有外键全错位,这是真正的灾难。
安全的增量更新写法:
INSERT INTO region (code, name, short_name, parent_id, level, sort, status) VALUES ('110105', '朝阳区', '朝阳', 1101, 3, 10, 1), ('110106', '丰台区', '丰台', 1101, 3, 20, 1) ON DUPLICATE KEY UPDATE name = VALUES(name), short_name = VALUES(short_name), parent_id = VALUES(parent_id), level = VALUES(level), sort = VALUES(sort), status = VALUES(status);依赖uk_code唯一索引,已存在就更新、不存在就插入,天然幂等,跑多少遍结果都一样。VALUES()函数在 MySQL 8.0.20 之后被标记为废弃,新版本可以改成别名写法AS new ON DUPLICATE KEY UPDATE name = new.name,如果目标库版本不确定,用VALUES()兼容性更好。
被撤销的区划怎么处理?我一般做「差集检测」:把官方最新数据的 code 集合和库里的 code 集合求差,库里多出来的那些,说明已被撤并,批量置status = 0,而不是删除。这个检测用一条 SQL 或者导到临时表里比对都行,关键是流程固定下来,每次更新必跑。
4.2 三条自检 SQL 兜住结构性问题
数据导完了、更新跑完了,还得有校验。下面三条 SQL 是我每次更新后必跑的,覆盖面不广但极准。
-- 1. 检查孤儿子节点:父级不存在的记录 SELECT c.id, c.code, c.name, c.parent_id FROM region c LEFT JOIN region p ON p.id = c.parent_id WHERE c.parent_id <> 0 AND p.id IS NULL; -- 2. 检查编码与父级是否自洽(省级跳过) SELECT id, code, name, parent_id FROM region WHERE level = 3 AND LEFT(code, 4) <> (SELECT LEFT(code, 4) FROM region p WHERE p.id = region.parent_id); -- 3. 检查同级重名,识别可能的重复录入 SELECT parent_id, name, COUNT(*) AS cnt FROM region WHERE status = 1 GROUP BY parent_id, name HAVING cnt > 1;第一条查孤儿记录,父级被误删或者 parent_id 填错都会被抓出来。第二条是编码自洽校验,区县记录的前 4 位编码必须和它父级市的前 4 位一致,不一致说明数据源本身有矛盾或者导入时串了。第三条查同级重名,正常情况同一个市下面不该有两个同名区县,出现多条说明重复导入了。
这三条跑下来没结果,基本可以放心上线;有结果就逐条排查,别抱着「应该不影响」的心态放过去。血泪经验是,孤儿记录一旦进了生产,前端下拉会出现「选了市但市下面是空的」,排查起来很费劲。
4.3 变更留痕与回滚方案
最后一个容易被忽视的点:更新前留底。省市区表不大,几千到几万行,每次更新前CREATE TABLE region_bak_20240101 AS SELECT * FROM region,成本极低,但真出事时是唯一的后悔药。
更进一步,可以建一张region_change_log,记录每次更新的 code、旧值、新值、操作时间。表结构调整、区县更名这类变更,业务侧经常要追溯「这个地址为什么变了」,有日志就能给出明确答复,没有就只能靠猜。
提示:留底表命名带上日期,并定期清理,避免几十张备份表堆在库里。一般保留最近 3 到 5 次即可,更早的归档到备份文件。
5. 常见问题排查:五条真会绊住人的坑
5.1 导入后中文全是问号或乱码
现象:数据导入成功,但select出来名称全是???或者方块乱码。
原因:字符集链路不一致。可能是源 SQL 文件编码是 GBK,也可能是客户端连接默认字符集不是 utf8mb4,还可能是建表时表字符集写了 utf8(MySQL 的 utf8 只支持 3 字节,存不下部分生僻字和 emoji)。
解决:三步对齐。建表时显式写CHARSET=utf8mb4;导入命令加--default-character-set=utf8mb4;连接串里确认characterEncoding=utf8(Java 驱动会自动映射到 utf8mb4)。已经导坏的,先用ALTER TABLE region CONVERT TO CHARACTER SET utf8mb4修表,再重新导入数据。
5.2 前导零丢失导致编码对不上
现象:编码010000存进去变成了10000,后续按编码前缀 join 全部失配。
原因:建表时code字段用了int或bigint,整型不保留前导零。
解决:字段类型改成char(6)或char(12),然后重新导入。如果表里已有大量数据,先用LPAD(code, 6, '0')补齐再改类型。这个坑几乎每个用整型存编码的团队都踩过一次,属于必修课。
5.3 级联下拉越用越慢
现象:上线初期流畅,数据量没涨,但地址选择页越来越卡。
原因:多半是循环单查导致的 N+1,也可能是parent_id没索引。还有一种隐蔽情况,查询里带了ORDER BY RAND()之类的随机排序——我在一个老项目里真见过,为了「推荐常用地址」,列表查询里塞了随机函数,全表扫加排序,几千行也能拖到几百毫秒。
解决:EXPLAIN看执行计划,确认走的是idx_parent;把循环查询改成IN批量查;排查是否有函数包裹索引列、随机排序这类隐形杀手。
5.4 直辖市层级显示异常
现象:北京、上海这类直辖市,前端下拉里出现「北京市 → 北京市 → 朝阳区」,多了一层重复。
原因:不同的数据源对直辖市处理方式不同,有的是「省=北京市、市=北京市」,有的是省级直接挂区县,层级跳了一级。前端按固定三级渲染,自然会出重复。
解决:导入前先抽样检查直辖市相关记录,确认市级记录存在且层级正确;如果数据源本身缺少市级,在导入阶段补一条与省级同名的市级记录,保持四级结构统一,比在前端写特判干净得多。
5.5 更新后历史订单地址显示为空
现象:做了一次区划数据更新,历史订单的收货地址查不出省市区名称了。
原因:更新脚本用了「先删后插」或者直接物理删除了被撤并的记录,历史订单引用的编码在表里不存在,join 出来是 null。
解决:恢复备份,改用 upsert 加置status = 0的方式;同时给业务侧的地址查询加上兜底,join 不到时至少回显订单里存的原始文本。教训很明确:区划数据只允许软删除,物理删除是给自己埋雷。
6. 把省市区数据接进缓存与接口层:一个能扛住并发的落地技巧
数据摆进 MySQL 就算完事,只适合后台管理这类低并发场景。真正面向 C 端的地址选择,如果每次都打数据库,几千 QPS 下再快的索引也顶不住,而且这类数据读多写极少,是最典型的缓存友好型数据。我的习惯是:启动时全量加载进本地缓存,配一个版本号做失效控制。
落地方式很朴素。应用启动时一次性select全表几千条记录,在内存里按parent_id建Map<Long, List<Region>>,级联查询退化成内存里的get操作,响应时间从毫秒级降到微秒级。数据量小,内存占用可以忽略;更新数据时不动缓存,而是让缓存带一个version字段,接口返回时带上版本号,前端发现版本变化就重新拉取,或者干脆让应用监听一个配置项,改版本号触发全量重载。
关键在接口设计上。我给这类接口定了个规矩:单接口返回整棵树的轻量结构,只带 id、code、name、parent_id、level 五个字段,不带拼音、不带排序、不带扩展信息,一个接口解决三级联动,前端拿着整棵树本地过滤。相比按层请求,少一次往返就少一次失败可能,弱网环境下的体验差距很明显。整棵树序列化后通常只有几百 KB,开 gzip 之后更小,比三次串行请求更快也更稳。
对应到缓存实现,Java 侧用ConcurrentHashMap存父级到子列表的映射,Python 侧用一个普通 dict 加模块级变量即可,重点是加载动作放在启动阶段而不是首次请求时——首次请求加载会带来一次雪崩式的慢响应,几个用户同时进来就都堵在那儿。
| 方案 | 响应时间 | 一致性 | 适用场景 |
|---|---|---|---|
| 每次查库 | 毫秒级 | 强 | 后台管理、低频操作 |
| 本地缓存全量树 | 微秒级 | 依赖版本号刷新 | C 端地址选择、高并发 |
| Redis 缓存 | 亚毫秒级 | 需过期策略 | 多实例共享、不能本地存储 |
还有个细节别忽略:接口返回的字段顺序和层级结构要固定,前端按parent_id分组时依赖这个稳定性。曾经有一次我在返回里顺手加了sort字段并改了默认排序,前端的省份顺序全乱了,排查了半天才发现是接口层动了手脚。从那以后我每次改这类基础数据接口,都强制走一遍「旧前端 + 新接口」的联调,确认结构向后兼容再发。
省市区这份数据本身不复杂,复杂的是围绕它的层级语义、编码规范和更新节奏,把这几点吃透,它能安安稳稳支撑你很多年。希望帮到你。
本文还有配套的精品资源,点击获取