MySQL手机号归属地查询:本地号段表实现批量补全与性能优化 简介这份资源是一份基于MySQL的手机号码归属地数据包面向从事营销、客户服务、数据分析及数据库学习的开发者与运维人员用于解决按手机号快速查询省份、城市归属地的实际需求。压缩包内共1个文件为单个sql格式的数据脚本整体约2.22MB导入MySQL后即可通过结构化数据表完成号码与归属地的关联查询并支持分组统计、多表关联等进阶分析。目前已有268人学习下载数据完整度较高适合作为练习SQL查询、批量处理与隐私合规管理的实战素材。读者可借此掌握按号码检索归属地、统计各省用户数量等典型操作同时理解敏感数据的加密存储与访问权限控制思路对初学者和有一定经验的开发者都具备参考价值。1. 手机号归属地查询从一条 SQL 说起手里有一批用户注册数据手机号字段是明文运营要按地域做分层触达第一反应是调第三方 API。但量一大QPS 和费用都扛不住而且很多接口对历史号段覆盖不全。这时候一张本地的手机号归属地表就显出价值了——一次导入永久查询没有网络依赖没有调用次数限制。标题里说的“mysql非常全”核心就是这件事把手机号前七位和运营商、归属地、区号、邮编的映射关系落到 MySQL 里用一条SELECT换掉一次 HTTP 请求。适合谁手里有几十万到上千万条手机号需要批量补全归属地、又不想被 API 限流卡脖子的后端和数据分析同学。下面把我自己建表、导数据、写查询、做批量更新的完整路径拆开讲。2. 号段表怎么建字段设计和索引选择2.1 为什么用前七位而不是前三位手机号归属地的判定精度取决于号段粒度。早期只看前三位比如 138就能定位到运营商但归属地会粗到省份甚至多个省共用。真正能落到城市级的是前七位——1381234这种粒度。中国移动、联通、电信的号段分配就是按前七位划块的同一个前七位基本对应同一个地级市。所以表的主键或者唯一索引应该建在prefix前七位上而不是phone全号。全号存进去既浪费空间又没必要查询时用LEFT(phone, 7)截取即可。常见做法是单独存一列prefix CHAR(7)查询时先截取再等值匹配走索引速度是毫秒级。2.2 建表语句与字段含义CREATE TABLE phone_location ( prefix char(7) NOT NULL COMMENT 号段前七位, province varchar(20) NOT NULL DEFAULT COMMENT 省份, city varchar(30) NOT NULL DEFAULT COMMENT 城市, area_code varchar(10) NOT NULL DEFAULT COMMENT 区号, post_code varchar(10) NOT NULL DEFAULT COMMENT 邮编, carrier varchar(20) NOT NULL DEFAULT COMMENT 运营商, PRIMARY KEY (prefix), KEY idx_province (province), KEY idx_carrier (carrier) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT手机号归属地映射表;prefix做主键因为它是查询入口等值匹配最快。province和carrier建普通索引是为了支持“按省统计用户数”“按运营商分组”这类运营查询。area_code和post_code用字符串而不是数字因为区号有前导零比如 010存成整型会丢零。carrier字段存“移动”“联通”“电信”“虚拟运营商”这类值不要存英文缩写后续做报表时直接可读。字符集用utf8mb4防止某些生僻地名乱码。2.3 数据从哪来导入前的清洗要点标题说“淘宝50元买的”本质就是一份 CSV 或 Excel 号段表。拿到手别急着LOAD DATA先做三件事。第一检查前七位是否有重复重复的只保留一条否则主键冲突会中断导入。第二检查是否有空值或NULL尤其是province和city空值会导致后续查询结果为空但又不报错排查起来很费时间。第三统一运营商名称比如“中国移动”“移动”“CMCC”统一成“移动”。清洗可以用 Python 脚本快速过一遍import pandas as pd df pd.read_csv(raw_segments.csv, dtypestr) df[prefix] df[prefix].str.strip().str[:7] df df.drop_duplicates(subsetprefix, keepfirst) df[province] df[province].fillna(未知) df[city] df[city].fillna(未知) df[carrier] df[carrier].str.replace(中国, ).str.replace(CMCC, 移动) df.to_csv(clean_segments.csv, indexFalse)这段脚本做了去重、空值填充和运营商名称归一化。dtypestr很关键否则 pandas 会把前导零的区号读成数字。清洗完再导入能避免 90% 的导入报错。3. 把 CSV 灌进 MySQL三种导入方式与速度对比3.1 LOAD DATA INFILE 的用法和参数数据量在十万行以上时LOAD DATA是最快的比逐条INSERT快一个数量级。前提是 CSV 格式规整字段顺序和表结构一致。LOAD DATA LOCAL INFILE /path/to/clean_segments.csv INTO TABLE phone_location FIELDS TERMINATED BY , OPTIONALLY ENCLOSED BY LINES TERMINATED BY \n IGNORE 1 ROWS (prefix, province, city, area_code, post_code, carrier);FIELDS TERMINATED BY ,指定逗号分隔OPTIONALLY ENCLOSED BY 处理字段里带逗号的情况IGNORE 1 ROWS跳过表头。如果 CSV 是 Windows 换行LINES TERMINATED BY \r\n要改一下否则最后一行会带\r导致导入失败。LOCAL关键字允许从客户端读文件如果 MySQL 服务端和客户端不在同一台机器去掉LOCAL并把文件放到服务端secure_file_priv目录下。3.2 用 Python 批量 INSERT 的兜底方案有些环境禁用了LOAD DATA LOCAL或者 CSV 里有脏数据导致整批失败。这时候用 Python 分批插入每批 5000 条配合executemany。import pymysql import pandas as pd conn pymysql.connect(host127.0.0.1, userroot, passwordyourpass, databasetest, charsetutf8mb4) cursor conn.cursor() df pd.read_csv(clean_segments.csv, dtypestr) sql INSERT IGNORE INTO phone_location (prefix, province, city, area_code, post_code, carrier) VALUES (%s,%s,%s,%s,%s,%s) batch_size 5000 for i in range(0, len(df), batch_size): batch df.iloc[i:ibatch_size].values.tolist() cursor.executemany(sql, batch) conn.commit() print(f已插入 {ilen(batch)} 条) cursor.close() conn.close()INSERT IGNORE遇到主键冲突会跳过而不是报错适合清洗不彻底的情况。batch_size设 5000 是经验值太大容易触发max_allowed_packet限制太小则提交次数多、速度慢。每批commit一次避免长事务锁表。3.3 导入后的校验查询导入完别急着用先跑三条校验。第一条看总数SELECT COUNT(*) FROM phone_location;。第二条看省份分布SELECT province, COUNT(*) FROM phone_location GROUP BY province ORDER BY COUNT(*) DESC LIMIT 10;。第三条随机抽几个已知号段验证SELECT * FROM phone_location WHERE prefix IN (1381234,1590000,1770000);。如果总数和 CSV 行数对不上大概率是主键冲突被IGNORE掉了回去查重复项。4. 查询与批量更新一条 SQL 补全整列归属地4.1 单条查询的写法SELECT province, city, carrier FROM phone_location WHERE prefix LEFT(13812345678, 7);LEFT函数截取前七位走主键索引执行计划是const级别基本不耗时。如果手机号字段有空格或86前缀先用REPLACE和SUBSTRING清洗再截取。4.2 批量更新用户表的 JOIN 写法假设用户表users有phone和province两列要把归属地批量刷进去UPDATE users u JOIN phone_location p ON LEFT(u.phone, 7) p.prefix SET u.province p.province, u.city p.city, u.carrier p.carrier WHERE u.province IS NULL OR u.province ;这个JOIN会让users全表扫描一次但phone_location走主键整体速度取决于users的行数。百万级用户表大概几十秒到几分钟。注意WHERE条件只更新未补全的行避免重复更新。如果users表很大建议分批每次LIMIT 10000循环执行减少锁持有时间。4.3 查询性能的边界在哪单条查询和批量 JOIN 的性能瓶颈不在phone_location表而在users表的扫描和LEFT函数的计算。LEFT(u.phone, 7)是函数运算无法走users表上phone的索引。如果users表有phone索引可以改成u.phone LIKE CONCAT(p.prefix, %)但这样phone_location又没法走索引了。常见做法是给users加一列prefix导入时就算好然后两列直接等值 JOIN两边都走索引。这是用空间换时间的典型操作。5. 避坑与排查号段表落地时最容易翻车的五件事5.1 导入报错 “Data too long for column”现象LOAD DATA或INSERT时报字段超长。原因CSV 里某些城市名或运营商名超过了varchar(30)或varchar(20)。解决先查SELECT MAX(CHAR_LENGTH(city)), MAX(CHAR_LENGTH(carrier)) FROM ...或者直接把字段改成varchar(50)。别小看这个有些县级市名字很长加上“自治县”三个字就超了。5.2 查询结果为空但手机号明明存在现象SELECT返回空集。原因手机号字段带了空格、86、或者存成了科学计数法。解决查询前先SELECT phone FROM users WHERE phone LIKE % % LIMIT 5;检查脏数据用TRIM和REPLACE清洗。另外确认LEFT截取的是数字如果手机号存成13812345678.0截取前七位还是对的但JOIN时可能因为类型不一致匹配不上。5.3 主键冲突导致导入中断现象LOAD DATA报Duplicate entry。原因CSV 里有重复前七位。解决导入前用GROUP BY prefix HAVING COUNT(*) 1查重复或者用INSERT IGNORE跳过。如果重复项的归属地不一致需要人工判断保留哪条通常保留更新时间较晚的。5.4 虚拟运营商号段覆盖不全现象170、171、165 等号段查不到归属地。原因很多低价号段表只覆盖三大运营商虚拟运营商移动转售、联通转售的号段缺失。解决单独补一份虚拟运营商号段表或者把carrier字段留空但province和city尽量补全。虚拟运营商的归属地通常跟随基础运营商但城市级信息可能不准业务上要接受这个精度损失。5.5 批量 UPDATE 锁表导致线上告警现象执行UPDATE users JOIN phone_location时线上写入变慢甚至超时。原因大事务持有行锁和间隙锁阻塞了其他写入。解决分批更新每批LIMIT 5000批间sleep 0.5秒。或者用pt-online-schema-change这类工具的思路先建影子表再切换。血泪经验是任何超过一万行的UPDATE都不要在业务高峰期裸跑。6. 进阶技巧把归属地查询做成可复用的存储过程6.1 封装一个按手机号返回归属地的函数如果多个业务线都要查归属地与其到处写JOIN不如封装一个FUNCTION。DELIMITER $$ CREATE FUNCTION get_phone_location(p_phone VARCHAR(20)) RETURNS VARCHAR(100) READS SQL DATA BEGIN DECLARE v_result VARCHAR(100); SELECT CONCAT(province, |, city, |, carrier) INTO v_result FROM phone_location WHERE prefix LEFT(p_phone, 7) LIMIT 1; RETURN IFNULL(v_result, 未知|未知|未知); END$$ DELIMITER ;调用方式SELECT get_phone_location(13812345678);返回省份|城市|运营商。READS SQL DATA声明只读LIMIT 1防止意外多行。这个函数在单条查询场景下很方便但批量更新时不要用因为逐行调用函数比JOIN慢很多。6.2 用定时事件自动更新号段表号段会随着运营商放号而变化建议每月更新一次。MySQL 的EVENT可以定时执行导入但更稳妥的做法是用外部脚本 定时任务因为LOAD DATA需要文件路径权限。如果坚持用EVENT可以建一个staging表先把新 CSV 导入staging再用INSERT ... ON DUPLICATE KEY UPDATE合并到主表。INSERT INTO phone_location (prefix, province, city, area_code, post_code, carrier) SELECT prefix, province, city, area_code, post_code, carrier FROM phone_location_staging ON DUPLICATE KEY UPDATE province VALUES(province), city VALUES(city), carrier VALUES(carrier);ON DUPLICATE KEY UPDATE保证新号段插入、旧号段更新不会丢数据。VALUES()函数引用的是SELECT子句里的值MySQL 8.0 之后推荐用别名写法但VALUES()仍然兼容。6.3 验证数据新鲜度的两个查询怎么知道手里的号段表是不是过时了跑两个查询。第一看最新号段SELECT MAX(prefix) FROM phone_location;如果最大值还停留在 199 开头说明 200 号段之后的新号段没覆盖。第二抽查已知新号段SELECT * FROM phone_location WHERE prefix LIKE 192% LIMIT 5;192 是广电号段如果查不到说明数据需要更新。我一般会在表里加一列update_time每次导入时打上时间戳查询时顺便看一眼。6.4 我踩过的一个坑字符集不一致导致 JOIN 失效有次帮同事排查一个“明明有数据却 JOIN 不上”的问题查了半天发现users表是utf8phone_location是utf8mb4JOIN时 MySQL 做隐式转换索引直接失效全表扫描导致查询超时。统一字符集之后问题消失。这个坑不常见但一旦遇到很难往字符集方向想。习惯是建表时显式指定CHARSETutf8mb4 COLLATEutf8mb4_general_ci两边保持一致。希望帮到你。本文还有配套的精品资源点击获取