☰
四级城市地区表xlsx转sql:联动ID、层级与末级字段实战
2026/10/9 13:50:01 网站建设 项目流程

简介:这份资源面向需要实现省、市、县、街道乡镇四级地址联动的开发者,尤其适合做电商收货地址、后台管理系统、表单级联选择等场景的前后端与数据库人员。包内提供 xlsx 与 sql 两种格式的四级地区数据,字段包含 ID、父 ID、名称、联动 ID、层级以及是否末级标记,可直接导入数据库或读取表格使用。压缩包共 6 个文件,以 1 个 xlsx 数据表、1 个 sql 建表与数据脚本为主,另附 3 张 png 预览图和 1 个 txt 说明,整体约 2.48MB,体积轻便便于快速集成。数据按层级组织,联动 ID 采用逐级拼接形式,末级字段可辅助判断街道乡镇节点,减少前端递归判断成本。目前已有 604 人学习下载,适合需要快速搭建国内四级地址联动、又不想自行采集整理数据的开发者参考使用。

1. 四级城市地区表:从一张 xlsx 到能跑联动的 sql 文件

做后端或数据中台的同学,大概率都接过「省市区街道四级联动」这种需求。产品经理一句「下拉框要能选到街道」,背后就是一张覆盖全国、层级清晰、ID 能对上的四级地区表。我见过太多团队在这件事上翻车:有人从网上随便下了份 xlsx,导入后发现「市辖区」重复了几百条;有人拿到的 sql 文件里 parent_id 全是空的,联动直接断链;还有人做到一半才发现数据只到区县,街道那一级压根没有。这篇笔记就围绕「四级城市地区表 xlsx、sql 文件、名称、联动 ID、层级、是否末级」这几个关键词,把选型、清洗、入库、联动查询的完整路径讲清楚。适合正在做地址模块的后端、做数据初始化的运维,以及需要给前端提供联动接口的全栈。读完你能自己判断一份地区表能不能用,也能把 xlsx 转成结构干净的 sql 并跑通四级联动。

2. 四级地区表的结构设计与字段取舍

2.1 为什么是「名称 + 联动 ID + 层级 + 是否末级」这四个字段

一份能用的四级地区表,核心不是数据量,而是字段设计能不能支撑联动查询和业务扩展。标题里点名的四个字段,其实对应了四类需求:名称用于展示,联动 ID 用于前后端传参和父子关联,层级用于判断当前处于省、市、县还是街道,是否末级用于告诉前端「这一级选完还要不要继续加载下级」。

我一般会把表设计成自关联结构,而不是四张独立的表。原因是四级联动本质是一棵树,用 parent_id 自关联,查询下级只需要一个 where 条件,扩展第五级也不用改表结构。如果拆成 province、city、district、street 四张表,联表查询会越来越重,而且层级判断逻辑会散落在代码各处。

字段名类型说明是否必填
idbigint联动 ID,全局唯一,建议用业务编码而非自增是
namevarchar(64)地区名称,如「某省」「某县」是
parent_idbigint父级联动 ID,顶级为 0是
leveltinyint层级,1 省 2 市 3 县 4 街道是
is_leaftinyint是否末级,1 是 0 否是
codevarchar(12)行政区划编码,用于对接外部系统否

这里有个容易忽略的点:联动 ID 和行政区划编码不是一回事。行政区划编码会随区划调整变化,而联动 ID 一旦被业务引用就不该变。常见做法是联动 ID 直接用行政区划编码,但要在文档里写清楚「编码变更时如何迁移」,否则后期区划调整会让历史订单的地址对不上。

2.2 层级和是否末级到底怎么定

层级字段看起来简单,实际最容易出错。省是 1、市是 2、县是 3、街道是 4,这个没问题。但直辖市怎么算?某市的「市辖区」算市还是算县?我的处理方式是:直辖市仍然按省、市、县、街道四级展开,把直辖市本身当作省级,其下的区当作市级,再往下才是县级和街道。这样层级判断逻辑统一,前端不用为直辖市写特例。

是否末级字段的作用是减少一次无意义的请求。前端拿到某个节点后,如果 is_leaf=1,就知道不用再去请求下级列表,直接结束联动。判断规则很简单:街道级一律是末级;如果某个县级下面没有街道数据,那这个县级也要标记为末级,否则前端会一直转圈等一个空列表。

提示:不要用「有没有子节点」在运行时动态判断是否末级,那样每次都要多查一次。初始化时算好写进字段,查询时直接读。

2.3 数据来源与 xlsx 的常见坑

网上流传的四级地区表 xlsx 来源很杂,质量参差不齐。我拿到一份 xlsx 后,第一件事不是导入,而是先做三项检查:行数是否在合理区间、名称里有没有明显重复、parent_id 能不能串成一棵完整的树。常见问题是「市辖区」这种名称在多个市下面重复出现,如果只按名称关联就会串数据,所以必须用联动 ID 关联。

另一个坑是 xlsx 里的层级列可能是文本「省」「市」而不是数字,导入数据库前要统一映射。还有的 xlsx 把街道和乡镇混在一起,名称里带「街道」「镇」「乡」后缀,这个不影响使用,但做搜索时要考虑分词。

3. 把 xlsx 清洗成可入库的 sql 文件

3.1 用 Python 读取 xlsx 并做基础校验

清洗的第一步是把 xlsx 读进来,做结构校验。我一般用 pandas,因为它处理缺失值和类型转换比较顺手。下面这段代码读取 xlsx,检查必需列是否存在,并统计各层级数量。

import pandas as pd # 读取 xlsx,指定 sheet 名,避免读到说明页 df = pd.read_excel("region_four_level.xlsx", sheet_name="data", dtype=str) # 必需列校验,缺一列就直接报错,不要带着问题往下走 required_cols = ["id", "name", "parent_id", "level", "is_leaf"] missing = [c for c in required_cols if c not in df.columns] if missing: raise ValueError(f"缺少必需列: {missing}") # 去空白,xlsx 里经常有看不见的空格导致关联失败 for col in ["id", "name", "parent_id", "level", "is_leaf"]: df[col] = df[col].astype(str).str.strip() # 层级分布统计,用来判断数据是否完整 print(df["level"].value_counts().sort_index())

这段代码的关键在 dtype=str。如果让 pandas 自动推断类型,像「01」这样的编码会被转成数字 1,前导零丢失,后面和外部系统对接就对不上。强制按字符串读,再在入库时转成目标类型,是更稳的做法。层级统计那行用来快速判断数据完整性:正常全国数据省级约 30 多、市级 300 多、县级 2800 多、街道 4 万左右,数量级明显不对就说明数据缺级。

3.2 校验父子关系和末级标记

读进来之后,要验证 parent_id 能不能串成树。核心检查两项:除顶级外每个 parent_id 都能在 id 列里找到;没有环形引用。同时根据子节点情况重新计算 is_leaf,不信任原始文件里的标记。

id_set = set(df["id"]) # 检查孤儿节点:parent_id 不在 id 集合里,且不是顶级 0 orphans = df[(df["parent_id"] != "0") & (~df["parent_id"].isin(id_set))] if not orphans.empty: print(f"发现 {len(orphans)} 个孤儿节点,示例:") print(orphans[["id", "name", "parent_id"]].head()) # 重新计算 is_leaf:有子节点的都不是末级 parent_ids = set(df[df["parent_id"] != "0"]["parent_id"]) df["is_leaf"] = df["id"].apply(lambda x: "0" if x in parent_ids else "1") # 街道级强制末级,防止数据里街道还挂着子节点 df.loc[df["level"] == "4", "is_leaf"] = "1"

孤儿节点是最常见的脏数据,通常是因为 xlsx 里某几行的父级 ID 写错了,或者父级本身被漏掉了。发现孤儿节点不要直接删,先看数量:如果只有几条,可能是数据录入错误,可以人工修正;如果成百上千,说明这份数据本身不完整,建议换一份来源。重新计算 is_leaf 比信任原始标记可靠,因为原始文件里的末级标记经常是错的。

3.3 生成 sql 文件与批量插入语句

清洗完就可以生成 sql 文件了。我一般生成两种:建表语句和批量插入语句。批量插入用多值 INSERT,比一行一条快很多,但要注意单条语句长度限制,通常每 500 到 1000 行拆一条。

def escape(s): # 单引号转义,防止名称里的引号破坏 sql return s.replace("\\", "\\\\").replace("'", "\\'") lines = [] lines.append("SET NAMES utf8mb4;") lines.append("START TRANSACTION;") batch_size = 500 rows = df[["id", "name", "parent_id", "level", "is_leaf"]].values.tolist() for i in range(0, len(rows), batch_size): batch = rows[i:i + batch_size] values = ",".join( f"({r[0]},'{escape(r[1])}',{r[2]},{r[3]},{r[4]})" for r in batch ) lines.append( "INSERT INTO region (id,name,parent_id,level,is_leaf) VALUES " + values + ";" ) lines.append("COMMIT;") with open("region_four_level.sql", "w", encoding="utf-8") as f: f.write("\n".join(lines))

这里用事务包起来,是因为几万行插入如果中途失败,没有事务会留下半截数据,排查起来很痛苦。batch_size 设 500 是折中:太小则语句条数多、导入慢,太大则可能超过数据库的 max_allowed_packet 限制。名称转义不能省,有些地区名称里带单引号,不转义直接语法错误。

注意:生成 sql 后先在测试库跑一遍,确认行数和 xlsx 一致,再上生产。我吃过一次亏,xlsx 里有个隐藏 sheet 也被读进来,导致多插了几千条脏数据。

4. 入库后的四级联动查询与接口设计

4.1 建表与索引:让联动查询走索引

表结构要和生成的 sql 对齐,索引是重点。联动查询的模式很固定:按 parent_id 查下级,按 id 查详情。所以 parent_id 必须有索引,level 在按层级筛选时也用得上。

CREATE TABLE region ( id BIGINT NOT NULL COMMENT '联动ID', name VARCHAR(64) NOT NULL COMMENT '地区名称', parent_id BIGINT NOT NULL DEFAULT 0 COMMENT '父级联动ID', level TINYINT NOT NULL COMMENT '层级 1省2市3县4街道', is_leaf TINYINT NOT NULL DEFAULT 0 COMMENT '是否末级 1是0否', PRIMARY KEY (id), KEY idx_parent (parent_id), KEY idx_level (level) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='四级地区表';

主键用 id 而不是自增列,是因为联动 ID 本身就是业务主键,再加自增列反而多一层映射。idx_parent 是联动查询的核心索引,没有它,每次查下级都要全表扫描,几万行数据下接口响应会明显变慢。idx_level 用于按层级做统计或批量导出。

4.2 查询下级列表的接口写法

前端联动的核心接口是「给我某个节点的下级列表」。这个查询很简单,但有几个细节决定接口质量。

-- 查询某节点的直接下级,按 id 排序保证顺序稳定 SELECT id, name, level, is_leaf FROM region WHERE parent_id = ? ORDER BY id;

参数就是当前选中的联动 ID。返回里带上 is_leaf,前端拿到后如果全是末级,就可以禁用「继续选择」或者自动收起。排序用 id 而不是 name,是因为按名称排序在不同数据库、不同字符集下结果可能不一致,按 id 排序稳定且走主键。

如果要做「根据名称模糊搜索地区」,记得给 name 加前缀索引或者用全文索引,不要直接 like '%关键词%',那样索引失效,几万行下还能忍,数据再涨就顶不住。

4.3 前端联动的数据流与缓存策略

前端四级联动通常是级联选择器,每选一级请求一次下级。这个模式在弱网下体验很差,常见优化是首屏把省和市一次性拉下来,县和街道按需加载。缓存策略上,地区数据变更频率极低,可以在前端做本地缓存,甚至把整份数据打包成静态 json 随前端发布。

我一般建议后端提供两个接口:一个按 parent_id 查下级,用于动态加载;一个全量导出,用于前端本地缓存。全量数据几万条,压缩后体积可控,比每次请求都走网络更稳。判断用哪种,看你的地区数据更新频率和前端包体积容忍度。

5. 四级地区表落地避坑与排查清单

5.1 名称重复导致关联串数据

现象:联动选到某个县,下级却出现了别的市的街道。原因:清洗时用名称做关联,而「市辖区」「城关镇」这类名称在多个市、县下重复。解决:所有关联一律用联动 ID,名称只用于展示,任何时候不要拿名称当外键。

5.2 层级字段是文本导致判断失效

现象:前端判断 level === 3 时永远不成立,县级节点被当成市级。原因:xlsx 里层级列是「县」这样的文本,导入后没转成数字。解决:清洗阶段做映射,省→1、市→2、县→3、街道→4,入库前用断言检查 level 只包含 1 到 4。

5.3 末级标记错误导致前端一直加载

现象:选到某个街道后,前端还在请求下级,返回空列表,用户看到一直转圈。原因:is_leaf 标记为 0,但实际没有子节点。解决:入库前按「是否有子节点」重新计算 is_leaf,街道级强制置 1,不要信任原始文件。

5.4 批量插入超长导致导入中断

现象:sql 文件导入到一半报错,前面数据进去了,后面没有。原因:单条 INSERT 拼接行数太多,超过 max_allowed_packet。解决:每 500 行拆一条,用事务包裹,失败可整体回滚。导入前先看数据库的 max_allowed_packet 配置。

5.5 区划调整后历史数据对不上

现象:某地区改名或合并后,历史订单里的地址显示异常。原因:联动 ID 直接用了行政区划编码,编码变更后旧数据找不到对应记录。解决:联动 ID 与行政区划编码分离,编码变更时保留旧 ID 的映射关系,或者用软删除加别名的方式兼容历史数据。

6. 用递归查询一次拿到完整四级路径

前面讲的都是逐级查询,适合前端联动。但有些场景需要一次性拿到「省-市-县-街道」的完整路径,比如订单详情展示、地址解析、数据导出。这时候逐级查要发四次请求,用递归 CTE 一条 sql 就能搞定。

以 MySQL 8.0 为例,假设已知街道的 id,向上追溯完整路径:

WITH RECURSIVE path AS ( -- 锚点:从目标节点开始 SELECT id, name, parent_id, level FROM region WHERE id = ? UNION ALL -- 递归:向上找父级 SELECT r.id, r.name, r.parent_id, r.level FROM region r JOIN path p ON r.id = p.parent_id ) SELECT id, name, level FROM path ORDER BY level ASC;

这段查询从目标节点出发,不断向上找 parent_id,直到顶级。结果按 level 升序排列,就是完整的省到街道路径。参数只有一个,就是末级节点的联动 ID。递归 CTE 在 MySQL 8.0、PostgreSQL、SQL Server 上都支持,语法略有差异,MySQL 需要 WITH RECURSIVE 关键字。

如果数据库版本低不支持递归 CTE,退而求其次的做法是在应用层循环查询,或者冗余一个 path 字段,存「省ID/市ID/县ID/街道ID」这样的路径字符串。冗余 path 的代价是区划调整时要批量更新,但查询时一次就能拿到,适合读多写少的场景。

验证递归查询是否正确,有个简单办法:拿几个已知的街道 id 跑一遍,人工核对路径是否和预期一致。我一般会挑直辖市、普通地级市、以及有「市辖区」的县级各一个,覆盖边界情况。跑通之后,把这个查询封装成接口,订单详情页就不用再逐级查了。

最后说个我自己的习惯:每次拿到新的地区表,我都会先写一个校验脚本,把行数、层级分布、孤儿节点数、末级数量四个指标打出来,和上一版对比。指标对不上就不入库。这个习惯帮我拦下过好几次脏数据,比事后排查省事得多。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询