简介:这是一份面向后端开发、数据分析与地图应用开发者的中国省市区城市列表MySQL数据表,以SQL脚本形式提供,可直接导入数据库,也可转换为JSON数据使用。数据覆盖全国省份、城市与区县,包含行政区划码和经纬度信息,适合地图服务、物流配送、电商收货地址自动补全、位置服务及地区经济与人口数据分析等场景。压缩包内共1个文件,为ChinaCity.sql脚本,整体约103KB,导入后即可生成结构完整的城市数据表,省去手工录入的繁琐工作。目前已有1928人学习下载,说明其在同类资源中具备一定参考价值。借助行政区划码与经纬度字段,读者可快速完成地区查询、距离计算与地图展示,为地理相关项目提供可靠的基础数据支撑。
1. 中国省市区城市列表 MySQL 数据表:一份能直接导入、还能转 JSON 的行政区划底表
做后端或数据开发的人,几乎都绕不开一张表:省市区城市列表。用户注册要选地区、订单要记收货地址、门店要按行政区统计、地图要按经纬度打点,最后都会落到同一件事上——你得有一份带行政区划码和经纬度的中国省市区数据,并且能直接导入 MySQL,最好还能顺手转成 JSON 喂给前端或接口。标题里这几个词其实是一条完整链路:MySQL 数据表负责存储和查询,行政区划码负责唯一标识和层级关联,经纬度负责定位和距离计算,JSON 负责跨端传输。这篇不聊虚的,就按一线做法把这份数据从建表、导入、校验到转 JSON、再到实际查询优化走一遍,新手能照着敲,熟手能对着参数和坑位做取舍。适合正在搭地区选择器、做地址库、搞数据同步或准备把 Excel 导入数据库的人。
2. 行政区划码和经纬度到底怎么设计表结构
2.1 为什么行政区划码不能当普通字符串随便存
行政区划码(也叫行政区划代码)是这份数据的骨架。国家标准里省级是 6 位,比如 110000 是北京市,前两位代表省,中间两位代表市,后两位代表区县。很多人第一次建表时直接varchar(20)一存了事,结果查询时where code like '11%'全表扫描,几万行数据还能忍,一旦和订单表关联就翻车。正确做法是把 code 定成char(6)或char(12),定长比变长在索引里更省空间,同时保留前导零——注意,如果你用整型存,110000没问题,但区县级像110101也还好,可一旦有010000这种前导零的码就会丢零,所以行政区划码一律用字符型,这是血泪经验。
层级关系有两种常见建模:邻接表(parent_code 指向父级)和路径枚举(把 110000/110100/110101 存成一列)。邻接表写入简单、查询要递归;路径枚举查询快、写入稍麻烦。做地区选择器这种「省-市-区」三级固定结构,我一般直接加level字段(1 省 2 市 3 区)加parent_code,三级查询用两次 join 就够,不必上递归 CTE。
2.2 经纬度字段选 decimal 还是 double
经纬度精度直接决定定位准不准。float 只有 7 位有效数字,存到小数点后 6 位就开始飘,算距离时误差能到几十米;double 有 15 位有效数字,够用但占 8 字节。真正稳妥的是decimal(10,6):经度范围 -180 到 180,纬度 -90 到 90,小数点后 6 位约等于 0.11 米精度,完全够城市级定位。注意经纬度顺序,国内很多数据源给的是「纬度,经度」,而 GeoJSON 和多数地图 API 要的是「经度,纬度」,导入前一定核对,否则会出现「北京跑到河北」这种玄学问题。
下面是一份可直接执行的建表语句,字段覆盖行政区划码、层级、经纬度和常用扩展:
CREATE TABLE `cn_region` ( `id` INT UNSIGNED NOT NULL AUTO_INCREMENT COMMENT '自增主键', `code` CHAR(6) NOT NULL COMMENT '行政区划码,6位定长,保留前导零', `name` VARCHAR(60) NOT NULL COMMENT '地区名称', `short_name` VARCHAR(30) DEFAULT NULL COMMENT '简称,如“京”', `parent_code` CHAR(6) DEFAULT NULL COMMENT '父级行政区划码,省级为 NULL', `level` TINYINT UNSIGNED NOT NULL COMMENT '层级:1省 2市 3区县', `lng` DECIMAL(10,6) DEFAULT NULL COMMENT '经度,小数点后6位', `lat` DECIMAL(10,6) DEFAULT NULL COMMENT '纬度,小数点后6位', `pinyin` VARCHAR(60) DEFAULT NULL COMMENT '拼音,便于搜索', `sort` SMALLINT UNSIGNED DEFAULT 0 COMMENT '排序权重', PRIMARY KEY (`id`), UNIQUE KEY `uk_code` (`code`), KEY `idx_parent` (`parent_code`), KEY `idx_level` (`level`), KEY `idx_name` (`name`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='中国省市区城市列表';逻辑说明:code用CHAR(6)定长并加唯一索引,保证行政区划码不重复;parent_code加普通索引,因为按父级查子级是最频繁的操作;level单独索引,方便只取省级或只取市级;name加索引是为了支持按名称模糊搜索。参数上,utf8mb4是必须的,因为地区名里可能有生僻字,utf8三字节存不下。sort字段给直辖市、省会加权,前端下拉框排序时不用再写复杂逻辑。
提示:如果你的数据源里区县码是 9 位或 12 位(比如街道级),把
code改成CHAR(12),同时parent_code同步加长,别只改一个字段导致关联断裂。
3. 把 Excel 或 CSV 导入 MySQL 的完整操作
3.1 导入前先做数据清洗和格式对齐
拿到手的省市区数据通常是 Excel 或 CSV,常见问题是:表头有合并单元格、经纬度是文本、行政区划码被 Excel 当成数字丢了前导零。导入前先统一成 CSV,UTF-8 编码,字段顺序固定为 code,name,parent_code,level,lng,lat。用 Python 做一次清洗最稳,pandas 读进来后强制把 code 转成字符串并补零:
import pandas as pd # 读取原始 Excel,dtype 指定 code 为字符串,避免丢前导零 df = pd.read_excel('region_raw.xlsx', dtype={'code': str, 'parent_code': str}) # 补零到 6 位,处理 Excel 把 110000 读成 110000.0 的情况 df['code'] = df['code'].str.replace(r'\.0$', '', regex=True).str.zfill(6) df['parent_code'] = df['parent_code'].str.replace(r'\.0$', '', regex=True).str.zfill(6) # 经纬度转数值,非法值置空 df['lng'] = pd.to_numeric(df['lng'], errors='coerce') df['lat'] = pd.to_numeric(df['lat'], errors='coerce') # 按层级补 parent_code:省级为空 df.loc[df['level'] == 1, 'parent_code'] = None df.to_csv('region_clean.csv', index=False, encoding='utf-8-sig') print('清洗完成,共', len(df), '条')逻辑说明:dtype={'code': str}是关键,否则 pandas 会把行政区划码读成整数;str.replace(r'\.0$', '')处理 Excel 导出的浮点尾巴;zfill(6)补齐前导零。参数上errors='coerce'让非法经纬度变成 NaN,导入 MySQL 时自动为 NULL,避免脏数据污染。utf-8-sig带 BOM,方便 Excel 二次打开不乱码。
3.2 用 LOAD DATA 批量导入并校验行数
清洗完的 CSV 用 MySQL 的LOAD DATA LOCAL INFILE导入,比逐条 insert 快几十倍。先确认 MySQL 开了local_infile:
SHOW VARIABLES LIKE 'local_infile'; SET GLOBAL local_infile = 1;然后执行导入,注意字段顺序和 CSV 对齐,空值用\N表示:
LOAD DATA LOCAL INFILE '/path/region_clean.csv' INTO TABLE cn_region CHARACTER SET utf8mb4 FIELDS TERMINATED BY ',' OPTIONALLY ENCLOSED BY '"' LINES TERMINATED BY '\n' IGNORE 1 LINES (code, name, parent_code, level, lng, lat) SET id = NULL;逻辑说明:IGNORE 1 LINES跳过表头;OPTIONALLY ENCLOSED BY '"'处理名称里带逗号的情况;SET id = NULL让自增主键重新生成。导入后必须校验:
SELECT level, COUNT(*) FROM cn_region GROUP BY level; SELECT COUNT(*) FROM cn_region WHERE code IS NULL OR name IS NULL; SELECT * FROM cn_region WHERE parent_code IS NOT NULL AND parent_code NOT IN (SELECT code FROM cn_region);第一条看省市区数量是否符合预期(通常省 34、市 300 多、区县 3000 左右);第二条查空值;第三条查孤儿节点,即父级码在表里找不到,这种数据会导致前端级联选择断链。发现孤儿节点要么补父级,要么把 parent_code 置空并降级处理。
注意:
LOAD DATA LOCAL INFILE在部分 MySQL 8.0 客户端默认被禁用,报The used command is not allowed时,除了服务端local_infile=1,连接串还要加allowLoadLocalInfile=true,这是 docker 安装 mysql 后最常踩的坑。
4. 转 JSON 的几种姿势和接口设计
4.1 用 SQL 直接拼 JSON 还是应用层组装
MySQL 5.7 以后自带 JSON 函数,可以直接在 SQL 里拼出层级结构。简单场景用JSON_OBJECT和JSON_ARRAYAGG:
SELECT JSON_ARRAYAGG( JSON_OBJECT('code', p.code, 'name', p.name, 'cities', ( SELECT JSON_ARRAYAGG( JSON_OBJECT('code', c.code, 'name', c.name, 'districts', ( SELECT JSON_ARRAYAGG(JSON_OBJECT('code', d.code, 'name', d.name)) FROM cn_region d WHERE d.parent_code = c.code )) ) FROM cn_region c WHERE c.parent_code = p.code )) ) AS region_json FROM cn_region p WHERE p.level = 1;逻辑说明:外层按省级聚合,内层子查询分别聚合市和区县,JSON_ARRAYAGG把多行压成数组。参数上要注意,子查询返回空时JSON_ARRAYAGG会给null,前端遍历要判空。这种写法适合一次性导出静态 JSON 文件,不适合高并发接口,因为三层嵌套子查询在数据量大时性能一般。
更常见的做法是应用层组装:一次查出全部数据,在内存里按 parent_code 建树,再序列化成 JSON。这样只查一次库,缓存友好。用 Python 示例:
import json import pymysql conn = pymysql.connect(host='127.0.0.1', user='root', password='***', database='test', charset='utf8mb4') cur = conn.cursor(pymysql.cursors.DictCursor) cur.execute('SELECT code,name,parent_code,level,lng,lat FROM cn_region ORDER BY sort,code') rows = cur.fetchall() # 先按 code 建索引,再挂子节点 node_map = {r['code']: {**r, 'children': []} for r in rows} tree = [] for r in rows: node = node_map[r['code']] if r['parent_code'] and r['parent_code'] in node_map: node_map[r['parent_code']]['children'].append(node) else: tree.append(node) with open('region.json', 'w', encoding='utf-8') as f: json.dump(tree, f, ensure_ascii=False, indent=2) print('导出完成,省级节点', len(tree))逻辑说明:node_map用行政区划码做键,一次遍历完成挂载,时间复杂度 O(n)。ensure_ascii=False保证中文正常输出,indent=2便于人工检查。参数上ORDER BY sort,code让输出顺序稳定,避免每次导出 JSON 顺序不同导致前端缓存失效。
4.2 前端拿 JSON 后怎么用才不卡
前端拿到这份 JSON 通常做级联选择器。数据量在 3000 条左右、层级三层时,整包 JSON 大约 300KB 到 500KB,gzip 后 100KB 以内,首屏加载可以接受。但如果每次打开页面都请求,就要考虑缓存。常见做法是给 JSON 加版本号或按行政区划码做增量,接口返回时带ETag。另外注意uni.getlocation拿到的经纬度是设备定位,和这份表里的城市中心经纬度不是一回事,别直接拿设备经纬度去匹配区县码,正确做法是用逆地理编码服务,或者按距离最近的城市中心点做粗匹配,再让用户手动确认。
提示:JSON 里经纬度建议保留 6 位小数,和数据库 decimal(10,6) 对齐,前端算距离时用 Haversine 公式,别用平面直角坐标,跨纬度误差会很大。
5. 避坑与排查:导入、查询、转 JSON 的 5 个真实翻车点
5.1 导入后中文变问号
现象:LOAD DATA成功后SELECT出来地区名全是???。原因:CSV 文件编码和连接字符集不一致,常见是 CSV 存成 GBK 而连接用 utf8mb4。解决:统一 CSV 为 UTF-8,导入语句里显式写CHARACTER SET utf8mb4,连接串加charset=utf8mb4,三处必须一致。
5.2 行政区划码前导零丢失
现象:区县码010101变成10101,和父级关联不上。原因:Excel 或 pandas 把 code 当数字读。解决:读取时强制dtype=str,导入 MySQL 用CHAR而非INT,如果已经导入错了,用LPAD(code, 6, '0')批量修复。
5.3 经纬度顺序颠倒导致定位偏移
现象:地图上北京的点落在河北。原因:数据源是「纬度,经度」,导入时没换序。解决:导入前统一成「经度,纬度」,校验方法很简单,取北京的数据看 lng 是否约 116、lat 是否约 39,反了就是顺序问题。
5.4 级联查询慢到超时
现象:前端选省后查市、选市后查区,接口响应超过 1 秒。原因:parent_code没索引,或者每次查询都全表扫描。解决:给parent_code加索引,省级数据可以整体缓存到 Redis,key 用region:children:{parent_code},过期时间设长一点,行政区划变动频率很低。
5.5 JSON 导出后前端解析报错
现象:JSON.parse抛异常。原因:SQL 拼 JSON 时字段里有换行或引号没转义,或者JSON_ARRAYAGG返回了null导致结构不合法。解决:应用层组装更可控,导出后用json.loads自检一遍,接口返回前做一次序列化验证,别把脏 JSON 直接扔给前端。
6. 进阶:用行政区划码做数据同步和增量更新
这份表不是导入一次就完事。行政区划每年都有调整,撤县设区、合并乡镇都会改码。做数据同步时,别整表删了重导,用行政区划码做主键做 upsert 更稳。MySQL 里用INSERT ... ON DUPLICATE KEY UPDATE:
INSERT INTO cn_region (code, name, parent_code, level, lng, lat, sort) VALUES ('110101', '东城区', '110100', 3, 116.416334, 39.928359, 10) ON DUPLICATE KEY UPDATE name = VALUES(name), parent_code = VALUES(parent_code), lng = VALUES(lng), lat = VALUES(lat), sort = VALUES(sort);逻辑说明:code上有唯一索引,新码插入、旧码更新,不会产生重复。参数上VALUES()引用的是待插入值,MySQL 8.0.20 以后推荐用别名写法,但VALUES()兼容性更好。增量更新时,先拉一份最新数据,和库里比对 code 集合,新增的插入、消失的标记is_active=0而不是物理删除,避免历史订单关联的地区查不到。
验证更新是否成功,用这条 SQL 查差异:
SELECT a.code, a.name AS old_name, b.name AS new_name FROM cn_region a JOIN region_new b ON a.code = b.code WHERE a.name <> b.name OR a.lng <> b.lng OR a.lat <> b.lat;我自己维护这份表时养成的习惯是:每次更新前先mysqldump备份,更新后跑一遍层级完整性校验,确认没有孤儿节点再发布 JSON。行政区划码是这份数据的后悔药,只要码对,名称和经纬度都能修;码错了,整条链路都得重来。希望帮到你。
本文还有配套的精品资源,点击获取