简介:这份资源是面向后端开发、数据分析与地理信息系统开发者的MySQL行政区划数据包,用于解决应用中省份、城市、区县层级数据的存储与查询问题。压缩包内共1个SQL脚本文件,整体约69KB,通过执行该脚本即可快速创建并填充全国省份城市数据表,省去手工整理行政区划的繁琐工作。表结构通常包含主键id、省份、城市、区县、行政区域编码、层级标识及父级ID等字段,借助parent_id可构建省市区三级层次关系,配合JOIN或递归查询能灵活获取完整行政链。该数据可广泛用于物流地址管理、人口统计分析与用户地域分布等场景,也能与人口表、公司地址表关联扩展。目前已有638人学习下载,适合需要快速搭建地域基础数据的中初级开发者参考使用。
1. 全国省份城市数据库表:一份被低估的“地基级”数据资产
做后端的人迟早会撞上一件事:用户注册要选地区,订单要记收货地址,后台报表要按省聚合。这时候你打开搜索引擎,输入“全国省份城市数据库表”,跳出来的多半是各种打包好的 SQL 文件。很多人第一反应是“这玩意儿有什么技术含量”,随手下一份导入就完事。但真正踩过坑的人知道,一份干净的省市区数据表,能省掉你后面至少两周的对账和清洗工作。它解决的不是算法问题,而是数据一致性、层级编码和联动查询这三个最容易被忽视的工程问题。适合谁用?做电商、CRM、本地生活、物流调度、后台管理系统的开发者,以及任何需要“省—市—区”三级联动的场景。这份数据表的价值不在于数据本身多难获取,而在于它把行政层级关系、编码规则和查询模式一次性固化下来,让你不用每次从零拼装。
2. 省市区三级表怎么设计:从编码规则到字段取舍
2.1 为什么不用一张自关联表打天下
最常见的偷懒做法是建一张region表,字段只有id、name、parent_id,然后靠递归查询拼出“省—市—区”。这种设计在数据量小的时候没问题,但一旦你要做“按省统计订单量”或者“查询某市下所有区县”,递归查询的性能就会成为瓶颈。更麻烦的是,自关联表很难在数据库层面做约束,比如你无法用外键保证“区的 parent_id 一定指向一个市,而不是另一个区”。
我一般会采用三张独立表的结构:province、city、district,每张表只存自己层级的记录,通过外键关联。这样做的好处是查询路径清晰,索引命中率高,而且每张表的字段可以按需定制。比如省份表可能需要short_name(简称)和sort_order(排序权重),城市表可能需要is_hot(是否热门城市),区县表可能需要zip_code(邮政编码)。如果全塞在一张表里,字段会变得非常稀疏。
2.2 行政区划编码:六位数字背后的逻辑
国家标准 GB/T 2260 规定了行政区划代码,六位数字,前两位是省,中间两位是市,后两位是区县。比如110101代表某直辖市的一个区。这个编码规则是省市区数据表的灵魂,因为它天然支持前缀查询。你想查某个省下所有市,只需要WHERE code LIKE '11%';想查某个市下所有区,WHERE code LIKE '1101%'。这比递归查询快一个数量级。
但要注意,编码不是一成不变的。撤县设区、合并乡镇都会导致编码变更。所以你的表里必须有一个version字段或者updated_at字段,记录这条数据是什么时候同步的。我见过太多项目因为用了三年前的静态数据,导致用户选不到新设的区,客诉电话直接打爆。
2.3 建表 SQL 与索引策略
下面是我常用的建表语句,以 MySQL 为例。注意字符集用utf8mb4,因为有些地名包含生僻字。
-- 省份表 CREATE TABLE `province` ( `id` int NOT NULL AUTO_INCREMENT, `code` char(2) NOT NULL COMMENT '省级编码前两位', `name` varchar(50) NOT NULL COMMENT '省份全称', `short_name` varchar(20) DEFAULT NULL COMMENT '简称', `sort_order` int DEFAULT 0 COMMENT '排序权重,越小越靠前', PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='省份表'; -- 城市表 CREATE TABLE `city` ( `id` int NOT NULL AUTO_INCREMENT, `code` char(4) NOT NULL COMMENT '市级编码前四位', `province_code` char(2) NOT NULL COMMENT '所属省份编码', `name` varchar(50) NOT NULL COMMENT '城市全称', `is_hot` tinyint(1) DEFAULT 0 COMMENT '是否热门城市', PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_province` (`province_code`), CONSTRAINT `fk_city_province` FOREIGN KEY (`province_code`) REFERENCES `province` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='城市表'; -- 区县表 CREATE TABLE `district` ( `id` int NOT NULL AUTO_INCREMENT, `code` char(6) NOT NULL COMMENT '区县完整编码', `city_code` char(4) NOT NULL COMMENT '所属城市编码', `name` varchar(50) NOT NULL COMMENT '区县全称', `zip_code` varchar(10) DEFAULT NULL COMMENT '邮政编码', PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_city` (`city_code`), CONSTRAINT `fk_district_city` FOREIGN KEY (`city_code`) REFERENCES `city` (`code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区县表';逻辑说明:三张表通过code字段的前缀关系隐式关联,同时用外键做显式约束。province.code是两位,city.code是四位,district.code是六位,这样设计的好处是任何层级的编码都能独立定位,不需要额外字段。参数方面,sort_order用来控制前端下拉框的默认排序,比如直辖市排前面;is_hot用于标记北上广深这类高频城市,前端可以单独分组展示。索引策略上,code的唯一索引保证编码不重复,province_code和city_code的普通索引加速层级查询。
2.4 数据导入的两种姿势:批量 INSERT 与 LOAD DATA
拿到 SQL 文件后,导入方式直接影响效率。如果文件只有几百 KB,直接source命令就行。但如果数据量上万条,建议用LOAD DATA INFILE,速度能快 5 到 10 倍。
# 方式一:直接执行 SQL 文件 mysql -u root -p your_database < province_city_district.sql # 方式二:如果数据是 CSV 格式,用 LOAD DATA mysql -u root -p your_database -e " LOAD DATA LOCAL INFILE 'district.csv' INTO TABLE district FIELDS TERMINATED BY ',' ENCLOSED BY '\"' LINES TERMINATED BY '\n' IGNORE 1 ROWS (code, city_code, name, zip_code); "逻辑说明:第一种方式适合小文件,简单直接。第二种方式需要先确认 MySQL 的local_infile参数是开启的,否则会报错。FIELDS TERMINATED BY指定分隔符,ENCLOSED BY处理字段里可能包含逗号的情况,IGNORE 1 ROWS跳过 CSV 的表头。导入前最好先TRUNCATE TABLE清空旧数据,避免主键冲突。
3. 查询与联动:把三级下拉框的响应压到 50ms 以内
3.1 前端联动查询的三种 SQL 写法
省市区联动是这类数据最典型的用法。用户选了省,你要立刻查出对应的市;选了市,要查出对应的区。下面三种写法我都用过,性能差异明显。
-- 写法一:逐级查询,最直观 SELECT code, name FROM city WHERE province_code = '11' ORDER BY is_hot DESC, code ASC; SELECT code, name FROM district WHERE city_code = '1101' ORDER BY code ASC; -- 写法二:一次查出所有层级,前端做缓存 SELECT p.code AS p_code, p.name AS p_name, c.code AS c_code, c.name AS c_name, d.code AS d_code, d.name AS d_name FROM province p LEFT JOIN city c ON c.province_code = p.code LEFT JOIN district d ON d.city_code = c.code WHERE p.code = '11' ORDER BY c.is_hot DESC, c.code ASC, d.code ASC; -- 写法三:用编码前缀模糊查询,适合已知编码的场景 SELECT code, name FROM district WHERE code LIKE '1101%' ORDER BY code ASC;逻辑说明:写法一适合前端按需加载,每次只查下一级,网络请求多但单次数据量小。写法二适合一次性把某个省的所有数据拉到前端缓存,后续切换城市不再请求后端,适合 Web 端。写法三适合已知编码前缀的场景,比如从 URL 参数里拿到了城市编码,直接查区县。参数上,ORDER BY is_hot DESC让热门城市排前面,code ASC保证行政区划顺序稳定。
3.2 用 Redis 缓存省市区数据:键设计比数据结构更重要
省市区数据的特点是读多写少,变更频率极低,非常适合缓存。我一般会把整个省份列表和每个省下的城市列表都塞进 Redis,键的设计直接决定查询效率。
import redis import json r = redis.Redis(host='localhost', port=6379, db=0) # 缓存省份列表,键名固定 provinces = [ {"code": "11", "name": "某直辖市", "short_name": "京"}, {"code": "31", "name": "某沿海省份", "short_name": "沪"} ] r.set('region:provinces', json.dumps(provinces, ensure_ascii=False)) # 缓存每个省下的城市列表,键名带省份编码 cities = [ {"code": "1101", "name": "某市", "is_hot": 1}, {"code": "1102", "name": "某地级市", "is_hot": 0} ] r.set('region:cities:11', json.dumps(cities, ensure_ascii=False)) # 查询时先查缓存,未命中再查数据库 def get_cities(province_code): key = f'region:cities:{province_code}' data = r.get(key) if data: return json.loads(data) # 回源数据库 cursor.execute("SELECT code, name, is_hot FROM city WHERE province_code = %s", (province_code,)) rows = cursor.fetchall() r.set(key, json.dumps(rows, ensure_ascii=False), ex=86400) return rows逻辑说明:键名用region:cities:{province_code}的格式,冒号分隔层级,方便批量管理和监控。ex=86400设置 24 小时过期,防止数据更新后缓存长期不失效。ensure_ascii=False保证中文正常存储,不然 Redis 里会变成一堆转义字符。注意,如果行政区划发生变更,需要主动删除对应的缓存键,或者用版本号做键前缀。
3.3 分页与模糊搜索:别让“全量返回”拖垮接口
有些场景需要搜索城市名,比如用户输入“南”字,要匹配出所有包含“南”的城市。这时候如果直接LIKE '%南%',在几万条数据里会全表扫描。我的做法是给name字段加一个前缀索引,或者单独建一张搜索表。
-- 给城市名加前缀索引,加速 LIKE '南%' 查询 ALTER TABLE city ADD INDEX idx_name_prefix (name(10)); -- 如果必须用 LIKE '%南%',建议限制返回条数 SELECT code, name FROM city WHERE name LIKE '%南%' LIMIT 20;逻辑说明:前缀索引只对LIKE '南%'这种左匹配有效,对LIKE '%南%'无效。所以如果业务允许,尽量引导用户从左到右输入。如果必须做全文模糊匹配,建议把数据同步到搜索引擎,或者用 MySQL 的全文索引(需要调整ngram参数)。LIMIT 20是兜底策略,防止一次返回几千条把接口拖死。
4. 避坑指南:省市区数据表最容易翻车的五个地方
4.1 编码不统一导致外键关联失败
现象:导入数据后,city表里有些记录的province_code是11,有些是110000,导致外键约束报错或者关联查询查不出数据。原因:不同来源的 SQL 文件编码格式不一致,有的用两位省码,有的用六位完整码。解决:导入前统一用SUBSTRING函数截取,或者写一个清洗脚本把province_code统一成两位。
-- 清洗:把六位省码截成两位 UPDATE city SET province_code = SUBSTRING(province_code, 1, 2) WHERE LENGTH(province_code) = 6;4.2 直辖市层级处理不当
现象:前端下拉框里,某直辖市下面直接跟了区,没有“市”这一级,导致用户选完省之后不知道选什么。原因:直辖市的行政层级是“省—区”,没有地级市。解决:在city表里为直辖市虚拟一个“市辖区”记录,编码用1101,这样三级联动逻辑就能统一。
4.3 数据版本过旧导致新设区县缺失
现象:用户反馈选不到某个新设的区,或者选到的区已经撤销了。原因:用的 SQL 文件是几年前打包的,没有跟进最新的行政区划调整。解决:定期从官方渠道同步数据,或者在表里加is_active字段,把撤销的区标记为失效而不是直接删除,保证历史订单还能关联到旧数据。
4.4 字符集问题导致生僻字乱码
现象:某些地名在数据库里显示为问号或者乱码。原因:建表时用了utf8而不是utf8mb4,utf8最多只支持 3 字节,而生僻字需要 4 字节。解决:建表和连接字符串都统一用utf8mb4,并且检查 MySQL 的character_set_server参数。
4.5 排序字段缺失导致下拉框顺序混乱
现象:前端下拉框里,省份顺序每次刷新都不一样,用户找不到想要的省。原因:查询时没有ORDER BY,MySQL 默认按主键或者存储引擎的物理顺序返回。解决:给province表加sort_order字段,按行政区划顺序或者拼音顺序预设值,查询时强制ORDER BY sort_order ASC。
5. 进阶技巧:用存储过程做数据校验与自动补全
5.1 写一个校验编码合法性的存储过程
数据导入后,最怕的是编码格式不对。我一般会写一个存储过程,批量检查city表的province_code是否都能在province表里找到对应记录。
DELIMITER // CREATE PROCEDURE check_region_integrity() BEGIN DECLARE done INT DEFAULT 0; DECLARE v_city_code CHAR(4); DECLARE v_province_code CHAR(2); DECLARE cur CURSOR FOR SELECT code, province_code FROM city; DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1; OPEN cur; read_loop: LOOP FETCH cur INTO v_city_code, v_province_code; IF done THEN LEAVE read_loop; END IF; IF NOT EXISTS (SELECT 1 FROM province WHERE code = v_province_code) THEN SELECT CONCAT('城市 ', v_city_code, ' 的省份编码 ', v_province_code, ' 不存在') AS error_msg; END IF; END LOOP; CLOSE cur; END // DELIMITER ; -- 调用 CALL check_region_integrity();逻辑说明:游标遍历city表,逐条检查province_code是否在province表里存在。如果不存在,输出错误信息。这个存储过程适合在数据导入后跑一次,确保没有孤儿记录。参数上,CONTINUE HANDLER FOR NOT FOUND是游标结束的标准写法,read_loop是自定义的循环标签。
5.2 用触发器自动维护更新时间
如果数据会定期同步,建议加一个updated_at字段,并用触发器自动更新。
ALTER TABLE city ADD COLUMN updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP;这样每次更新记录,updated_at都会自动刷新,方便排查数据是什么时候变更的。
5.3 导出与迁移:用 mysqldump 只导数据不导结构
有时候你只需要把数据迁移到另一个环境,不想覆盖表结构。可以用--no-create-info参数。
mysqldump -u root -p --no-create-info --complete-insert your_database province city district > region_data_only.sql逻辑说明:--no-create-info表示不导出建表语句,--complete-insert表示导出的 INSERT 语句包含字段名,这样即使目标表字段顺序不同也能正确导入。迁移前记得在目标库先建好表结构。
5.4 一个我踩过的坑:别在事务里做全量导入
有一次我在一个事务里批量插入几万条区县数据,结果事务日志暴涨,直接把磁盘写满了。后来改成每 1000 条提交一次,问题解决。所以如果你用脚本导入,记得分批提交,别一个事务包到底。
# 分批提交示例 batch_size = 1000 for i in range(0, len(rows), batch_size): batch = rows[i:i+batch_size] cursor.executemany("INSERT INTO district (code, city_code, name) VALUES (%s, %s, %s)", batch) conn.commit()这个习惯帮我省了好几次“后悔药”。数据导入看起来简单,但批量操作的资源消耗往往被低估。希望帮到你。
本文还有配套的精品资源,点击获取