地址下拉出现「市辖区」「县」:GB/T 2260 占位词污染数据库的三层防线
某天做门店管理后台的城市筛选时,发现一个诡异现象:重庆市下面并列着「重庆市」「市辖区」「县」三个选项,天津市下面只有孤零零的「市辖区」,北京和上海也好不到哪去。前端同学第一反应:地址库是不是给了脏数据?排查之后发现,脏数据确实存在——但根源不在前端,而在 GB/T 2260 这份国家标准本身。
现象
省市区三级联动的城市层,GROUP BY city 之后,直辖市下面出现了额外的分组:
重庆市
├── 重庆市 (8 条门店)
├── 市辖区 (105 条门店) ← 幽灵分组
└── 县 (3 条门店) ← 幽灵分组
天津市
└── 市辖区 (4 条门店) ← 正常门店全被归到这里
根因
中国行政区划是四级:省 → 地级市 → 县 / 区 → 乡镇。但北京、上海、天津、重庆四个直辖市直接管辖区县,中间没有地级市这一层。
GB/T 2260(国家行政区划代码标准)为了保持数据结构统一,在直辖市的「地级市」位置填入了占位词:
| 占位词 | 语义 |
|--------|------|
| 市辖区 | 该直辖市下辖的各城区 |
| 县 | 该直辖市下辖的县 |
| 市辖县 | 较老的写法,同上 |
| 省直辖县级行政区划 | 省直管县(如湖北省神农架等) |
这些词本是层级槽的填充符,不是真实地名。但很多 POS 机、CRM、第三方地址库在导出数据时会把它们原样写入 city 字段。进了数据库,城市列就被污染了。
坑:只在前端过滤治标不治本
看到这个现象,直觉反应是在前端联动选项里 filter 掉这些词:
const placeholders = ['市辖区', '县', '市辖县', '省直辖县级行政区划'];
cities = cities.filter(c => !placeholders.includes(c.name));这么做有三个隐患:
- Excel 导出、BI 报表、其他后台消费点仍然读到脏数据
- 后续同步/导入脏数据时会继续污染,前端过滤只是掩盖
- 按城市统计门店数永远是错的——同一批门店被拆进了多个分组
正解
必须在存储、写入、读取三层同时封堵,缺一不可。
第一层:存量 DML 清洗
-- 直辖市:把占位词折叠回省名
UPDATE store_info
SET city = province
WHERE province IN ('北京市', '上海市', '天津市', '重庆市')
AND city IN ('市辖区', '县', '市辖县', '省直辖县级行政区划');
-- 省直管县兜底
UPDATE store_info
SET city = province
WHERE city IN ('市辖区', '县', '市辖县', '省直辖县级行政区划');
-- 验证:应返回 0 行
SELECT COUNT(*) FROM store_info
WHERE city IN ('市辖区', '县', '市辖县', '省直辖县级行政区划');第二层:写入侧拦截(同步/导入接口边界)
$placeholders = ['市辖区', '县', '市辖县', '省直辖县级行政区划'];
$province = $data['province'] ?? '';
if (in_array($data['city'] ?? '', $placeholders, true) && $province !== '') {
$data['city'] = $province; // 占位词 → 省名
}第三层:读取侧归并(不要用 SQL GROUP BY city)
// SQL 只 SELECT province, city,不做 GROUP BY
$rows = $model->field('province, city')->select();
$grouped = [];
$placeholders = ['市辖区', '县', '市辖县', '省直辖县级行政区划'];
foreach ($rows as $r) {
// 应用层归并,确保 (重庆市, 市辖区) 和 (重庆市, 重庆市) 合并
$city = in_array($r['city'], $placeholders, true)
? $r['province']
: $r['city'];
$grouped[$r['province']][$city] = ($grouped[$r['province']][$city] ?? 0) + 1;
}若直接在 SQL 里 GROUP BY city,(重庆市, 重庆市) 和 (重庆市, 市辖区) 会被当成两组,归并逻辑失效。
正解:三层同时封堵,占位词折叠回省名
- 存量:DML 一次性清洗,验证归零
- 增量写入:在接口边界把占位词替换为省名,阻断再污染
- 读取:应用层归并,不依赖 SQL
GROUP BY
三层缺任何一层,另外两层的修复都会因为"漏网之鱼"逐渐失效。
一句话外卖
GB/T 2260 的「市辖区」「县」是行政层级的占位符,不是城市名——进库前折叠成省名,写入、读取、存量三层同时封堵,缺一不可。