简介:这份 MySQL 版中国省市区数据表 SQL 资源,面向需要处理行政区划信息的后端开发者、电商与物流系统设计人员,以及正在学习数据库表结构设计的学习者。它提供了一张可直接导入使用的db_yhm_city表,通过class_id、class_parent_id、class_name、class_type四个字段构建起国家、省、市、区(县)的层级关系,并配有主键及class_parent_id、class_type索引,便于按上级 ID 逐级查询城市或区县。资源包共 1 个文件,为 PDF 格式,约 495KB,内容涵盖建表语句、省市区数据插入示例以及常用查询 SQL,可直接参考或迁移到实际项目中。目前已有 1081 人学习下载,适合需要快速搭建地址库、实现地址自动填充或进行地理维度数据分析的开发者参考使用。
1. 从一张只有四个字段的表说起:为什么省市区数据总在项目里翻车
做电商下单页、物流面单、用户地址管理,绕不开省市区三级联动。很多人第一反应是找一份 JSON 前端写死,或者干脆调第三方接口。前端写死的问题在于数据更新要发版,第三方接口的问题在于限流和网络抖动,一旦挂了整个下单流程就卡住。这份 MySQL 版中国省市区数据表 SQL 走的是另一条路:把行政区划直接落库,用一张自增主键加父级 ID 的表把国家、省、市三级串起来,查询靠class_parent_id递归,不依赖任何外部服务。它适合中小型业务快速接入,也适合作为地址模块的底座数据。表结构极简,四个字段:class_id、class_parent_id、class_name、class_type,但正是这种极简设计,让不少人在导入和查询时踩了坑。下面从建表、导入、查询到避坑,一步步拆开讲。
2. 建表与字段设计:四个字段怎么撑起三级联动
2.1 表结构逐字段拆解
拿到 SQL 文件,第一段就是建表语句。表名db_yhm_city,存储引擎 MyISAM,字符集 utf8。四个字段各司其职:
| 字段名 | 类型 | 作用 | 关键点 |
|---|---|---|---|
class_id | smallint(5) unsigned | 行政区划唯一标识,自增主键 | 手动插入时需显式指定值,否则自增序列会乱 |
class_parent_id | smallint(5) unsigned | 上级行政区划 ID | 顶级节点(中国)为 0,省为 1,市为对应省 ID |
class_name | varchar(120) | 行政区划名称 | utf8 下中文占 3 字节,120 足够长名称 |
class_type | tinyint(1) | 层级类型:0 国家、1 省、2 市 | 查询时靠它过滤层级,避免混查 |
class_type是这份数据表设计里最值得说的一个字段。没有它,你查“所有省”只能靠class_parent_id = 1来推断,但一旦数据里混入直辖市或特别行政区,逻辑就会变得脆弱。有了class_type,WHERE class_type = 1直接锁定省级,语义清晰。class_parent_id上建了普通索引,class_type上也建了索引,这两个索引是后续所有查询的性能基础。
2.2 建表语句执行与字符集确认
把建表段单独拿出来执行,建议先确认数据库默认字符集。如果库是utf8mb4,表是utf8,跨表 JOIN 时可能出现排序规则冲突。常见做法是统一改成utf8mb4,或者在建表时显式指定COLLATE utf8_general_ci。
-- 先建库(如果还没有),字符集与表保持一致 CREATE DATABASE IF NOT EXISTS `demo_region` DEFAULT CHARACTER SET utf8 COLLATE utf8_general_ci; USE `demo_region`; -- 建表:注意 ENGINE 和 CHARSET 与原始 SQL 一致 DROP TABLE IF EXISTS `db_yhm_city`; 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;这段代码里DROP TABLE IF EXISTS是必须保留的,因为原始 SQL 文件通常包含它,重复导入时不会报错。AUTO_INCREMENT=1配合后续 INSERT 里显式指定的class_id,实际自增值会被推到最大 ID 之后。MyISAM 引擎不支持事务,导入中途失败不会回滚,所以导入前最好先备份或确认表是空的。
提示:如果业务需要事务支持,可以把
ENGINE=MyISAM改成ENGINE=InnoDB,字段和索引定义不用动。InnoDB 在并发查询下表现更稳,代价是插入速度略慢。
2.3 数据层级关系验证
建完表先别急着写业务代码,用几条查询确认层级关系是否正确。原始数据里class_id=1是中国,class_parent_id=0,class_type=0;class_id=2是北京,class_parent_id=1,class_type=1;class_id=52是北京下的“北京”,class_parent_id=2,class_type=2。这种“省名和市名相同”的情况在直辖市里很常见,查询时要注意区分。
-- 查所有省级节点 SELECT class_id, class_name FROM db_yhm_city WHERE class_type = 1 ORDER BY class_id; -- 查北京省下的所有市级节点 SELECT class_id, class_name FROM db_yhm_city WHERE class_parent_id = 2 AND class_type = 2; -- 验证层级完整性:有没有市级节点的父级不是省级 SELECT c.class_id, c.class_name, c.class_parent_id FROM db_yhm_city c LEFT JOIN db_yhm_city p ON c.class_parent_id = p.class_id WHERE c.class_type = 2 AND (p.class_id IS NULL OR p.class_type != 1);第三条查询是数据质量检查的常用手段。如果返回空结果,说明市级节点的父级都能正确指向省级节点。如果返回了记录,说明数据里有“孤儿节点”或者父级类型不对,需要手动修正。这类检查在导入任何层级数据后都值得跑一遍。
3. 数据导入:批量 INSERT 的三种姿势与性能对比
3.1 直接执行原始 SQL 文件
最省事的方式是用命令行一次性导入。假设文件名为db_yhm_city.sql,放在当前目录:
# 方式一:mysql 命令直接导入 mysql -u root -p demo_region < db_yhm_city.sql # 方式二:进入 mysql 后用 source mysql -u root -p USE demo_region; SOURCE db_yhm_city.sql;方式一适合脚本化部署,方式二适合交互式调试。注意SOURCE命令的路径是相对于 mysql 客户端当前工作目录的,不是相对于 SQL 文件所在目录。如果文件很大,导入过程中可以用SHOW PROCESSLIST;另开一个会话查看进度。
3.2 用 Python 脚本拆分导入并做校验
原始 SQL 文件里 INSERT 语句是一条一条写的,几千条数据直接执行没问题,但如果后续要增量更新或做数据清洗,用脚本处理更灵活。下面这段 Python 用pymysql连接数据库,逐条读取 SQL 文件里的 INSERT 并执行,同时统计各层级数量。
import re import pymysql # 连接配置:按实际环境改 conn = pymysql.connect( host='127.0.0.1', user='root', password='your_password', database='demo_region', charset='utf8' ) cursor = conn.cursor() # 读取 SQL 文件,提取所有 INSERT 语句 with open('db_yhm_city.sql', 'r', encoding='utf-8') as f: content = f.read() # 匹配 INSERT INTO `db_yhm_city` VALUES (...); pattern = re.compile(r"INSERT INTO `db_yhm_city` VALUES \((.+?)\);") matches = pattern.findall(content) inserted = 0 for values in matches: sql = f"INSERT INTO `db_yhm_city` VALUES ({values})" try: cursor.execute(sql) inserted += 1 except Exception as e: print(f"失败: {sql[:80]}... 原因: {e}") conn.commit() # 校验各层级数量 cursor.execute("SELECT class_type, COUNT(*) FROM db_yhm_city GROUP BY class_type") for row in cursor.fetchall(): print(f"class_type={row[0]}, 数量={row[1]}") print(f"共导入 {inserted} 条") cursor.close() conn.close()正则INSERT INTO \db_yhm_city` VALUES ((.+?));里的.+?是非贪婪匹配,确保每条 INSERT 单独捕获。class_type` 分组统计能快速看出数据是否完整:正常应该有 1 条国家、30 多条省级、几百条市级。如果某个层级数量明显偏少,说明 SQL 文件可能被截断,需要检查文件末尾是否完整。
3.3 导入后的索引重建与统计信息更新
MyISAM 表在大量 INSERT 之后,索引统计信息可能不是最新的。虽然 MyISAM 不像 InnoDB 那样有复杂的统计信息,但执行一次ANALYZE TABLE可以让优化器更准确地选择索引。
-- 重建索引统计信息 ANALYZE TABLE db_yhm_city; -- 查看表状态,确认行数和索引情况 SHOW TABLE STATUS LIKE 'db_yhm_city'; -- 确认最大 ID,后续新增数据时参考 SELECT MAX(class_id) FROM db_yhm_city;SHOW TABLE STATUS返回的Rows字段在 MyISAM 下是精确值,可以直接用来核对导入条数。MAX(class_id)决定了后续如果手动插入新节点,应该从哪个 ID 开始,避免主键冲突。
4. 查询实战:从省查市、从市查区、递归查全路径
4.1 基础两级查询与索引命中
最常见的需求是“给定省 ID,查出所有市”。原始数据里省级class_type=1,市级class_type=2,父级关系靠class_parent_id。
-- 查广东省(class_id=6)下的所有市 SELECT class_id, class_name FROM db_yhm_city WHERE class_parent_id = 6 AND class_type = 2 ORDER BY class_id;这条查询会命中class_parent_id索引。用EXPLAIN看一下执行计划:
EXPLAIN SELECT class_id, class_name FROM db_yhm_city WHERE class_parent_id = 6 AND class_type = 2;如果key列显示class_parent_id,说明索引生效。如果显示NULL或走了全表扫描,可能是数据量太小优化器认为全表更快,或者索引统计信息过期,执行ANALYZE TABLE后再试。
4.2 递归查询全路径:从区县反查省市
业务里经常需要把“区县 ID”还原成“省-市-区”完整地址。这份数据表只有两级父级关系(市→省→国),用两次 JOIN 就能拼出全路径。
-- 假设区县 ID 为 100,反查省、市、区名称 SELECT province.class_name AS province_name, city.class_name AS city_name, district.class_name AS district_name FROM db_yhm_city AS district LEFT JOIN db_yhm_city AS city ON district.class_parent_id = city.class_id LEFT JOIN db_yhm_city AS province ON city.class_parent_id = province.class_id WHERE district.class_id = 100;这里用了两次自连接。district是区县节点,city是它的父级,province是父级的父级。LEFT JOIN保证即使某一级缺失也能返回部分结果,方便排查数据问题。如果数据里区县层级是class_type=3,但原始 SQL 里只到市级,那么这条查询的district部分需要根据实际数据调整。
4.3 用变量实现递归查询(MySQL 8.0 以下)
MySQL 8.0 之前没有 CTE 递归,但可以用会话变量模拟。下面这段 SQL 从任意节点向上追溯所有祖先:
-- 从 class_id=100 向上查所有祖先 SELECT class_id, class_name, class_parent_id, class_type FROM ( SELECT class_id, class_name, class_parent_id, class_type, @pid := class_parent_id AS next_pid, @lvl := @lvl + 1 AS level FROM db_yhm_city, (SELECT @pid := 100, @lvl := 0) vars WHERE class_id = @pid UNION ALL SELECT c.class_id, c.class_name, c.class_parent_id, c.class_type, @pid := c.class_parent_id, @lvl := @lvl + 1 FROM db_yhm_city c JOIN (SELECT @pid := @pid) tmp WHERE c.class_id = @pid AND @pid != 0 ) AS tree ORDER BY level;这段 SQL 依赖会话变量@pid在每一行更新后传递给下一行。写法比较绕,实际项目中更推荐在应用层用循环查询,可读性更好。如果数据库是 MySQL 8.0+,直接用WITH RECURSIVE更清晰:
WITH RECURSIVE region_tree AS ( SELECT class_id, class_name, class_parent_id, class_type, 0 AS level FROM db_yhm_city WHERE class_id = 100 UNION ALL SELECT c.class_id, c.class_name, c.class_parent_id, c.class_type, rt.level + 1 FROM db_yhm_city c INNER JOIN region_tree rt ON c.class_id = rt.class_parent_id ) SELECT * FROM region_tree ORDER BY level;WITH RECURSIVE的终止条件是INNER JOIN找不到匹配的父级,自然停止。level字段从 0 开始递增,方便前端按层级展示。
5. 避坑与排查:导入和查询中最容易翻车的五个点
5.1 现象:导入后中文显示乱码,查询出来是问号
原因:数据库、表、连接三者的字符集不一致。原始 SQL 是utf8,但客户端连接可能默认latin1或gbk。解决:在连接字符串里显式指定charset='utf8',建库时也指定DEFAULT CHARACTER SET utf8。如果已经乱码,需要重新导入,不能靠ALTER TABLE修复已损坏的数据。
5.2 现象:class_parent_id索引没生效,查询慢
原因:MyISAM 表的索引统计信息过期,或者查询条件里对class_parent_id做了函数运算(如WHERE class_parent_id + 0 = 6)。解决:执行ANALYZE TABLE db_yhm_city;更新统计信息,查询时保持字段裸用,不要在索引列上做运算。
5.3 现象:直辖市查询结果重复,北京省下还有一个北京市
原因:原始数据里直辖市既作为省级节点(class_id=2,class_type=1),又作为市级节点(class_id=52,class_type=2)。解决:查询时严格用class_type过滤。前端联动时,省级选中北京后,市级列表里会出现“北京”,这是正常设计,不要误删。
5.4 现象:AUTO_INCREMENT冲突,新增节点报主键重复
原因:原始 SQL 里显式插入了class_id,但表的AUTO_INCREMENT值没有同步更新。解决:导入后执行SELECT MAX(class_id) FROM db_yhm_city;,然后ALTER TABLE db_yhm_city AUTO_INCREMENT = 最大值+1;。或者新增节点时也显式指定class_id,不依赖自增。
5.5 现象:MyISAM 表在并发写入时锁表,下单高峰期卡顿
原因:MyISAM 只支持表级锁,写操作会阻塞所有读。解决:如果业务有频繁写入地址数据的需求,把引擎改成 InnoDB。改法:ALTER TABLE db_yhm_city ENGINE=InnoDB;。改完后确认索引还在,InnoDB 支持行级锁,并发读写不会互相阻塞。
6. 进阶技巧:把省市区数据用出花来的三个习惯
第一个习惯是给class_name加前缀索引。如果业务需要按名称模糊搜索,比如用户输入“广”要匹配“广东”“广西”“广州”,直接LIKE '%广%'会全表扫描。可以加一个KEY idx_name (class_name(10)),让前缀匹配走索引。虽然%广%这种前后模糊仍然无法命中,但LIKE '广%'可以。
-- 添加名称前缀索引 ALTER TABLE db_yhm_city ADD KEY idx_name (class_name(10)); -- 前缀匹配走索引 EXPLAIN SELECT * FROM db_yhm_city WHERE class_name LIKE '广%';第二个习惯是定期用CHECKSUM TABLE校验数据一致性。如果有多套环境(开发、测试、生产),导入同一份 SQL 后执行CHECKSUM TABLE db_yhm_city;,比对校验和是否一致。不一致说明某个环境的导入过程出了问题,比如文件截断或字符集转换错误。
-- 校验数据一致性 CHECKSUM TABLE db_yhm_city;第三个习惯是导出时用mysqldump带--no-create-info,只导数据不导建表语句,方便在已有表结构的环境里增量更新。
# 只导出数据,不导出建表语句 mysqldump -u root -p --no-create-info demo_region db_yhm_city > city_data_only.sql # 导入时用 INSERT IGNORE 避免主键冲突 mysql -u root -p demo_region --execute="SET SESSION sql_mode=''; SOURCE city_data_only.sql;"--no-create-info适合表结构已经存在、只需要刷新数据的场景。配合INSERT IGNORE或REPLACE INTO,可以在不删表的情况下更新行政区划数据。我一般会在每次数据更新前先CHECKSUM TABLE记录旧值,导入后再校验一次,确认数据确实变了且没有丢行。从那以后我每次导入省市区数据都强制走一遍“建表→导入→ANALYZE→CHECKSUM→抽样查询”的流程,再也没出现过上线后地址下拉框空白的事故。希望帮到你。
本文还有配套的精品资源,点击获取