3.那既然括号里的数字,不是用来限制取值范围的那为什么要设置长度呢,我不设置行不行
4.如果表中有INT(11),而插入的是100,你会看到什么样的显示效果?INT(11) 是不是就是存储11位的数字,如果你插入了一个大于99999 的数字(如100000),你认为INT(5) 会如何显示
5.MySQL里面date,datetime,TIMESTAMP有什么区别,假设这个时候我要做一个跨国的系统,那么你觉得选哪个字段会更合适?为什么
6.varcahr最大长度是多少能设置多少就括号里面那个数值
7.唯一索引和普通索引mysql综合读写来看哪个更快
8.唯一索引他是怎么做那个唯一检查的
9.Doublewrite机制了解么
10.mysql 的主从复制它的流程
11Ok那这个时候有问题了,就是说我从库拉到了binlog之后转成Relaylog准备重放的时候,那么它的执行是单线程还是多线程?
12.它怎么执行到区分不同的group去复制呢?
13. 那mysql在做双主复制时,双主它会不会导致循环复制,就是a给BB又给A这种,如果会该怎么去避免这种情况呢
14你基础确实掌握的挺好的,然后我来问一下你设计相关的,就像我们做系统,经常有那个菜单树的概念,比如左边有个菜单有很多层级。对吧,那现在我不设置,比如最深要求多少级,如果说让你设置一个菜单表,尽可能的少的层级也不要care他的性能的前提下,最少需要多少列?
15如果我这时候又要考虑性能呢?比如说我有两个场景,第一个场景是我选择一个父节点父菜单,我能查出全部的子孙然后第二种呢,是我选一个子孙,我能查询出全部,他的父和爷爷以及所有的先祖,你觉得应该怎么设计
16第二个设计题:如果我想要设计一个浏览用户的历史记录,比如说淘宝那种历史记录,我可以看我过去看到的是什么?这个浏览得有两个维度,第一个维度是以个人C端的角度去看这是我自己的历史记录,第二个角度是用供应商的角度去看这个商品被哪些人浏览了,必须要用到分库分表你决定应该怎么设计
问题3:INT括号里的长度为什么设置?不设置行不行?
核心回答
可以完全不设置,且日常开发中建议不设置(因为显示宽度几乎无实际价值)。
为什么会有这个“长度”(显示宽度)?
这是 MySQL 的历史设计残留:早期数据库工具(如 mysql 命令行)依赖显示宽度做格式化展示,核心场景只有一个——配合ZEROFILL做零填充补位(比如 INT(5) ZEROFILL 插 123 显示 00123);此外,显示宽度可作为“字段注释”,让维护者直观知道该字段预期显示的位数(比如 INT(5) 暗示该字段存 5 位以内的编号),但仅为可读性,无强制约束。不设置的后果?
不设置时 MySQL 会用默认显示宽度:INT 默认 11,TINYINT 默认 4,SMALLINT 默认 6,MEDIUMINT 默认 9,BIGINT 默认 20。这些默认值仅为“能完整显示该类型所有数值的最小宽度”(比如 INT 最小负数 -2147483648 是 11 位),不影响任何存储/计算逻辑。
总结:显示宽度是“非必要设计”,不设置完全没问题,现代业务中更推荐靠注释说明字段用途,而非依赖显示宽度。
问题4:INT(11)插100的显示效果;INT(11)是否存11位;INT(5)插100000的显示
核心结论(先破误区)
INT(11) 绝对不是“存储11位数字”,INT 类型固定4字节,最多存 10 位正数(2147483647)或 11 位负数(-2147483648),括号内数字仅影响零填充场景的显示。
INT(11) 插入 100 的显示效果:
- 不加 ZEROFILL:显示
100(和 INT(5)/INT(20) 无区别); - 加 ZEROFILL:显示
00000000100(补0到11位,且自动转为无符号数)。
- 不加 ZEROFILL:显示
INT(5) 插入 100000(6位数字)的显示效果:
- 不加 ZEROFILL:显示
100000(位数超过5也正常显示,无截断/报错); - 加 ZEROFILL:显示
100000(零填充仅补位“不足显示宽度”的情况,超过则原样显示)。
- 不加 ZEROFILL:显示
问题5:date/datetime/TIMESTAMP区别;跨国系统选哪个?
第一步:三者核心区别(表格更清晰,面试口述可简化)
| 特性 | date | datetime | TIMESTAMP |
|---|---|---|---|
| 存储格式 | ‘YYYY-MM-DD’ | ‘YYYY-MM-DD HH:MM:SS’ | 时间戳(秒级,4字节) |
| 取值范围 | 1000-01-01 ~ 9999-12-31 | 1000-01-01 00:00:00 ~ 9999-12-31 23:59:59 | 1970-01-01 00:00:01 ~ 2038-01-19 03:14:07 |
| 时区感知 | 否(纯日期,无时区) | 否(固定时间,不转换) | 是(存储时转UTC,查询时转当前会话时区) |
| 存储字节 | 3字节 | 8字节 | 4字节(5.6+支持毫秒,7字节) |
| 自动赋值 | 无 | 无 | 可设为 CURRENT_TIMESTAMP(更新/插入自动填当前时间) |
第二步:跨国系统选型
优先选 TIMESTAMP(需注意范围),其次用 datetime + 应用层时区转换。
理由:
- 跨国系统核心痛点是时区不一致(比如中国用户(UTC+8)和美国用户(UTC-5)操作同一条数据,时间需自动适配本地时区);
- TIMESTAMP 天然支持时区转换:存储时会将会话时区的时间转为 UTC 存到库中,查询时再根据当前会话的时区转回本地时间,无需应用层做复杂的时区计算;
- 注意事项:TIMESTAMP 范围有限(到2038年),如果业务需要存储超过2038年的时间,可改用
datetime+ 应用层维护时区(比如额外加一个timezone字段记录用户时区,查询时应用层转换)。
问题6:varchar最大长度(括号内的数值)
核心结论:分“理论最大值”和“实际可用值”,核心受3个因素限制
- 底层限制:MySQL 规定行的最大字节数为 65535(所有列的总字节,含字段长度标识、NULL标记等),varchar 本身的“长度”是字符数,但存储时占用“字符字节数 + 1/2字节(长度标识)”。
- 分场景说明:
- 字符集影响:
- utf8(3字节/字符):理论最大字符数 = (65535 - 2) / 3 ≈ 21844(减2是长度标识);
- utf8mb4(4字节/字符):理论最大字符数 = (65535 - 2) / 4 ≈ 16383;
- InnoDB 额外限制:
- 若 varchar 是表的唯一可变长度列,且行格式为 COMPACT/DYNAMIC,超过768字节的 varchar 会存到溢出页,此时“实际常用值”:utf8mb4 下建议设 191(避免索引长度超限),utf8 下建议设 255;
- 括号内数值:直接填“字符数”(如 varchar(20) 表示存20个字符,无论utf8/utf8mb4)。
- 字符集影响:
总结:括号内可设的最大值受字符集和行总字节限制,utf8mb4 下理论最大16383,实际开发中建议按业务需求设(如手机号设11,姓名设50),而非拉满。
问题7:唯一索引 vs 普通索引 综合读写速度
核心结论:读几乎无差,写普通索引更快,综合看普通索引更优(写多场景)
读性能:几乎无区别(唯一索引微优,但可忽略)
- 普通索引:找到匹配行后,会继续往后扫一小段(确认是否有下一个匹配行);
- 唯一索引:找到匹配行后直接返回(无需确认下一行);
- 实际场景中,这个差异极小(尤其是InnoDB的聚簇索引结构),用户感知不到。
写性能:普通索引显著更快
- 唯一索引:写入/更新时,必须先做“唯一性检查”(加锁+索引查找),且无法使用“变更缓冲(Change Buffer)”(变更缓冲仅对普通索引生效,可延迟写入磁盘,提升写性能);
- 普通索引:无需唯一性检查,可利用变更缓冲,写操作的磁盘IO更少。
综合读写:
- 读多写少:两者几乎无差,唯一索引可避免重复数据,略优;
- 写多读少:普通索引优势明显,综合速度更快。
问题8:唯一索引的唯一检查机制
核心流程(分插入/更新场景)
- 检查时机:写操作(INSERT/UPDATE)的“执行阶段”(而非提交阶段),先检查唯一性,再执行写入。
- 检查步骤:
- 步骤1:根据要写入的值,走唯一索引快速查找(B+树查找,O(logn)),确认是否存在相同值;
- 步骤2:加锁防止并发冲突(关键!避免幻读导致重复):
- 插入场景:加“间隙锁/Next-Key锁”(隔离级别≥RR),锁定要插入的位置,防止其他会话同时插入相同值;
- 更新场景:先锁定旧值行,再检查新值的唯一性;
- 步骤3:若找到重复值,直接返回报错(1062 Duplicate entry);若未找到,执行写入。
- 特殊情况:
- 批量插入(INSERT … VALUES (…)):逐行检查唯一性,一行失败则整个语句回滚(5.7+可设
innodb_duplicate_key_error_is_recoverable=1跳过错误行); - 联合唯一索引:检查所有索引列的组合值是否唯一。
- 批量插入(INSERT … VALUES (…)):逐行检查唯一性,一行失败则整个语句回滚(5.7+可设
问题9:Doublewrite机制(双写缓冲区)
核心:解决InnoDB“部分写页”崩溃问题(比如写页时断电,页只写了一半)
- 为什么需要:InnoDB 页大小默认16KB,写入磁盘时若只写了一部分(如8KB),页会损坏,且redo log仅记录“页的修改”,无法修复损坏的页。
- 核心流程:
- 步骤1:InnoDB 准备将内存中的脏页刷到磁盘时,先把页内容写入“双写缓冲区”(内存中2MB,对应磁盘共享表空间的doublewrite区,连续的128个页);
- 步骤2:双写缓冲区刷到磁盘(顺序写,性能高),确保页完整写入;
- 步骤3:再将双写缓冲区的页刷到数据文件的对应位置(随机写);
- 步骤4:崩溃恢复时,检查数据文件的页是否完整:若完整则正常恢复;若损坏,从doublewrite区读取完整页覆盖,再用redo log恢复数据。
- 性能影响:看似多写一次,但doublewrite区是顺序写,实际性能损耗仅5%-10%,换来了页的完整性。
问题10:MySQL主从复制流程
核心三步(面试可分阶段讲,逻辑更清晰)
阶段1:主库生成Binlog
- 主库执行增删改操作,事务提交时,将修改记录按顺序写入Binlog(二进制日志);
- 主库维护一个 dump 线程,等待从库连接并请求Binlog。
阶段2:从库拉取Binlog
- 从库启动 IO 线程,连接主库的 dump 线程;
- 从库向主库请求“指定位置后的Binlog”,主库 dump 线程推送Binlog到从库;
- 从库 IO 线程将收到的Binlog写入本地 Relaylog(中继日志),并记录主库的Binlog位置(master_log_file + master_log_pos)。
阶段3:从库重放Relaylog
- 从库启动 SQL 线程,读取Relaylog中的日志事件;
- 按顺序执行日志中的增删改操作,还原主库的数据,保持主从数据一致。
总结:主库(Binlog + dump线程)→ 从库IO线程(拉取→写Relaylog)→ 从库SQL线程(重放Relaylog)。
问题11:Relaylog重放是单线程还是多线程?
核心:分MySQL版本,从单线程进化到多线程
- 5.6及之前:单线程(SQL线程)→ 性能瓶颈(主库高并发时,从库重放慢,导致主从延迟);
- 5.6:支持“基于库的并行复制”→ 多线程(每个库一个线程),但如果所有写操作都在一个库,还是单线程;
- 5.7:支持“基于GTID的并行复制”(逻辑时钟)→ 无冲突的事务可并行执行(按事务的提交顺序,同一组的事务并行);
- 8.0:支持“基于写集(Write Set)的并行复制”→ 更智能,按事务修改的数据行哈希分组,无冲突的事务跨库/跨表并行,性能大幅提升。
总结:现代MySQL(5.7+)默认多线程重放,老版本单线程。
问题12:如何区分不同group复制?(MySQL Group Replication,MGR)
核心:基于“事务的冲突检测”和“逻辑时钟/写集”分组
- 核心原理:MGR 是分布式一致性复制(Paxos协议),将事务分为“可并行”和“需串行”的组:
- 步骤1:每个事务提交前,生成“写集”(Write Set,即事务修改的行的哈希值);
- 步骤2:节点间交换写集,检测事务是否冲突(比如两个事务修改同一行,则冲突);
- 步骤3:无冲突的事务分到同一个“组”(Logical Clock),可并行执行;有冲突的事务分到不同组,串行执行。
- 传统并行复制(非MGR):
- 5.6:按库分组(不同库的事务并行);
- 5.7:按GTID的“事务序列号”分组,同一逻辑时钟的事务并行。
总结:核心是“冲突检测”,无冲突的事务归为同一group并行,冲突的串行。
问题13:双主复制的循环复制及避免
第一步:会导致循环复制!
双主复制(A←→B)中,A的Binlog同步到B,B执行后生成新的Binlog,又会同步回A,A执行后再同步到B,无限循环。
第二步:避免方案(核心是“识别并丢弃自己生成的Binlog”)
- 核心方案:设置唯一的server_id + 过滤自身server_id的Binlog:
- 给主库A和B设置不同的server_id(如A=1,B=2);
- 开启
log_slave_updates(从库执行Relaylog后,将操作写入自己的Binlog)时,主库收到Binlog后,先检查Binlog中的server_id:如果是自己的server_id,则丢弃该Binlog,不执行;
- 辅助方案1:使用GTID:
MySQL 5.6+的GTID(全局事务ID)包含server_id,从库执行Relaylog时,会记录已执行的GTID,若收到的Binlog的GTID已存在,则跳过执行,天然避免循环; - 辅助方案2:业务层面隔离:
比如A主库写奇数ID的数据,B主库写偶数ID的数据,Binlog同步后,对方执行时无匹配数据,不会产生新的Binlog。
问题14:菜单树表 最少列(不考虑性能)
核心:最少3列(满足任意层级,无性能要求)
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 菜单主键(唯一标识) |
| menu_name | VARCHAR(50) | 菜单名称(必选,展示用) |
| parent_id | BIGINT | 父菜单ID(顶级菜单设为0或NULL) |
理由:
- id:唯一标识每个菜单,是层级关联的基础;
- menu_name:菜单的核心业务属性,必须有;
- parent_id:通过“子菜单的parent_id=父菜单的id”维护层级关系,无论多少层,仅靠这一列就能递归遍历(比如顶级菜单parent_id=0,一级菜单parent_id=0,二级菜单parent_id=一级菜单id,以此类推);
- 无其他必选列:比如排序、状态等属于业务扩展,题目要求“最少列”,可省略。
问题15:菜单树表 考虑性能(查子孙/查先祖)
核心方案:放弃纯递归,改用“物化路径法”(最优平衡方案)
第一步:表结构设计(新增1列,共4列)
| 列名 | 类型 | 说明 |
|---|---|---|
| id | BIGINT | 菜单主键 |
| menu_name | VARCHAR(50) | 菜单名称 |
| parent_id | BIGINT | 父菜单ID(兼容旧逻辑) |
| path | VARCHAR(1000) | 物化路径(如“0,1,5,8”) |
path字段规则:
- 用分隔符(如逗号)拼接所有祖先ID,从根到当前菜单;
- 顶级菜单path=“0”;
- 一级菜单(父=0)path=“0,1”;
- 二级菜单(父=1)path=“0,1,5”;
- 三级菜单(父=5)path=“0,1,5,8”。
第二步:满足两个性能场景
- 查父节点的所有子孙:
SQL:SELECT * FROM menu WHERE path LIKE CONCAT(#{parentPath}, ',%');
示例:查父节点1的子孙 →path LIKE '0,1,%',直接命中所有子、孙、曾孙,无需递归,一次查询搞定; - 查子孙的所有先祖:
SQL:拆分path字符串,按分隔符分割后,直接查询所有ID对应的菜单;
示例:path=“0,1,5,8” → 拆分出[0,1,5,8],查询id in (0,1,5) 就是所有先祖,拆分可在应用层做(如Java用split),或用MySQL的SUBSTRING_INDEX函数。
进阶方案(极致性能):闭包表
如果数据量极大,可新增“闭包表”(menu_closure),存储所有祖先-后代关系:
| 列名 | 类型 | 说明 |
|---|---|---|
| ancestor_id | BIGINT | 祖先菜单ID |
| descendant_id | BIGINT | 后代菜单ID |
| depth | INT | 祖先到后代的层级(可选) |
- 查子孙:
SELECT descendant_id FROM menu_closure WHERE ancestor_id=#{parentId}; - 查先祖:
SELECT ancestor_id FROM menu_closure WHERE descendant_id=#{childId}; - 缺点:新增/删除菜单时,需维护闭包表(比如新增二级菜单,要给所有祖先添加一条关联记录),但查询性能极致。
问题16:用户浏览历史记录 分库分表设计(C端+供应商端)
核心思路:双维度分片 + 双写保证一致性
第一步:核心诉求拆解
- C端:按用户ID查自己的浏览记录(用户维度,高频读);
- 供应商端:按商品ID查浏览用户(商品维度,低频读,需全局视角);
- 分库分表:解决单表数据量过大(如亿级浏览记录)的问题。
第二步:表结构设计(基础表)
CREATETABLEuser_browse_record(idBIGINTNOTNULLAUTO_INCREMENTCOMMENT'主键',user_idBIGINTNOTNULLCOMMENT'用户ID(C端分片键)',goods_idBIGINTNOTNULLCOMMENT'商品ID(供应商端分片键)',supplier_idBIGINTNOTNULLCOMMENT'供应商ID(快速过滤)',browse_timeDATETIMENOTNULLCOMMENT'浏览时间',goods_nameVARCHAR(100)COMMENT'商品名称(冗余,避免联表)',PRIMARYKEY(id),KEYidx_user_time(user_id,browse_time)COMMENT'C端查询索引',KEYidx_goods_time(goods_id,browse_time)COMMENT'供应商查询索引')ENGINE=InnoDBDEFAULTCHARSET=utf8mb4;第三步:分库分表策略
- C端维度(优先):
- 分片键:user_id;
- 分片规则:user_id % 分库数 = 库号,user_id % 分表数 = 表号;
- 优势:用户查自己的历史记录时,直接定位到对应的库表,查询速度极快;
- 供应商端维度:
- 方案1:双写(推荐)→ 写入C端库表的同时,通过消息队列(如RocketMQ)异步写入“商品维度”的分库分表(分片键=goods_id);
- 方案2:全局索引 → 用Elasticsearch存储“goods_id → user_id列表”,供应商查询时先查ES,再到C端库表拉取详情;
- 优势:供应商查商品浏览记录时,直接定位到商品维度的库表,无需全库扫描。
第四步:细节补充
- 过期数据:定时清理(如超过3个月的记录),避免表过大;
- 读写分离:分库分表后,读请求走从库,写请求走主库;
- 一致性:双写时用消息队列保证最终一致,失败重试;
- 分片数:分库数建议8/16,分表数建议64/128(避免后续扩容麻烦)。