ARTICLE DETAIL

资讯详情

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

20亿手机号存储选型:int还是string?varchar还是char?

20亿手机号存储选型:int还是string?varchar还是char? 前两天有个学弟面试字节回来跟我复盘一面环节说有一道题答得并不好20亿手机号存储选int还是stringvarchar还是char为什么我听完第一反应是这题其实很典型表面在问类型选型实际上在考一个人有没有真正理解存储。这道题网上其实也有不少答案但大多止步于“手机号要用varchar存因为int会溢出”这个回答放在一面基本等于没答。面试官紧接着一个“那为什么不用bigint”就能把人问懵。所以这篇我打算把这道题从头到尾拆一遍从数值边界、存储字节、字符集、索引行为一直聊到20亿行数据下的分表和扩展设计。这不是八股背诵而是把“选型”这件事的完整决策链路走一遍看完你不仅能正面回答这道题还能接住面试官后面可能追的三个问题。1. 先说结论这题考的不是“选哪个”1.1 面试官真正想听什么很多候选人拿到这类问题时第一反应是给出一个正确的类型名然后开始背优缺点。但面试官并不想听你一锤子定音他真正想考察的是你的决策过程。一道存储选型题在面试官心里通常有三个层次第一层你知不知道int和string各自的范围、长度限制这是最基础的语言/数据库常识。第二层你知不知道char和varchar在存储引擎里是怎么落盘的比如固定长度、变长记录、字符集、行格式这些概念。第三层你有没有工程全局观能不能把“20亿行”这个规模放到真实场景里想到容量、分表、号码扩展、敏感数据这些后续问题。三层都答到才算答完一道题。只答到第一层面试官会觉得你是背过答案的于是紧接着追问“为什么不用bigint”“varchar和char在InnoDB里差多少字节”“如果以后要存国际号码怎么办”基本上三个问题就能判断出你是真懂还是假懂。1.2 为什么“20亿手机号”这个数字很关键先注意题目里的“20亿”不是随便写的。20亿这个数正好卡在int有符号范围的上界附近——int最大是21.47亿。出题人故意放一个这么暧昧的数量级就是看你有没有意识到20亿已经远超单表能承载的合理规模这背后是分库分表问题手机号本身不是“序号”不能在20亿这个数量级里直接用自增int塞更关键的是20亿行数据意味着存储成本会被强烈放大。假如手机号这一列每个值多占1个字节20亿行就是20GB的额外磁盘和内存开销。在这个量级下“varchar还是char”的字节级差异就不是抠细节而是实打实的成本问题。所以这个数字决定了你不能简简单单说“都能存看需求”而必须拿出另外一套容量判断的逻辑。1.3 常见的答题误区我先列几个我见过的高频错误答法这些都是会直接把一面聊死的那种上来就说“用varchar(11)就完了”没有任何计算过程面试官问“为什么不用bigint”就卡住。说“手机号是数字所以用bigint”这个答案比int好一点但没意识到手机号是标识符不是参与运算的数。说“用char(11)因为手机号长度固定”听起来有道理但完全忽略了字符集和存储开销在utf8mb4下char(11)反而是最费空间的选择。把“varchar”和“char”的区别背得像教科书一样但没结合InnoDB实际情况听上去就很悬浮。下面我按真正的答题顺序展开先解决int还是string再解决varchar还是char。2. 类型第一关int为什么直接出局2.1 int的取值范围根本不支持11位手机号先把数字边界放到桌面上。MySQL的int有符号范围是 -2147483648 到 2147483647无符号范围是 0 到 4294967295。也就是不管有没有符号int最多只能表示到42.9亿左右。再看手机号。一个标准的国内手机号是11位以1开头所以它的最小形式是10000000000最大形式大约是19999999999位数上已经达到百亿级别。光从数值大小来看int连10亿都不够11位手机号更是远超int上限。这里还有一个更隐蔽的点哪怕你自作聪明只存手机号的“后10位”仍然装不进int。后10位最大是9999999999接近100亿比无符号int的42.9亿还要大一倍多。所以int这条路的物理边界是死的没有任何绕过去的空间。我建议你在面试中直接把这段换算说出来而不是简单地扔一句“int会溢出”。能在纸上写出“11位手机号大于int上限所以int直接排除”就已经比绝大多数候选人专业了。2.2 能装下的bigint为什么也不推荐int排除之后很多人会立刻想到bigint。bigint有8字节有符号范围大约是 -9.22×10^18存11位手机号绰绰有余。从“装得下”这个角度看bigint确实没问题但它依然不是好选择。首先是语义问题。手机号的本质是一个标识符不是数值。你用bigint存等于把一个不需要参与加减乘除的账号ID当成了数字。这在逻辑上就和用int存身份证号、订单号是一样的错误类型暗示了用途一旦字段类型是数值后续开发就可能不自觉地对它做数值处理。其次是扩展性问题。国内手机号看起来是纯数字但业务一旦走到海外就会遇到“86 13800138000”“0044 20 7946 0958”这样的格式。bigint只能存纯数字面对号、空格、括号直接崩。而现实业务中手机号字段为了兼容国际号码改成varchar的情况非常常见但把bigint改成varchar的代价极大。再说一个我实际见过的坑如果用bigint存手机号业务代码里查询时很容易写成WHERE phone 13800138000看起来没问题但因为传入的是字符串类型MySQL需要做隐式类型转换一旦数据量上来索引能不能命中就是另一回事。这个我在第4节详细讲。2.3 手机号的本质是标识符不是数值把这个问题想透其实就理解了为什么业界普遍推荐用string。你可以把手机号类比成快递单号、车牌号、身份证号。它们的共同点是不需要参与任何数学运算有固定的格式和号段语义未来可能包含字母、符号或国家码查询场景通常只有等值匹配、前缀匹配、模糊匹配。把这些需求套到int、bigint上你会发现数值类型天生就不适合。比如你想查“所有13x开头的手机号”用varchar可以直接LIKE 13%但用bigint就没法优雅地做前缀匹配你只能先算数值范围再逐步缩小条件繁琐且容易出错。所以这里真正重要的不是“装不装得下”而是“这个字段在业务里到底扮演什么角色”。它是标识符就该按字符串的思维去设计而不是因为它长了一副数字的样子就扔进数值类型。3. 存储第二关varchar和char的差别要算到字节3.1 定长与变长理解清楚存储开销类型落到字符串之后才是varchar和char的较量。char是定长类型声明char(11)后每条记录都会按11个字符占位。如果实际内容不足11个字符MySQL会用空格填充读取时再去掉尾随空格。varchar是变长类型varchar(11)只表示“最多11个字符”实际存几个字符就占几个字符的空间同时额外用1到2个字节记录实际长度。听起来好像char是浪费varchar是省空间但事情没那么简单字符集和行格式会让结果反转。先记一个最基础的结论在InnoDB里char的存储是按“声明字符数 × 该字符集最大字节数”提前预留的varchar则是按实际字节数加长度前缀存放。这两个机制在不同字符集下的表现天差地别。3.2 字符集是最大的隐藏变量手机号里的数字、加号这些都是ASCII字符在utf8mb4下只占1个字节。但MySQL声明CHAR(11)的时候可不看你的实际内容它只看字符集和声明长度。如果表的默认字符集是utf8mb4一个中文字符最多占4个字节那么char(11)会按11×444字节预留空间不管你存的是不是纯数字。如果是varchar(11)它没预留实际存11位数字就只占11字节再加上1字节的长度前缀总共12字节。一对比就很清楚在utf8mb4下char(11)存一个手机号要浪费32字节。20亿条数据光是这一列char就要比varchar多出大约60GB以上的空间开销。这还只是一个列的差距如果是大宽表差距会更夸张。那是不是说char就一定不好也不是。如果表的字符集是latin1这种单字节字符集char(11)就占11字节varchar(11)反而要12字节char又成了更省的那个。所以下次面试如果只说“char定长费空间”是不够严谨的一定要补充字符集这个前提。3.3 行格式、碎片和更新场景的影响除了静态字节数还要考虑InnoDB的行存储行为。InnoDB默认的compact和dynamic行格式对varchar这类变长字段的处理是如果一行里变长字段多了或某条记录的varchar值变长记录可能发生页内移动甚至造成页分裂。char定长时存储位置相对固定更新同长度内容不需要移动数据这是定长字段在老存储引擎里的优势。但注意“定长不移动”这块优势在InnoDB里被削弱了。InnoDB本身按主键聚簇B树节点由行记录填充每条记录还有事务ID、回滚指针等额外信息。char定长带来的“随机访问快”红利在InnoDB这种行存储结构里并不明显反而因为char在utf8mb4下占44字节把一页能容纳的行数压得更低导致同样数据量需要读更多页IO反而更差。还有碎片问题。varchar如果不断从短变长Page内会有碎片但可以通过表重建来整理。char虽然不会因为长度变化产生碎片但如果各种定长字段堆在一起行变大一个页里能放的行数少也是变相的性能损失。所以内存中、MyISAM时代那种“char更快”的惯性思维放在InnoDButf8mb4的现代组合下基本不成立。更好的判断标准是先看字符集再看实际业务更新频率最后看扩展空间。3.4 我的建议默认varchar(11)综合上面的字节计算和InnoDB行为我的倾向非常明确国内手机号纯数字11位字段设计推荐phone varchar(11) NOT NULL如果业务确定只存国内号码且不会国际化也建议保留varchar(11)不要用char因为utf8mb4下char浪费空间如果有海外号码可能直接用phone varchar(32)一步到位别等到需要支持“86”时再改表。有人可能会问那用char(11)配合latin1字符集不也能省1个字节吗理论上成立但你为了一个字段单独把表改成latin1会牺牲其他中文字段的存储能力。除非整张表确定只有ASCII内容否则这个优化得不偿失。而且手机号未来的格式变化远比省那1个字节重要。4. 从答案到方案DDL、索引与面试追问4.1 一段能直接落地的建表语句光说结论不给方案等于没说。我直接把一个比较合理的设计写在下面CREATE TABLE user_phone ( id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, phone VARCHAR(11) NOT NULL COMMENT 国内手机号, status TINYINT NOT NULL DEFAULT 1 COMMENT 账号状态, created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uk_phone (phone) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_bin COMMENT用户手机号表;几个细节值得说phone用varchar(11)并加唯一索引保证同一手机号不重复注册。collate用utf8mb4_bin避免某些字符集的排序规则把手机号当成文本做奇怪的大小写转换。手机号是纯数字用bin比较最直接。主键用自增bigint而不是手机号做主键。原因很简单手机号有隐私属性且后续可能变化不适合做聚簇主键。如果业务允许“用户没有手机号”我建议用空字符串而不是NULL。因为MySQL唯一索引允许出现多个NULL一旦用NULL代表“无手机号”唯一索引的约束效果就会被绕过导致同一手机号被插入多行NULL。4.2 索引失效、隐式转换这些坑要怎么避移动号段查询是典型场景。很多人把手机号存成数值类型后写查询时喜欢写WHERE phone 13800138000这条语句在phone是整数类型时也许能走索引但如果phone是varchar类型MySQL会把字符串常量转成数字去比较还是会把列类型隐式转为数字这里有一个很容易搞错的行为当列类型是varchar条件里给的是数字时MySQL会尝试把列值转换成数字进行比较此时索引可能失效因为每一行的列值都需要被转换后才能参与比较。当列类型是int/bigint条件里给的是字符串时MySQL会把字符串转换成数字然后与列值比较索引通常还是能用的。所以哪怕你选了string类型也要让查询条件里的参数保持字符串类型养成写WHERE phone 13800138000的习惯。这个问题在多语言ORM里尤其隐蔽比如Java里直接用Long类型接手机号参数到了SQL里就会变成数字坑很多。另一个常见坑是LIKE。手机号查询常需要按号段匹配比如统计某个运营商的用户量。varchar列上写WHERE phone LIKE 138%如果前缀足够有区分度是有机会走索引的但如果是int/bigint列这种写法根本没法成立。这也是字符串存储的一个隐形优势。4.3 一套完整的回答参考我把前面的分析压缩成一段面试答案你可以直接拿去参考但建议改成自己的话我会分三层来分析。第一层int不能选因为int有符号最大2147483647无符号最大4294967295而11位手机号已经到百亿规模连存后10位都放不进int。第二层bigint虽然能装下但手机号是标识符不是数值不需要参与运算而且未来可能支持86这种带符号的国际号码数值类型根本存不了。第三层在string里我选varchar而不是char。假设表用utf8mb4char(11)会按最大字节数预留44字节而varchar(11)只存11个数字加1字节长度前缀总共12字节20亿行光这一列就能省几十GB空间。而且varchar变长未来扩张到varchar(32)去兼容国际号码也比char更自然。如果业务确定只存国内号码我会用varchar(11)并加唯一索引。这段话有结论、有计算、有存储原理、有扩展性基本能覆盖大部分追问。5. 如果是20亿行数据问题还没结束5.1 这个量级先要解决分表问题回到题干的20亿。就算你选了varchar(11)单表20亿行在InnoDB里也是不现实的。按每行100字节估算20亿行光数据就是2TB级别再加上二级索引单机磁盘、内存、备份、运维全都扛不住。所以聊到这一步一定要把分库分表补上。手机号作为天然的业务标识很适合做分片键。常见的做法是对手机号做hash再模分片数比如拆成1024张表让同一个手机号的查询落到固定分片。这里就能看到“一开始选对类型”的长期价值如果一开始就把phone设计成bigint分片时还可以勉强转换但一旦字段定义是数值类型而业务后来要求支持国际号码你连分片逻辑都要跟着改迁移成本是灾难级的。所以很多老系统后期改造时第一件事就是把手机号字段从bigint换成varchar再加新数据同步双写、校验、切流量整个流程走下来以月为单位。面试中能提到这一段会让面试官觉得你见过真实生产环境。5.2 号码扩展和国际化的兼容设计再往后想一步手机号不是只有中国有。出海业务、跨境电商、海外用户注册都会遇到E.164格式的号码比如8613800138000最长可以达到15位左右。如果你当初设计的是char(11)现在要改列长度线上大表ALTER TABLE的代价非常高不仅要拷数据还可能锁表、影响线上写入。这类问题在面试里经常被用来考察“设计时有没有预留扩展性”。所以我的习惯是没出海的团队手机号字段用varchar(11)没问题但在DDL注释里写清楚“预留国际号码扩展”如果公司有出海规划直接用varchar(32)起步多花不了多少空间但省掉了未来一次大迁移。还有一个容易被忽略的点如果未来要支持国际号码原来的唯一索引就不能简单建立在phone上需要考虑国家码手机号的联合唯一。字段设计上分开存country_code和phone会更干净但这是另一个话题面试时点到即可。5.3 安全合规手机号不是普通字段最后手机号属于个人信息和用户名、昵称不一样。真实生产环境里手机号这列通常要做脱敏展示、加密存储、访问审计查询接口不能直接返回完整号码。这会对字段设计产生影响。比如加密存储后密文长度可能会超过11个字符如果当初用的是char(11)密文根本塞不下但varchar天然变长配合合理的上限值比如varchar(64)还能兼容密文存储。另外如果需要对手机号做等值查询又不想存明文常见的做法是再加一个专门的hash列用sha2之类的算法存定长hash值作为查询索引明文列则加密存储。这些点虽然超出了一道面试题的字面范围但恰恰是“20亿手机号”真实项目中绕不开的环节。你能主动提到合规和脱敏会让整段回答的工程分量完全不一样。最后说个我自己的实际体会。这些年接触过的老系统里把手机号设计成bigint的并不少见表面看没出大问题真到接国际号码、做号段分析、改分表方案的时候个个都想骂当年拍板的人。类型选型这种事不只是“能存不能存”而是给未来三五年的业务留空间。你画表的时候多花两分钟想清楚后面能帮运维和业务省下几个月的时间。另外分享一个排查小技巧不确定某个字段的行为时先SHOW CREATE TABLE看字段类型和字符集再EXPLAIN看查询是否走索引这两步能解决大部分和存储选型相关的线上问题。
返回列表