mercury保险一面:面试题与参考回答
根据提供的截图整理,保留原题编号。截图只包含第 3~7 题,第 1、2 题未提供,不作补写。数据库回答以 MySQL / InnoDB 为主,算法使用 Java。回答为整理后的参考答案,不代表面试官标准答案。
3. SQL 索引怎么用?什么情况下索引会失效?
截图底部第 3 条:参考答案与补充
截图参考答案(原文,部分表述需结合下方说明理解):
or条件,like通配符,对索引使用函数,表达式,隐式转换(类型不匹配)(如果是MongoDB还可以加一个先sort再match导致索引失效)
面试时展开说:我会检查 OR 各分支是否有合适索引、LIKE 是否以通配符开头、索引列是否被函数或表达式包裹,以及参数类型是否导致列发生隐式转换。最后用 EXPLAIN 判断实际访问方式,不能把这些关键词一律等同于“索引失效”。
- OR:某个分支缺少索引时可能扫描整表;多个分支都有合适索引时,也可能使用 Index Merge。参见 MySQL Index Merge 文档。
- LIKE:普通 B+ 树索引对 LIKE 'abc%' 有机会做范围定位;LIKE '%abc%' 通常不能按固定前缀定位。是否扫描索引、是否高效是不同问题。
- 函数、表达式和类型转换:尽量让被索引列保持原样,例如把 DATE(created_at)=某天改为当天起止范围;绑定参数与列类型匹配。函数索引等特殊设计需要另行判断。
- MongoDB:不能说“先 $sort 后 $match 一定失效”。优化器可以把适用的 $match 移到 $sort 前,仍需查看优化后的 explain。参见 MongoDB 管道顺序优化。
下面保留索引设计、SQL 示例及更完整的追问说明。
面试回答:
索引的作用是减少查询需要扫描的数据,也可能帮助排序、分组,或者通过覆盖索引减少回表。设计索引时,我会先看高频 SQL 的过滤条件、关联条件、排序和返回字段,再结合数据分布设计联合索引,而不是给所有字段都建索引。
普通 InnoDB 索引通常按 B+ 树组织,联合索引按照字段顺序排序。比如索引 (merchant_id, status, created_at),适合商户和状态等值过滤后,再按创建时间做范围查询。需要遵循最左前缀,但这是索引定义的顺序,不要求 WHERE 中的条件按相同顺序书写。MySQL 联合索引说明
所谓“索引失效”需要区分:条件无法利用索引定位、只能使用部分索引,以及优化器认为全表扫描更划算。最终要用执行计划确认,不能只背“出现某个关键字就失效”。
联合索引示例:
CREATE INDEX idx_merchant_status_time
ON pay_order (merchant_id, status, created_at);
SELECT id, created_at
FROM pay_order
WHERE merchant_id = 1001
AND status = 1
AND created_at >= '2026-09-01 00:00:00'
AND created_at < '2026-10-01 00:00:00'
ORDER BY created_at
LIMIT 100;
假设 id 是 InnoDB 主键,上述查询所需字段可以由该二级索引提供,有机会实现覆盖索引。增加索引也会增加写入维护和存储成本,要根据查询收益取舍。MySQL 索引优化说明
常见情况与准确表述:
| 情况 | 示例 | 面试中应如何解释 |
|---|---|---|
| 联合索引缺少最左列 | 索引 (a,b,c),只查 b=2 |
通常无法进行常规最左前缀定位;仍可能出现索引全扫描或特定条件下的跳跃扫描,不能说完全不会用索引 |
| 索引列上使用函数 | DATE(created_at) = '2026-09-15' |
对普通 created_at 索引,通常无法直接形成高效定位范围;可改成日期范围,或评估匹配表达式的函数索引 |
| 索引列参与运算 | amount + 1 = 100 |
优先改写为 amount = 99,使被索引列保持原样 |
| 字符串列与数字比较 | phone = 13800138000,phone 为 VARCHAR |
可能因字符串列发生数值转换,无法按原字符串索引快速定位;参数应使用字符串类型 |
| LIKE 以通配符开头 | name LIKE '%张'、name LIKE '%张%' |
普通 B+ 树索引通常无法按固定前缀做范围定位;LIKE '张%' 则可能使用范围扫描 |
| OR 中存在无合适索引的分支 | a=1 OR b=2,只有 a 有索引 |
可能选择全表扫描;两个分支都有合适索引时,也可能使用 Index Merge |
| 条件命中比例过高 | 查询表中绝大多数行 | 优化器可能认为大量二级索引回表比扫描整表更贵 |
| 统计信息偏差 | 估算行数与实际行数差异很大 | 可能选错计划,需要检查统计信息和数据倾斜 |
范围扫描、LIKE 前缀匹配和跳跃扫描的适用条件参见 MySQL 范围优化。字符串列与数字比较的限制参见 MySQL 类型转换。OR 的多索引访问参见 MySQL Index Merge。
函数条件改写示例:
-- 对索引列做函数运算,普通索引通常难以直接用于范围定位。
SELECT id FROM pay_order
WHERE DATE(created_at) = '2026-09-15';
-- 改成左闭右开区间,避免遗漏带小数秒的记录。
SELECT id FROM pay_order
WHERE created_at >= '2026-09-15 00:00:00'
AND created_at < '2026-09-16 00:00:00';
Java 使用 JDBC / MyBatis 时,要保证绑定参数的类型与字段匹配,并统一业务时区和数据库时间口径。
容易被追问的点:
- 范围条件之后的字段不等于“完全失效”。例如
(a,b,c)上的a=1 AND b>10 AND c=3,c 通常不能继续缩小该连续扫描区间,但仍可能通过索引条件下推参与过滤,减少回表。MySQL ICP !=、NOT IN、IS NULL不应一律判定为不走索引,要看实际条件、命中比例和执行计划。- 把 OR 改成 UNION / UNION ALL 需要保持结果语义,尤其要处理重复行;不能机械替换。
EXPLAIN中key非空不代表查询高效,仍需看扫描行数、过滤效果和回表量。
截图中 MongoDB 扩展的纠正:
“先 $sort 再 $match 一定导致索引失效”不准确。MongoDB 优化器可将适用的 $match 前移到 $sort 前面,实际索引使用应看优化后的执行计划,而不是只看代码中的阶段顺序。MongoDB 聚合管道优化
4. SQL 较慢的原因如何排查?除了索引,还有什么方法提高效率?
截图底部第 4 条:参考答案与补充
截图参考答案(原文):
用explain排查,排查锁竞争,硬件配置,对大表分区,缓存结果,批量更新数据(hive的话还可以加一个数据倾斜的处理方法,加salt,小表join的时候广播,group by语句添加distribute by控制分布)
面试回答顺序:先用执行计划确认扫描、关联和排序成本,再查锁等待与 CPU、内存、磁盘 I/O;确认瓶颈后,选择分区裁剪、缓存、适量批量读写等方案。不是所有慢 SQL 都能靠加索引解决,也不是所有大表都需要分区。
- 分区:查询条件能裁剪掉不相关分区时才可能减少扫描;跨全部分区查询不一定更快。
- 缓存结果:适合高频重复查询,需要明确有效期、更新与失效规则。
- 批量更新:减少数据库往返,控制每批记录数和事务长度,避免长时间持锁。
- 资源配置:依据监控确认 CPU、内存、I/O 或连接瓶颈后再调整,而不是直接认定硬件不够。
Hive 数据倾斜:截图里的三个关键词怎么展开?
先定位:查看任务耗时和数据量,确认是否少数热点 key 让个别任务处理了远多于其他任务的数据。
- 加 salt:将热点 key 拆成多个子 key 分摊处理。聚合场景先按“原 key+salt”局部聚合,再去掉 salt 按原 key 汇总;SUM 可累加局部和,AVG 需要汇总 sum 和 count,不能直接平均各组平均值。JOIN 场景还要让另一侧正确匹配盐值,不能两边随意加随机数。
- 小表广播 / MapJoin:小表满足内存条件时,可用 MapJoin 减少大表 JOIN 的 Shuffle;需要关注加载后的内存开销与 JOIN 类型限制。参见 Hive JOIN 文档。
- DISTRIBUTE BY:按指定表达式分发数据,本身不保证解决 GROUP BY 倾斜。如果仍按同一个热点 key 分发,热点仍然集中;通常需要配合拆分热点和两阶段聚合。参见 Hive DISTRIBUTE BY 文档。
下面保留完整排查清单、执行计划示例和其他优化方式。
面试回答:
我会先确认慢的是数据库执行,还是连接池排队、网络传输、结果映射或整个接口。然后结合慢查询日志和调用链拿到真实 SQL、参数、耗时和调用次数,用 EXPLAIN 看访问路径;必要时在可控环境用 EXPLAIN ANALYZE 对比估算行数和实际执行情况。同时排查锁等待、长事务、CPU、磁盘 I/O 和缓存命中情况。
优化也要对症处理:减少扫描行数和返回字段,优化关联和分页,缩短事务,减少 Java 端 N+1 查询,按场景采用适量批处理、缓存、预聚合和读写分离。只有确认单实例容量或吞吐成为瓶颈后,才进一步考虑分库分表。
建议按以下顺序排查:
| 步骤 | 看什么 | 要回答的问题 |
|---|---|---|
| 确认现场 | SQL、绑定参数、表结构、数据量、发生时段、P95/P99 | 一直慢,还是偶发慢?所有参数慢,还是特定商户慢? |
| 分解接口耗时 | 获取连接、SQL 执行、结果读取、对象映射、下游调用 | 时间究竟花在哪里? |
| 分析执行计划 | key、访问方式、估算扫描行数、过滤比例、连接顺序、排序 | 是否扫描过多、重复访问或关联结果膨胀? |
| 对比实际执行 | 实际行数、循环次数、各节点耗时 | 估算是否失真,哪个节点放大了成本? |
| 检查并发等待 | 行锁、元数据锁、长事务、连接池队列 | SQL 本身快,是否主要在等待? |
| 检查资源 | CPU、磁盘时延、Buffer Pool、临时表、并发量 | 是否资源饱和或热点集中? |
| 验证改动 | 相同参数和负载下对比延迟、吞吐及资源消耗 | 是否真的改善,有没有伤害写入或其他查询? |
EXPLAIN
SELECT id, amount
FROM pay_order
WHERE merchant_id = 1001 AND status = 1;
-- 会实际执行查询,应在已评估负载的环境中使用。
EXPLAIN ANALYZE
SELECT id, amount
FROM pay_order
WHERE merchant_id = 1001 AND status = 1;
EXPLAIN ANALYZE 会真正运行语句并返回实际执行信息,不能把它当成无执行成本的静态分析。MySQL EXPLAIN
索引之外的优化方式:
| 方法 | 适用场景 | 需要注意 |
|---|---|---|
| 只查需要的列、限制结果量 | 大字段、多列、一次返回太多数据 | 减少数据库读取、网络和 Java 对象分配 |
| 优化 JOIN 和子查询 | 一对多关联后数据成倍增长 | 先明确关联基数,减少无意义的中间结果 |
| 游标分页 | 深分页列表 | 需要稳定排序键;不适合直接任意跳到第 N 页 |
| 批量查询和适量批量写入 | 循环查库、逐条网络往返 | 控制批次,避免超大 IN、长事务和长时间持锁 |
| 缩短事务 | 锁竞争和长事务 | 事务内避免远程调用、用户等待等慢操作 |
| 缓存 | 高频、重复且允许一定延迟的查询 | 定义失效规则,并处理穿透、击穿和一致性 |
| 预聚合、汇总表 | 仪表盘反复统计海量明细 | 明确更新周期,处理补数和重复计算 |
| 读写分离 | 读压力大且允许复制延迟 | 强一致查询和写后立即读不能随意读副本 |
| 冷热分离、归档 | 历史数据大,热点集中在近期 | 保证历史查询和恢复路径可用 |
| 分区或分库分表 | 维护成本、容量或吞吐达到瓶颈 | 分区要能裁剪;分表要能精确路由,否则收益有限 |
| 调整资源和配置 | 明确存在 I/O、内存等瓶颈 | 依据测量调整,避免只靠扩容掩盖低效 SQL |
深分页示例:
-- 偏移量很大时,通常仍需处理大量前置记录。
SELECT id, created_at
FROM pay_order
ORDER BY created_at, id
LIMIT 1000000, 20;
-- 用上一页最后一条记录的时间和 ID 继续向后查。
SELECT id, created_at
FROM pay_order
WHERE created_at > :last_time
OR (created_at = :last_time AND id > :last_id)
ORDER BY created_at, id
LIMIT 20;
游标分页仍需要匹配排序和过滤的索引,并用执行计划验证。额外的唯一 ID 用于解决时间相同的记录排序问题;并发修改下若要求严格一致的分页,还需要快照或固定查询边界。
关于截图中的 Hive 扩展:
如果岗位确实涉及 Hive,可以补充分区裁剪、减少 Shuffle、预聚合以及排查热点 key。加盐需要配套正确的二次聚合或 JOIN 处理;小表广播要求小表能装入内存。单纯增加 DISTRIBUTE BY 不能保证解决同一个热点 key 的倾斜。Java/MySQL 面试中应先回答上述主线,再根据追问展开大数据引擎。
5. A、B 已落入 15 日数据,C 只落入 14 日数据,如何保证展示包含 ABC?如何与上下游沟通?
截图底部第 5 条:参考答案与补充
截图参考答案(原文):
取一个区间的时间数据再聚合,如果一个区间内数据都不存在和上游反馈产出时间问题
这句话怎么理解:如果业务允许按周、按月或某个日期区间展示,可以统一 ABC 的业务日期范围再分别聚合,避免只看最新一天时某类数据还未到齐。发现区间缺失时,向上游核实任务产出时间、完成状态、调度延迟和补数计划。
适用条件:扩大区间不会自动补齐缺失数据。如果选 1—15 日,A、B 已完整到 15 日,C 只完整到 14 日,那么 C 的区间结果仍不完整。不能只因为 ABC 在区间里“都至少有一条记录”就宣布数据齐全。
假设 ABC 的 1—14 日数据都完整:
同口径区间统计:A、B、C 都统计 1—14 日。
页面标注:完整数据截至 14 日,15 日 C 待更新。
如果必须展示 1—15 日:
C 标记“待更新 / 区间不完整”,不要把缺失当作 0。
15 日全部到齐并校验后,再统一发布完整区间结果。
- 检查完整性:按业务日期、数据类型及批次检查完成状态,不能只看有没有记录;正常零业务量和未产出要区分。
- 聚合口径:订单金额、笔数等可加指标可以按统一区间汇总;每日余额等快照指标通常不能直接跨天相加,需要另定期末值或平均值口径。
- 与上游:确认业务日期、预计到数时间、完成标志、异常原因和补数时间;关键日期未完成或超时就沟通,不必等整个区间都没有数据。
- 与下游:确认优先完整性还是时效性,解释当前统计区间、缺失类型和更新计划;未经约定不要混合不同日期的数据计算合计或比例。
下面保留“最近完整日、各类最新、指定日期部分结果”三种模式,以及就绪状态表和发布流程。
题目整理:
上游数据落入过程正常,但各任务调度时间不同。A、B、C 三类数据每天产出,目前 A、B 已经有 15 日数据,C 只有 14 日数据。如何设计数据展示,使 ABC 都被合理呈现,并与上游数据负责人、下游查询方协商?
面试回答:
我会先确认业务要的是“同一天完整的 ABC”,还是“每类数据各自最新”。这是两种不同口径。
如果页面需要横向比较、求合计或计算比例,我会默认展示 ABC 都已完成的最近业务日期。题目中如果三类的 14 日数据都完整,就统一展示 14 日,并明确标注“完整数据截至 14 日,15 日 C 待更新”。等 15 日 ABC 全部到齐并校验成功后,再统一发布 15 日版本。
如果下游更重视时效性,可以展示 A、B 的 15 日数据和 C 的 14 日数据,但每类必须标明业务日期与更新时间,避免让用户误以为来自同一天。另一种方式是展示 15 日页面,把 C 明确标为“待更新”,不能当作零,也不能无提示地拿 14 日数据填充。
三种展示方式:
| 模式 | A | B | C | 适用场景 |
|---|---|---|---|---|
| 最近完整日,默认推荐 | 14 日 | 14 日 | 14 日 | 同口径报表、比较、总额和比例计算 |
| 各类最新 | 15 日 | 15 日 | 14 日,突出日期 | 各指标独立,优先时效性 |
| 指定日期的部分结果 | 15 日 | 15 日 | 待更新 | 用户明确要求查看 15 日进度 |
金额、件数等实际值取自业务数据,这里只展示日期口径。不同业务日期的数据不能不加说明地合计。
后端如何落地:
维护一张数据就绪状态表,每个业务日期、数据类型和发布版本都有状态。例如:
dataset_status
biz_date 业务日期
dataset_type A / B / C
release_version 统一发布版本
status LOADING / COMPLETE / FAILED
row_count 实际记录数
completed_at 完成时间
唯一键:(biz_date, dataset_type, release_version)
COMPLETE 必须表示数据已完整可见并通过校验,不能只因为插入了第一条记录就标记完成。正常的零条数据也可以是 COMPLETE,要与“尚未到数”区分开。
查找各版本中 ABC 都完成的最近日期,可以用下面的示意 SQL:
SELECT biz_date, release_version
FROM dataset_status
WHERE dataset_type IN ('A', 'B', 'C')
AND status = 'COMPLETE'
GROUP BY biz_date, release_version
HAVING COUNT(DISTINCT dataset_type) = 3
ORDER BY biz_date DESC, release_version DESC
LIMIT 1;
如果 A、B、C 使用各自独立的源版本号,则另建发布清单,将一个统一发布版本映射到三类源数据版本,而不是强行要求源版本号相同。
发布流程:
- 上游按业务日期和批次写入暂存区或不可变版本数据。
- 对记录数、必要字段、重复记录及关键业务总量进行校验,完成后标记该类数据 COMPLETE。
- 发布任务确认同一天 ABC 都完成,构建该版本展示结果。
- 数据准备完整后,原子切换“当前发布日期+版本”的指针。
- 一次页面请求固定使用同一发布版本,缓存键也包含版本,避免多个查询混用新旧数据。
- 迟到数据或历史修正生成新版本,校验后重新发布,保留可追踪的版本记录。
这是一种建议的系统设计,不依赖必须使用某一种调度平台。若上游支持任务依赖,优先让展示汇总任务依赖 ABC 的完成事件,不只依靠固定时间轮询。
为什么不能直接取三个 MAX 日期的最小值?
只有在每类数据按天连续完整产出、没有日期空洞时,min(max_date_A, max_date_B, max_date_C) 才能作为共同完成日期的简化算法。如果 A 有 13 日和 15 日,却缺少 14 日,这种算法可能错误地选中 14 日。
更可靠的方式是取三类“已完成业务日期集合”的交集,再选最近日期。若展示的是累计区间,还要检查区间内每天都完整,不能只检查结束日期。
与上游沟通:
- 统一业务日期、时区、统计截止点,以及增量数据还是全量快照。
- 约定每类数据最晚产出时间和完成信号,了解正常调度差异。
- 约定完整性校验方式,区分“当天业务为零”和“数据未到”。
- 明确迟到、失败、重跑、补数和数据修正的处理机制,以及超时告警和负责人。
可以这样说:“展示端需要同一天完整的 ABC。请提供每类数据的业务日期、完成状态和最晚到数时间;我们在三类完成后发布,超过约定时间则保留上一完整版本并告警。”
与下游沟通:
- 确认完整性和时效性的优先级,默认展示哪个口径。
- 确认可接受的延迟、缺失标识和页面提示。
- 确认是否允许跨日展示,以及哪些合计、比例必须禁止跨日计算。
- 约定接口返回
bizDate、releaseVersion、complete和各类更新时间,便于解释与追溯。
可以这样说:“当前 15 日 A、B 已完成,C 尚未完成。默认报表展示完整的 14 日数据;如果需要查看最新进度,可以切换到 15 日,并明确标记 C 待更新。”
对截图参考答案的补充:
“取一段时间的数据再聚合”只在业务本来就需要区间统计时成立。它不能自动解决同日数据不完整,也可能把缺失问题隐藏在汇总中。区间聚合仍需要统一业务日期范围、定义完成条件并检查缺失。
6. LeetCode 206:反转链表
题目:
给定一个单链表的头节点 head,反转链表,返回反转后的头节点。假设输入为无环单链表。
输入:1 → 2 → 3 → 4 → 5 → null
输出:5 → 4 → 3 → 2 → 1 → null
面试回答:
使用迭代法,维护 prev 和 curr。遍历时先保存当前节点的原后继,再把当前节点的 next 指向 prev,最后推进两个指针。遍历结束后,prev 就是新头节点。关键是先保存 next,避免断开链表后丢失剩余节点。
Java 实现,推荐迭代:
class ListNode {
int val;
ListNode next;
ListNode(int val) {
this.val = val;
}
}
class ReverseListSolution {
public ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode curr = head;
while (curr != null) {
ListNode next = curr.next; // 先保存原后继
curr.next = prev; // 反转当前指针
prev = curr; // 推进已反转部分
curr = next; // 继续处理剩余部分
}
return prev;
}
}
复杂度: 时间 O(n),额外空间 O(1)。直接修改原链表,不创建新节点。
递归写法,可用于追问:
class RecursiveReverseListSolution {
public ListNode reverseList(ListNode head) {
if (head == null || head.next == null) {
return head;
}
ListNode newHead = reverseList(head.next);
head.next.next = head;
head.next = null; // 断开原方向,避免形成环
return newHead;
}
}
递归时间 O(n),调用栈空间 O(n)。长链表可能栈溢出,不能把递归解法说成 O(1) 空间。
需要覆盖的边界: 空链表返回 null;单节点返回原节点;双节点正确交换;反转后原头节点的 next 为 null;反转两次恢复原连接顺序。
7. LeetCode LCR 144:翻转二叉树
题目:
给定二叉树的根节点 root,将整棵树左右镜像翻转,返回根节点。
翻转前: 翻转后:
4 4
/ \ / \
2 7 7 2
/ \ / \ / \ / \
1 3 6 9 9 6 3 1
面试回答:
对每个节点交换左右子树,再递归翻转它的两个子树。遇到空节点返回 null。每个节点只处理一次,时间 O(n),递归栈空间 O(h),h 是树高;平衡树时 O(log n),最坏退化成链时 O(n)。
Java 实现,递归:
class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int val) {
this.val = val;
}
}
class MirrorTreeSolution {
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
TreeNode temp = root.left;
root.left = root.right;
root.right = temp;
mirrorTree(root.left);
mirrorTree(root.right);
return root;
}
}
Java 实现,迭代 BFS:
class IterativeMirrorTreeSolution {
public TreeNode mirrorTree(TreeNode root) {
if (root == null) {
return null;
}
java.util.Deque<TreeNode> queue = new java.util.ArrayDeque<>();
queue.offer(root);
while (!queue.isEmpty()) {
TreeNode node = queue.poll();
TreeNode temp = node.left;
node.left = node.right;
node.right = temp;
if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
return root;
}
}
迭代时间 O(n),队列空间 O(w),w 为最大层宽,最坏 O(n)。ArrayDeque 不允许 null,因此入队前要判空。
容易出错的地方:
- 只交换根节点的两个孩子,没有继续处理每个子树。
- 先覆盖左子树引用,再拿它给右子树赋值,导致原子树丢失;需要临时变量。
- 把翻转二叉树理解成交换节点值;题目需要改变左右子树连接关系。
- 忽略退化树上的递归栈深度。
需要覆盖的边界: 空树、单节点、只有左孩子、只有右孩子、不对称树,以及翻转两次恢复原树结构。
面试前快速复习
| 题号 | 记忆重点 |
|---|---|
| 3 | 围绕 SQL 设计索引;区分无法定位、部分使用和成本选择;用执行计划验证,避免绝对化结论 |
| 4 | 先定位耗时,再看执行计划、锁等待和资源;减少数据访问、网络往返与事务持锁时间 |
| 5 | 先确认业务口径;共同完成日期、就绪状态、统一版本发布、缺失提示和上下游 SLA |
| 6 | 保存 next → 反转指针 → 推进 prev/curr;迭代 O(n) 时间、O(1) 空间 |
| 7 | 每个节点交换左右子树;递归 O(n) 时间、O(h) 栈空间;BFS 用队列 |