简介:这是一份面向后端开发、全栈工程师及需要处理地址信息的项目开发者的MySQL行政区划数据资源,以SQL脚本形式提供中国省、市、区(县)三级数据,可直接导入数据库使用。压缩包内共1个PDF文件,约495KB,内容涵盖建表语句、字段说明与完整数据插入示例,核心表db_yhm_city通过class_id、class_parent_id、class_name、class_type四个字段构建层级关系,并配有class_parent_id与class_type索引以提升查询效率。资源中给出了按省份查城市、按城市查区县等典型SQL示例,可支撑电商地址自动填充、物流配送路径规划及基于地理位置的数据分析等场景。目前已有1080人学习下载,适合需要快速搭建地区数据表、减少手工整理成本的开发者参考使用。
1. 一份能直接导入的 MySQL 省市区 SQL:它到底省掉了哪些活
做过后台的人都碰过这个场景:运营要一个省市区三级联动下拉框,前端催着要接口,你打开数据库发现地址表是空的,或者字段设计得乱七八糟——省市区混在一张表里没有层级,查一个「广东省下面有哪些市」得写三段 UNION。这时候如果手里有一份结构清晰、能直接source进去的 SQL 文件,十分钟就能把接口跑通。这份 MySQL 版中国省市区数据表 SQL 就是干这个的:一张db_yhm_city表,四个字段,用class_parent_id把国家、省、市串成树,class_type标记层级,导入即用。它适合中小型 Web 项目、后台管理系统、电商收货地址模块这类不需要街道级别、只要省市两级就够的场景。下面我按「表结构怎么读 → 怎么导入 → 怎么写查询 → 哪里会翻车」的顺序拆一遍,都是能直接抄的。
2. 拆开 db_yhm_city:四个字段怎么撑起三级行政区划
2.1 字段设计背后的层级逻辑
先看建表语句,这是整份资源的骨架:
CREATE TABLE `db_yhm_city` ( `class_id` smallint(5) unsigned NOT NULL AUTO_INCREMENT, `class_parent_id` smallint(5) unsigned NOT NULL DEFAULT '0', `class_name` varchar(120) NOT NULL DEFAULT '', `class_type` tinyint(1) NOT NULL DEFAULT '2', PRIMARY KEY (`class_id`), KEY `class_parent_id` (`class_parent_id`), KEY `class_type` (`class_type`) ) ENGINE=MyISAM AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;四个字段各司其职。class_id是自增主键,也是每个行政区划的唯一身份证,插入数据时是显式指定值的(比如中国是 1,北京是 2),所以AUTO_INCREMENT=1在这里更多是个形式,实际 ID 由 INSERT 语句写死。class_parent_id是整张表的灵魂,它指向父级记录的class_id:中国的class_parent_id是 0(顶层),北京的class_parent_id是 1(指向中国),安庆的class_parent_id是 3(指向安徽)。class_name存名称,class_type标记层级——从数据看,0 是国家,1 是省级,2 是市级。
这里有个容易忽略的点:class_type的默认值是 2,意味着如果你插入时漏写这个字段,数据会被当成市级。批量导入自己补充的数据时,这个默认值可能让省级数据悄悄变成市级,查询时怎么都查不出来。我一般会在导入后跑一遍校验,确认每个层级的数量对得上。
2.2 索引为什么建在 parent_id 和 type 上
两个 KEY 不是随便加的。省市区查询的典型模式是「给定父级 ID,查它下面所有子级」,也就是WHERE class_parent_id = ?,所以class_parent_id必须有索引,否则每次联动查询都全表扫描。class_type的索引服务于「我要所有省份」这类查询,WHERE class_type = 1能快速过滤出省级记录。
但要注意,这份表用的是 MyISAM 引擎。MyISAM 不支持事务,插入过程中断了不会回滚,可能留下半截数据。对于省市区这种读多写少、导入一次基本不改的数据,MyISAM 够用,查询速度也不差。但如果你的项目已经在用 InnoDB,或者需要外键约束,建议把ENGINE=MyISAM改成ENGINE=InnoDB,其余不动。改引擎不影响数据本身,只是换个存储方式。
2.3 数据覆盖范围与层级完整性
从 INSERT 数据看,省级覆盖了 34 个(含香港、澳门、台湾),市级数据按省逐个列出。以安徽为例,class_parent_id=3下面挂了安庆、蚌埠、巢湖、池州等 16 个市,class_type都是 2。广东挂了 21 个市,数据密度因省而异。
需要提前说清楚:这份数据是省市两级,没有区县级别。摘要里提到「区(县)级别字段」,但实际 INSERT 数据里class_type只出现 0、1、2 三种值,没有 3。如果你的业务需要区县级联动,这份资源不够用,得自己补第三级数据,或者换一份带区县的数据源。这一点在选型时就要确认,别导进去才发现少一层。
3. 导入实操:从 source 命令到字符集校验
3.1 导入前的环境确认
导入之前先确认三件事:MySQL 版本、目标库的字符集、SQL 文件编码。这份 SQL 用的是DEFAULT CHARSET=utf8,如果你的库是utf8mb4,导入后中文不会乱码,但表本身的字符集是 utf8。utf8 在 MySQL 里最多存 3 字节,存不下 emoji 和部分生僻字。省市区名称一般用不到 4 字节字符,但如果你的应用同一张库里有用户昵称之类的字段,建议统一成 utf8mb4。
确认命令:
# 查看 MySQL 版本 mysql --version # 登录后查看目标库字符集 mysql -u root -p -e "SHOW CREATE DATABASE your_db\G"如果目标库是 utf8mb4,导入后可以手动改表字符集:
ALTER TABLE db_yhm_city CONVERT TO CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci;3.2 三种导入方式与适用场景
方式一:命令行 source,适合本地开发:
mysql -u root -p your_db进入 MySQL 交互界面后:
source /path/to/db_yhm_city.sql;source命令要求文件路径是绝对路径或相对于 MySQL 启动目录的路径,写相对路径经常找不到文件,这是新手第一个坑。
方式二:重定向导入,适合脚本自动化:
mysql -u root -p your_db < /path/to/db_yhm_city.sql这种方式不需要进交互界面,一条命令搞定,适合写进部署脚本。注意<是 shell 重定向,不是 MySQL 命令。
方式三:Navicat 等工具导入,适合不熟悉命令行的同学。右键目标库 → 运行 SQL 文件 → 选择文件 → 开始。工具会自动处理编码,但导入大文件时可能超时,省市区数据量不大,一般没问题。
导入完成后验证:
-- 确认总数 SELECT COUNT(*) FROM db_yhm_city; -- 按层级统计 SELECT class_type, COUNT(*) FROM db_yhm_city GROUP BY class_type; -- 抽查省级 SELECT * FROM db_yhm_city WHERE class_type = 1 LIMIT 5;如果class_type=1的数量明显不对(比如只有几个),大概率是导入中断或者字符集问题导致部分 INSERT 失败。
3.3 导入后的字符集与排序规则检查
导入后跑一条带中文的查询,看返回是否正常:
SELECT class_name FROM db_yhm_city WHERE class_id = 3;正常应该返回「安徽」。如果返回??或乱码,说明客户端连接字符集不对。检查:
SHOW VARIABLES LIKE 'character_set%';重点看character_set_client、character_set_connection、character_set_results三个值。如果不是 utf8 或 utf8mb4,在连接时指定:
mysql -u root -p --default-character-set=utf8mb4 your_db或者在 MySQL 配置文件里改[client]段的default-character-set。字符集问题是导入环节最高频的翻车点,没有之一。
4. 查询实战:省市区联动的 SQL 写法与性能边界
4.1 查省份列表与查下级城市
最基础的两个查询,前端联动全靠它们:
-- 查所有省级(class_type=1) SELECT class_id, class_name FROM db_yhm_city WHERE class_type = 1 ORDER BY class_id; -- 查某个省下面所有市(以安徽为例,class_id=3) SELECT class_id, class_name FROM db_yhm_city WHERE class_parent_id = 3 AND class_type = 2 ORDER BY class_id;第一条走class_type索引,第二条走class_parent_id索引,都是毫秒级返回。注意第二条加了class_type = 2条件,虽然class_parent_id=3下面目前只有市级数据,但加上类型过滤更严谨,防止未来混入其他层级数据。
4.2 一次查出「省-市」两级结构
如果不想发两次请求,可以用自连接一次查出省和它下面的市:
SELECT p.class_id AS province_id, p.class_name AS province_name, c.class_id AS city_id, c.class_name AS city_name FROM db_yhm_city p LEFT JOIN db_yhm_city c ON c.class_parent_id = p.class_id AND c.class_type = 2 WHERE p.class_type = 1 ORDER BY p.class_id, c.class_id;这条查询返回的结果集是「省 + 市」的笛卡尔展开,前端拿到后按province_id分组即可。数据量方面,34 个省加 300 多个市,结果集几百行,一次传输完全可接受。但要注意LEFT JOIN的使用——如果某个省下面没有市级数据,city_id会是 NULL,前端要处理这种情况。
4.3 递归查询的替代方案
MySQL 5.7 不支持 CTE 递归,MySQL 8.0 才支持WITH RECURSIVE。这份数据只有两级,用不上递归。但如果以后扩展到区县级(三级),查询某个省下面所有区县就需要递归或者多次查询。在 5.7 环境下,常见做法是应用层做两次查询:先查省下的市,再用IN查这些市下的区县。
-- 第一步:查安徽下的所有市 ID SELECT class_id FROM db_yhm_city WHERE class_parent_id = 3 AND class_type = 2; -- 第二步:用上一步的 ID 列表查区县(假设有 class_type=3 的数据) SELECT * FROM db_yhm_city WHERE class_parent_id IN (36,37,38,...) AND class_type = 3;这种写法在市级数量不多时没问题,但如果要查全国所有区县,IN列表会很长,建议改成 JOIN:
SELECT d.* FROM db_yhm_city d INNER JOIN db_yhm_city c ON d.class_parent_id = c.class_id WHERE c.class_parent_id = 3 AND c.class_type = 2 AND d.class_type = 3;4.4 缓存策略与查询频率控制
省市区数据几乎不变,每次请求都查库是浪费。常见做法是在应用启动时把整张表加载到内存,用class_parent_id做 key 建一个 Map。Java 里可以用Map<Integer, List<City>>,PHP 里用数组,Node 里用对象。加载一次,后续联动查询全走内存,数据库压力为零。
如果不想全量加载,至少给省级列表加缓存,因为「查所有省份」这个查询每个用户打开地址表单都会触发。缓存时间可以设长一点,比如 24 小时,数据更新时手动刷新。
5. 避坑排查:导入和查询中最容易翻车的五个点
5.1 导入报「Unknown command '''」
现象:用source导入时,终端刷出一堆Unknown command '\''或者ERROR at line ...。
原因:SQL 文件编码和 MySQL 客户端字符集不匹配,常见于文件是 GBK 编码但客户端按 utf8 解析,或者文件里有 BOM 头。
解决:用file命令确认文件编码,如果是 GBK,先转成 utf8:
iconv -f GBK -t UTF-8 db_yhm_city.sql -o db_yhm_city_utf8.sql然后用--default-character-set=utf8重新导入。BOM 头可以用sed去掉:
sed -i '1s/^\xEF\xBB\xBF//' db_yhm_city.sql5.2 中文变成问号或乱码
现象:导入成功,但SELECT出来的中文全是???。
原因:连接字符集不是 utf8。MySQL 客户端、连接层、结果集三层字符集只要有一层不对,中文就废了。
解决:在连接串里显式指定字符集。命令行加--default-character-set=utf8mb4,JDBC 加?useUnicode=true&characterEncoding=utf8,PHP PDO 在 DSN 里加charset=utf8。导入前先SET NAMES utf8mb4;也能临时解决。
5.3 class_type 默认值导致层级错乱
现象:自己补充了几个省的数据,插入时没写class_type,结果查省级列表查不到这些新增的省。
原因:class_type默认值是 2,不写就变成市级。省级必须是 1,国家是 0。
解决:插入时显式指定class_type。批量插入后跑校验:
SELECT class_id, class_name FROM db_yhm_city WHERE class_parent_id = 1 AND class_type != 1;这条查询会揪出所有挂在「中国」下面但不是省级的记录。
5.4 MyISAM 表导入中断留下脏数据
现象:导入到一半网络断了或者终端关了,重新导入时主键冲突。
原因:MyISAM 不支持事务,已插入的数据不会回滚。重新执行CREATE TABLE前没有DROP TABLE,或者DROP TABLE IF EXISTS没生效。
解决:SQL 文件开头有DROP TABLE IF EXISTS db_yhm_city;,重新导入前先手动确认表已删除:
DROP TABLE IF EXISTS db_yhm_city;然后再source。如果用的是 InnoDB,可以把整个导入包在一个事务里,出错自动回滚。
5.5 查询结果顺序不稳定
现象:同样的查询,两次执行返回的城市顺序不一样。
原因:没有ORDER BY,MySQL 不保证返回顺序。MyISAM 表尤其明显,因为数据按插入顺序存储,但查询优化器可能选择不同的索引。
解决:所有对外输出的查询都加ORDER BY class_id。如果业务需要按名称排序,加ORDER BY class_name,但要注意中文排序在 utf8_general_ci 下是按 Unicode 码点排的,不是拼音顺序。需要拼音排序的话,得额外加一个拼音字段。
6. 进阶技巧:把这份数据用出花来的三个习惯
第一个习惯是导入后立刻跑一遍完整性校验。我一般会写一个校验脚本,检查三件事:省级数量是否等于 34、每个省下面是否至少有一个市、有没有孤儿记录(class_parent_id指向不存在的class_id)。孤儿记录的检查 SQL 是这样:
SELECT c.* FROM db_yhm_city c LEFT JOIN db_yhm_city p ON c.class_parent_id = p.class_id WHERE c.class_parent_id != 0 AND p.class_id IS NULL;正常应该返回空结果。如果有记录,说明数据有断裂,联动查询时这些城市永远显示不出来。
第二个习惯是给表加一个sort_order字段。原始数据按class_id排序,但class_id是插入顺序,不是业务想要的顺序。比如直辖市应该排在省份前面,或者按拼音排序。加字段的语句:
ALTER TABLE db_yhm_city ADD COLUMN sort_order INT NOT NULL DEFAULT 0 AFTER class_name;然后按需更新sort_order,查询时ORDER BY sort_order, class_id。这个字段不影响原有逻辑,但让前端展示更可控。
第三个习惯是导出时带上CREATE TABLE和INSERT的完整语句,方便迁移。用mysqldump:
mysqldump -u root -p --no-create-db --skip-extended-insert your_db db_yhm_city > db_yhm_city_backup.sql--skip-extended-insert让每条记录一行 INSERT,方便 diff 和手动修改。不加这个参数的话,所有记录会合并成一条长 INSERT,改一个值要翻半天。
还有一个实际项目里常用的技巧:把省市区数据做成 JSON 文件,前端直接加载,完全不经过数据库。适合纯静态页面或者对接口延迟敏感的场景。导出 JSON 可以用 Python 脚本:
import pymysql import json conn = pymysql.connect(host='localhost', user='root', password='', db='your_db', charset='utf8') cursor = conn.cursor(pymysql.cursors.DictCursor) cursor.execute("SELECT class_id, class_parent_id, class_name, class_type FROM db_yhm_city ORDER BY class_id") rows = cursor.fetchall() # 构建嵌套结构 provinces = [r for r in rows if r['class_type'] == 1] result = [] for p in provinces: cities = [r for r in rows if r['class_parent_id'] == p['class_id'] and r['class_type'] == 2] result.append({ 'id': p['class_id'], 'name': p['class_name'], 'cities': [{'id': c['class_id'], 'name': c['class_name']} for c in cities] }) with open('cities.json', 'w', encoding='utf-8') as f: json.dump(result, f, ensure_ascii=False, indent=2)这段脚本把扁平表转成嵌套 JSON,前端拿到后直接渲染三级联动,省掉接口调用。注意ensure_ascii=False保证中文正常输出,indent=2让文件可读。生成的 JSON 大概几百 KB,gzip 后几十 KB,加载速度没问题。
从那以后我每次拿到一份新的地址数据 SQL,都强制走一遍「导入 → 校验 → 导出 JSON → 前端联调」的完整链路,确认每一环都通再往项目里合。省市区数据看着简单,但字符集、层级、排序这三个地方各踩一次坑,半天就没了。希望帮到你。
本文还有配套的精品资源,点击获取