ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

手机号归属地查询:MySQL本地表设计与查询优化实战

手机号归属地查询:MySQL本地表设计与查询优化实战 简介这是一份面向数据库学习者与开发者的MySQL手机号归属地数据资源适合需要做用户地域分析、营销分群或客户服务支撑的技术人员。压缩包内共1个文件为phone_msg.sql格式的SQL脚本整体约2.22MB导入MySQL后即可得到手机号码与省份、城市对应的结构化数据表便于用SELECT、JOIN、GROUP BY等语句完成单条查询或批量统计。资源标题标注数据较为完整可支撑按省份统计用户数量、按城市聚合分析等常见场景也能作为练习SQL查询与数据处理的实战素材。目前已有267人学习下载。使用时需注意手机号归属地属于个人敏感信息应遵守《个人信息保护法》等相关法规在合法合规前提下使用并采取加密存储、限制访问权限等措施防止泄露。对于希望快速获得可用归属地数据、同时练习数据库查询与信息安全意识的读者这份资源具备一定参考价值。1. 手机号归属地查询这件事为什么最后都绕回一张 MySQL 表做用户注册、风控、订单归属地校验的同学大概率都遇到过同一个需求给一个 11 位手机号立刻返回省、市、运营商。接口方案很多但真到内网、离线、批量几百万条跑数据的时候最稳的还是本地落一张 MySQL 归属地表。这份资源就是干这个的——一份号称「非常全」的手机号归属地数据配 MySQL 建表和查询脚本淘宝上花 50 块买的那种。它解决的不是「怎么调第三方 API」而是「数据在我自己库里查询不求人、不怕限流、不怕接口下线」。适合谁做后台管理系统的、跑数据清洗的、写 JavaWeb 项目要手机号校验的以及被第三方归属地接口按次收费搞烦了的人。下面我按「表怎么建 → 数据怎么灌 → 怎么查得快 → 坑在哪」拆一遍能直接抄作业。2. 建表与数据导入把归属地数据落进 MySQL2.1 先想清楚表结构别一上来就堆字段拿到这份数据第一反应不该是「赶紧导进去」而是先看它长什么样。常见的手机号归属地数据有两种形态一种是纯文本每行「号段,省份,城市,运营商」另一种是已经带 SQL 插入语句的.sql文件。不管哪种落到 MySQL 里我一般会设计成一张窄表而不是把省市区运营商拆成四张表做关联——归属地查询是典型的「读多写极少」拆表只会让每次查询多几次 join得不偿失。我常用的表结构是这样的号段用char(7)存前 7 位比如1381234省、市、运营商各一个varchar。为什么不存完整 11 位因为归属地是按号段分配的前 7 位就能定位存完整号码既浪费空间又没法做前缀匹配。这里有个选型理由要讲透手机号归属地本质是「号段 → 地区」的映射不是「号码 → 地区」理解这一点后面的索引和查询方式就顺了。-- 归属地表按号段存储前7位定位 CREATE TABLE phone_attribution ( id INT UNSIGNED NOT NULL AUTO_INCREMENT, prefix CHAR(7) NOT NULL COMMENT 号段前7位如1381234, province VARCHAR(20) NOT NULL DEFAULT COMMENT 省份, city VARCHAR(30) NOT NULL DEFAULT COMMENT 城市, carrier VARCHAR(20) NOT NULL DEFAULT COMMENT 运营商, PRIMARY KEY (id), UNIQUE KEY uk_prefix (prefix) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT手机号归属地;逻辑说明prefix加了唯一索引一是防止重复导入二是查询时能直接走索引。字符集用utf8mb4因为省份城市里偶尔会有生僻字用utf8可能报错或乱码。carrier单独存一列而不是拼进城市里是因为很多业务要按运营商做统计拆开更灵活。参数说明CHAR(7)而不是VARCHAR(7)因为长度固定CHAR在定长场景下更省空间UNIQUE KEY是这份数据能反复导入而不炸的关键配合后面的INSERT IGNORE用。2.2 导入的两种姿势LOAD DATA 和批量 INSERT数据量这块完整的手机号段归属地大概在 40 万行上下不同版本有差异以你手上的为准。这个量级用LOAD DATA INFILE最快但很多人卡在secure_file_priv权限上所以我一般给两条路服务器上能碰文件的用LOAD DATA本地开发图省事的用批量INSERT。先看LOAD DATA的写法前提是你的数据是 CSV 或制表符分隔的文本-- 服务器端导入注意 secure_file_priv 限制 LOAD DATA INFILE /var/lib/mysql-files/phone.csv INTO TABLE phone_attribution FIELDS TERMINATED BY , LINES TERMINATED BY \n IGNORE 1 LINES (prefix, province, city, carrier);逻辑说明FIELDS TERMINATED BY ,要和你的源文件分隔符一致制表符就写\t。IGNORE 1 LINES跳过表头。如果源文件里号段是完整 11 位这里不能直接导得先用脚本截前 7 位否则CHAR(7)会截断或报错。参数说明secure_file_priv是 MySQL 的安全限制SHOW VARIABLES LIKE secure_file_priv能看到允许的目录文件必须放那个目录下否则报ERROR 1290。这是新手最容易翻车的地方不是 SQL 写错了是文件位置不对。如果数据是.sql文件里面全是INSERT INTO ... VALUES (...)那就更简单# 命令行导入-u 用户名 -p 回车输密码 mysql -u root -p your_database phone_attribution.sql逻辑说明这种.sql文件通常已经包含了建表和插入语句直接重定向导入即可。导入前建议先CREATE DATABASE别导进系统库。参数说明your_database换成你自己的库名。如果文件很大加--max_allowed_packet64M避免包过大中断。导入完一定要验一下行数和抽样SELECT COUNT(*) FROM phone_attribution; SELECT * FROM phone_attribution WHERE prefix 1381234;行数对不上八成是分隔符或编码问题别急着往下走。3. 查询优化从 LIKE 到前缀索引差的是几十倍速度3.1 为什么 LIKE %xxx% 是归属地查询的灾难很多人拿到表第一反应是WHERE phone LIKE %1381234%。这个写法在归属地场景下是血泪教训级别的错误——前置通配符会让索引彻底失效40 万行全表扫描单次查询几百毫秒批量跑起来直接拖垮库。归属地查询的正确姿势是「取前 7 位等值匹配」。-- 正确取前7位做等值查询走唯一索引 SELECT province, city, carrier FROM phone_attribution WHERE prefix LEFT(13812345678, 7);逻辑说明LEFT(phone, 7)把 11 位号码截成 7 位号段然后和prefix做等值比较直接命中uk_prefix唯一索引查询是毫秒级甚至微秒级。这个思路的本质还是那句话归属地按号段分配不是按完整号码。参数说明LEFT(str, 7)的 7 要和表里prefix的长度严格一致改成 8 或 6 都查不到。如果你的数据号段是 8 位那表结构和这里都要同步改。3.2 应用层怎么接Java 和 Python 各一段数据库查得再快应用层写错了也白搭。JavaWeb 项目里常见的是 JDBC 查询这里给一段能直接用的// 根据手机号查归属地注意参数化查询防注入 public String[] getAttribution(String phone) { String sql SELECT province, city, carrier FROM phone_attribution WHERE prefix ?; try (Connection conn DriverManager.getConnection(URL, USER, PWD); PreparedStatement ps conn.prepareStatement(sql)) { // 截取前7位作为号段 ps.setString(1, phone.substring(0, 7)); try (ResultSet rs ps.executeQuery()) { if (rs.next()) { return new String[]{rs.getString(province), rs.getString(city), rs.getString(carrier)}; } } } catch (SQLException e) { // 记录日志返回空而不是抛异常避免影响主流程 log.error(归属地查询失败: {}, phone, e); } return new String[]{, , }; }逻辑说明phone.substring(0, 7)在应用层截号段和数据库的LEFT二选一即可别两边都截。用PreparedStatement是防 SQL 注入的底线手机号虽然是数字但来源不可控时照样要参数化。查询失败返回空数组而不是抛异常是因为归属地查询通常是辅助逻辑不该因为它挂掉整个注册或下单流程。参数说明URL、USER、PWD换成你的连接配置。如果用了连接池Druid、HikariCPgetConnection从池里拿别每次新建。Python 侧如果是做数据清洗用pymysql或SQLAlchemy都行import pymysql def get_attribution(phone): conn pymysql.connect(hostlocalhost, userroot, passwordyour_pwd, databaseyour_db, charsetutf8mb4) try: with conn.cursor() as cur: # 参数化%s 占位 cur.execute( SELECT province, city, carrier FROM phone_attribution WHERE prefix %s, (phone[:7],) ) row cur.fetchone() return row if row else (, , ) finally: conn.close()逻辑说明phone[:7]截号段%s是 pymysql 的占位符不是字符串拼接。charsetutf8mb4要和建表时一致否则中文城市名可能乱码。参数说明批量清洗时别一条条查把手机号列表去重后一次性WHERE prefix IN (...)查出来在内存里映射能少几千次数据库往返。3.3 缓存和批量场景的取舍单条查询走索引已经够快但如果你的业务是「每秒几千次归属地查询」还是建议在应用层加一层本地缓存Caffeine、Guava Cache 都行key 是号段value 是归属地。号段总量就 40 万热点号段更集中缓存命中率很高。批量场景比如导入十万条用户数据要补归属地就别一条条查了正确做法是把所有号段去重分批IN查询每批 1000 个号段查完在内存里做映射。这样十万条数据可能只需要几十次数据库查询比循环单查快两个数量级。这个取舍的判断标准很简单单条实时查询靠索引批量离线处理靠内存映射。4. 避坑与排查导入和查询里最容易翻车的五件事4.1 导入报 ERROR 1290文件明明存在现象LOAD DATA INFILE报ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option。原因MySQL 限制了LOAD DATA只能读secure_file_priv指定目录下的文件你的文件不在那个目录。解决先SHOW VARIABLES LIKE secure_file_priv看允许目录把文件挪过去或者改用LOAD DATA LOCAL INFILE需要客户端和服务端都开启local_infile。我一般直接挪文件改配置重启服务太折腾。4.2 中文城市名导入后变问号现象查出来的城市是???或乱码。原因源文件编码和表的字符集不一致常见是源文件 GBK、表 utf8mb4。解决导入前用file -i phone.csv看编码GBK 的先iconv -f GBK -t UTF-8转一遍建表和连接都用utf8mb4。这个坑在 Windows 上导出的数据里特别常见。4.3 号段长度不一致导致查不到现象明明表里有这个号段查询返回空。原因源数据里号段有的是 7 位、有的是 8 位或者带前导零被当成数字丢了。解决导入前统一截成 7 位prefix用CHAR而不是INT避免前导零丢失。查不到的时候先SELECT * FROM phone_attribution LIMIT 10看看实际存进去的号段长什么样比瞎猜快。4.4 用 LIKE 查询把库拖慢现象归属地查询偶尔超时慢查询日志里全是LIKE %xxx%。原因前置通配符索引失效全表扫描。解决改成WHERE prefix LEFT(phone, 7)等值查询。如果历史代码里已经写死了 LIKE至少改成LIKE 1381234%让索引能用上但根治还是等值匹配。4.5 数据版本老旧新号段查不到现象192、197 这类较新的号段查不到归属地。原因这份数据是某个时间点导出的运营商放号是持续的新号段不会自动出现。解决定期用运营商公开的号段列表补录或者接受「查不到就返回空」的降级逻辑。这一点要提前和产品说清楚别指望一份静态数据能覆盖未来所有号段——这是所有本地归属地库的共同边界。5. 进阶技巧把归属地查询做成一个可维护的小服务前面都是单点操作真要在项目里长期用我建议把它包成一个独立的小查询服务而不是散落在各个业务代码里。理由很实际数据要更新、缓存要管理、降级逻辑要统一散着写迟早失控。具体做法是起一个轻量的 HTTP 服务Spring Boot 或 Flask 都行对外只暴露一个接口GET /attribution?phonexxx内部做三件事先查本地缓存缓存没有查 MySQLMySQL 查不到返回空并记一条日志。这样业务方只管调接口数据更新和服务重启都在这一层解决。接口返回结构固定成{province, city, carrier}别今天返回数组明天返回对象调用方会骂人。验证这套东西是否可靠我一般跑三个检查。第一抽样比对随机抽 100 个号段和运营商公开信息对一遍看准确率。第二边界测试传空串、传 10 位、传带字母的字符串看服务是返回空还是崩。第三压测用ab或wrk打 1000 并发看加了缓存之后 QPS 和响应时间。这三个跑完基本能判断这份数据和服务能不能上生产。# 简单压测-n 总请求数 -c 并发数 ab -n 10000 -c 100 http://localhost:8080/attribution?phone13812345678逻辑说明ab是 Apache 自带的压测工具-n是总请求数-c是并发数。重点看Requests per second和Time per request两个指标加了本地缓存后 QPS 应该能到几千甚至上万。参数说明并发数别一上来就拉满先 100 再逐步加观察数据库连接池有没有被打满。如果 QPS 上不去先看是不是缓存没生效再看数据库连接数。还有个容易被忽略的点数据更新。归属地数据不是一劳永逸的新号段、运营商调整都会让老数据失效。我的习惯是每季度对一遍运营商公开号段把新增的补进去用INSERT IGNORE避免重复。补数据的时候别直接TRUNCATE重导线上服务会瞬间查不到正确做法是导到临时表校验行数无误后再RENAME TABLE切换这样切换是原子的业务无感知。从那以后我每次接归属地需求都强制走一遍「建表 → 导入校验 → 等值查询 → 缓存 → 压测」这条链路再也没出现过上线后查不到或者拖慢主库的情况。希望帮到你。本文还有配套的精品资源点击获取
返回列表