想象你管理着一座巨型城市图书馆,馆内藏书 2000 万册,每天有几十万读者进出。糟糕的是,这座图书馆没有检索目录——管理员找一本书要从第一排书架逐本翻到最后一排,运气差时一整天都耗在一本书上。这就是“没有索引的数据库”:每次查询都是一次全表扫描,每一行记录都要读一遍。 后来你痛定思痛,先做了一套「分类编号检索目录」,读者先查目录、再按书架号取书,三五步就能定位目标。再后来,图书馆单层放不下了,你按读者卡尾号把藏书分流到 8 个分馆(分库分表),让总馆只负责入库登记(写),各分馆开放阅览(读)——这就是读写分离。于是一座吞吐百万读者、毫秒级响应的“城市图书馆系统”诞生了。

岗位关联

对 AI 从业者而言,数据库是所有 AI 应用的数据底座。掌握索引、分库分表与读写分离,就是掌握高并发系统的地基工程能力,这是后端架构师、分布式系统工程师与技术专家的必修内功。

目录
  1. 可视化演示
  2. 索引寻址原理
  3. 索引设计策略
  4. 慢查询优化
  5. 分库分表策略
  6. 中间件方案
  7. 读写分离
  8. 高可用架构
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

展示数据库优化三模式:B+Tree索引结构(根-中间-叶子三层+叶子链表+四种索引类型对比+查询性能Before/After)、分库分表策略(垂直分库UDB/ODB/PDB/IDB+水平分表Hash/Range/Consistent/Geo四策略+数据分布柱状图)、读写分离架构(APP-PRX-MST主库写+S0-S2从库读+主从复制+读写比例80/20)。

本节配有交互动效(进入视口后自动加载,约 3KB)。

B+Tree索引结构与查询优化 B+Tree三层结构:根节点ROT→中间层N0-N2→叶子层L0-L5链表连接 BT/HS/FN/CPT四种索引类型对比,B+Tree范围查询最优90% IO = O(log N), 索引加速8x Before 120 QPS → After 950 QPS,加速7.9x

索引寻址原理

走进那座藏书 2000 万册的图书馆,入口处最显眼的是一面巨大的卡片目录柜。每张卡片写着「分类号—书架编号—书名」,你要找一本《分布式系统设计》,管理员按「计算机类 → 分布式系统 → 设计模式」三级分类,两三分钟就能定位到 B2-15-07 号书架。没有这套目录,管理员只能从第一排书架开始逐本翻阅——这正是全表扫描的真实写照。

数据库索引就是图书馆的“卡片目录柜”。最常见的 B+ 树索引,结构上像三级分类体系:根节点相当于一级分类(计算机/文学/历史),中间层节点相当于二级分类(分布式/网络/数据库),叶子节点保存数据行的主键或物理地址。查询时从根节点逐层下探,每层只需一次磁盘 I/O 就能确定走向哪个分支,而不是扫描整本书架。

B+ 树最精妙的设计是:所有数据都存放在叶子节点,非叶子节点只存“键值+指针”。这样整棵树保持“矮胖”形态,高度只有 3~4 层时就能容纳千万级数据;叶子节点之间用链表相连,范围查询(如“价格 100~200 元的商品”)可以沿链表顺序扫描,无需回溯上层。MySQL 的 InnoDB 引擎中,主键索引就是聚簇索引——表数据按照主键物理排序存放,二级索引的叶子存的是主键值,查到后还要“回表”再取一次完整行。

回到图书馆的场景:你不需要翻遍 2000 万张卡片,只需在三五层目录内逐级下钻。这正是“以空间换时间”——多放一排目录柜,换来读者几秒钟的精准定位。索引的本质,就是用额外的存储空间,换取数量级级别的查询速度提升。

L=⌈log⁡FN⌉\displaystyle L = \left\lceil \log_F N \right\rceil
公式通俗说明:B+ 树的层数 L 约等于以扇出 F 为底、对数据总量 N 取对数。N 越大树越深,但 F 很大时,千万级数据也只需 3~4 层,这就是索引查找极快的原因。
  1. N(数据总行数)表里一共要索引多少行记录,比如 2000 万条用户数据。
    N=20,000,000\displaystyle N = 20{,}000{,}000
  2. F(扇出)一个节点最多能伸出多少个子分支;MySQL 一个 16KB 页约能存 1000 个键值对。
    F=1000\displaystyle F = 1000
  3. log_F N(取对数)把 N 不断均分成 F 份,需要分多少层。
    log⁡100020,000,000≈2.4\displaystyle \log_{1000} 20{,}000{,}000 \approx 2.4
  4. \lceil \cdot \rceil(向上取整)层数必须是整数,算出 2.4 层也要按 3 层算。
    L=⌈2.4⌉=3\displaystyle L = \lceil 2.4 \rceil = 3

索引设计策略

图书馆的目录柜并非随意摆放,而是按「作者姓氏 → 书名首字母 → 出版年份」三级顺序排列的。读者查书时必须从第一级“作者姓氏”开始,跳过作者直接查书名,目录管理员就无从下手。这种“必须从最左边开始匹配”的规则,正是联合索引的最左前缀原则。

假设我们建了一个联合索引 (a, b, c),它相当于一本按 a、b、c 三个字段排序的字典。查询条件 WHERE a = 1 AND b = 2 AND c = 3 能完整走索引;而 WHERE a = 1 AND c = 2 只能用到 a 列,因为 b 列被跳过,c 列无法继续匹配;WHERE b = 1 AND c = 2 则完全用不上索引。设计联合索引时,要把等值查询频率最高的列放在最左边,把范围查询列放在右边。

覆盖索引则更进一步:如果查询所需的列全部包含在某棵索引树内,数据库就直接从索引里取数,连“回表”这一步都省了。比如索引 (user_id, name) 上执行 SELECT name FROM t WHERE user_id = 100,数据全部在索引中,无需回表。但索引并非越多越好——每多一个索引,写入时就要多维护一棵 B+ 树,相当于图书馆每本新书都要多抄一份目录卡片。

目录卡片多了,书架反而被挤占;卡片排序乱了,读者反而找不到书。索引设计的精髓就是“少而精”:用最少的索引覆盖最高频的查询路径,让每一棵 B+ 树都成为读者真正会去翻的那张卡片。

Costquery≈Nrows_scanned×Csingle\displaystyle \text{Cost}_{\text{query}} \approx N_{\text{rows\_scanned}} \times C_{\text{single}}
公式通俗说明:一次查询的代价约等于“扫描行数 × 每行处理成本”。索引优化的一切努力,都是为了让扫描行数 N 从全表大小降到目标记录数附近。
  1. N_rows_scanned(扫描行数)优化器预估需要扫描多少行,EXPLAIN 的 rows 列可以看到。
    Nrows_scanned=200,000(全表)\displaystyle N_{\text{rows\_scanned}} = 200{,}000 \text{(全表)}
  2. C_single(单行成本)比较一行数据所需的 CPU 与内存开销,通常视为常数。
    Csingle=0.01ms\displaystyle C_{\text{single}} = 0.01 \text{ms}
  3. Cost_query(总代价)两者相乘得到查询总开销。
    Cost=200,000×0.01=2000ms\displaystyle \text{Cost} = 200{,}000 \times 0.01 = 2000 \text{ms}
  4. 索引的目标把扫描行数降到目标行附近,总代价骤降。
    Costwith_idx=3×0.01=0.03ms\displaystyle \text{Cost}_{\text{with\_idx}} = 3 \times 0.01 = 0.03 \text{ms}

慢查询优化

图书馆前台放着一本《找书超时登记本》,每次查书超过 5 秒就会被记录在案,管理员每天翻看这本子,找出哪些书架、哪些查询方式浪费了读者时间。数据库里的“慢查询日志”正是这样一本登记本——它记录下所有执行时间超过阈值的 SQL,是性能优化的第一手线索。

定位到慢 SQL 后,数据库提供了“执行计划体检表”:EXPLAIN。它像图书管理员的路线图,展示这条 SQL 会走哪些表、用哪个索引、扫描多少行。重点关注三列:type 是否出现 ALL(全表扫描)、key 是否命中索引、rows 预估扫描行数是否异常大。若发现某条 SQL 扫描了 20 万行只返回 1 行,就要立刻检查索引是否建对。

SQL 改写是常见优化手段。把 SELECT * 改成只查需要的列;把 IN 子查询改写成 JOIN,更能利用索引;在 LIKE '%keyword%' 这种无法走索引的查询里,换成前缀匹配 LIKE 'keyword%' 就能命中索引;避免在索引列上做函数运算,例如 WHERE DATE(create_time) = '2024-01-01' 改成 create_time >= ... AND create_time < ... 的范围写法。

慢查询优化的本质,就是拿着登记本找到耗时最大的那个环节:如果 T_io 是 900ms,就建索引;如果 T_queue 是 800ms,就去查锁和连接池。像管理员一样,把每一毫秒都花在刀刃上。

Tquery=Tqueue+Texec+Tio+Tnetwork\displaystyle T_{\text{query}} = T_{\text{queue}} + T_{\text{exec}} + T_{\text{io}} + T_{\text{network}}
公式通俗说明:一条慢 SQL 的耗时由排队、执行、磁盘 I/O、网络四段组成。优化要针对占比最大的环节,索引主要压减 T_io,改写 SQL 压减 T_exec。
  1. T_queue(排队等待)连接池和锁等待占用的时间,并发高时占比飙升。
    Tqueue=20ms\displaystyle T_{\text{queue}} = 20 \text{ms}
  2. T_exec(执行计算)排序、分组、函数计算等 CPU 消耗。
    Texec=15ms\displaystyle T_{\text{exec}} = 15 \text{ms}
  3. T_io(磁盘读写)扫描数据页的 I/O 时间,全表扫描时占大头。
    Tio=900ms\displaystyle T_{\text{io}} = 900 \text{ms}
  4. T_network(传输返回)把结果集传回客户端的时间。
    Tnetwork=5ms\displaystyle T_{\text{network}} = 5 \text{ms}

分库分表策略

图书馆单层楼再也放不下 2000 万册书了。馆长决定开分馆:把藏书按读者卡尾号 0~7 分成 8 个分馆,尾号 0 的去第一分馆,尾号 1 的去第二分馆……读者拿卡一刷,就知道该往哪走。数据库的“分库分表”正是这个逻辑——当单表数据量过大、写入和查询都变慢时,把数据拆到多个库、多张表中去。

拆分有两种姿势。垂直拆分就像按楼层分大类:一层放计算机书、二层放文学书——在数据库里就是把一张列很多的宽表,按业务边界拆成“用户基础表”和“用户扩展表”,减少单行宽度、让每页容纳更多行。水平拆分则像同一类书分多个书架:把 2000 万条用户记录,按某个分片键散列到 user_0、user_1、user_2…… 多张结构完全相同的表里,每张表只有 250 万行。

分片键的选择决定一切。它必须是查询频率最高的条件,且分布足够均匀。比如按 user_id 分片,WHERE user_id = 1024 就能直接定位到对应分片——这称为“数据亲和”;如果按 id 分片却经常按 order_no 查询,就不得不广播到所有分片再聚合,反而更慢。容量规划上,业界经验是单表超过 500 万行或单表超过 10GB 时,就该认真考虑水平拆分。

分馆开起来后,每个分馆的藏书量只有原来的八分之一,找书、上架都快了好几倍。分库分表的核心就是“把大问题拆小”——用一台机器扛不住的容量,交给多台机器一起扛。

node_id=hash(shard_key) mod N\displaystyle \text{node\_id} = \text{hash}(\text{shard\_key}) \bmod N
公式通俗说明:把分片键(如 user_id)做哈希后对分片数 N 取模,得到的余数就是数据应该去的分片编号。
  1. shard_key(分片键)决定数据归属的字段,必须是高频查询条件。
    shard_key=user_id=1024\displaystyle \text{shard\_key} = \text{user\_id} = 1024
  2. hash()(哈希函数)将任意长度的键映射为整数,让数据分布更均匀。
    hash(1024)=3345678\displaystyle \text{hash}(1024) = 3345678
  3. mod N(取模)对分片总数取余,得到 0~(N-1) 的分片编号。
    3345678 mod 8=6\displaystyle 3345678 \bmod 8 = 6
  4. node_id(目标分片)数据落到第 6 号分片。
    node_id=6\displaystyle \text{node\_id} = 6

中间件方案

分馆开多了,读者站在总馆大厅一脸茫然:“我这本书到底在哪个分馆?”于是馆长设了一个“总咨询台”——读者只需对咨询台报出书名,咨询台立刻告诉你该去第几馆、第几层。数据库中间件就是这样一个统一路由的咨询台,让应用无需关心数据散落在哪张表。

市面上主流方案有两类。ShardingSphere 属于“应用内路由”:它作为客户端 SDK 嵌入业务代码,SQL 经过它解析后自动路由到真实分片,Java 生态使用最广;MyCat 则是“独立代理网关”:应用先连 MyCat,再由 MyCat 转发到后端分片,对业务代码透明,支持多语言。选择时,团队以 Java 为主可优先 ShardingSphere,异构系统多则 MyCat 更灵活。

分片后最大难题是主键:多个分片各自自增会产生重复。雪花算法(Snowflake)是工业界标准解法——用一个 64 位长整型做主键,高 41 位存时间戳、中间 10 位存机器 ID、低 12 位存同一毫秒内的序列号,保证全局唯一且趋势递增。平滑迁移则讲究“双写+校验”:新老库同时写入,跑对比任务校验两边数据一致后,再把读流量逐步切到新库。

有了总咨询台,读者不需要记住每个分馆的藏书目录,应用层也不需要感知分片细节。中间件把“数据在哪里”的复杂度封装起来,让分库分表成为一件“对上层透明”的工程决策。

ID64=(timestamp−T0)≪41  ∣  machine_id≪10  ∣  sequence\displaystyle \text{ID}_{64} = (\text{timestamp} - T_0) \ll 41 \;|\; \text{machine\_id} \ll 10 \;|\; \text{sequence}
公式通俗说明:雪花算法就是把“时间戳、机器编号、毫秒内序列”三段信息拼成一个 64 位数字,任何一台机器在任何时刻生成的 ID 都不重复。
  1. 时间戳段(41 位)当前时间与起始时间 T0 的毫秒差,保证 ID 随时间递增。
    timestamp−T0=1,712,345,678,901\displaystyle \text{timestamp} - T_0 = 1{,}712{,}345{,}678{,}901
  2. 机器段(10 位)5 位机房号 + 5 位机器号,区分不同节点。
    machine_id=3\displaystyle \text{machine\_id} = 3
  3. 序列段(12 位)同一毫秒内从 0 递增到 4095,单机每毫秒可生成 4096 个 ID。
    sequence=205\displaystyle \text{sequence} = 205
  4. 位或拼接三段向左移位后按位或,合成一个 64 位整数。
    ID=688126570126438429\displaystyle \text{ID} = 688126570126438429

读写分离

图书馆原来的设计有个大问题:读者来借书要排队登记,管理员既要办借阅手续、又要回答“这本书在哪个书架”的咨询,两条队伍堵在一起。馆长决定改造:总馆只负责入库登记和写数据,各分馆专供读者阅览查询——这就是读写分离。

实现依赖“主从复制”机制:主库(Master)负责处理 INSERT/UPDATE/DELETE,每次写入都会记录到 binlog(二进制日志);从库(Slave)通过 IO 线程拉取 binlog、由 SQL 线程回放,从而与主库保持数据同步。应用层再配一个读写路由——写请求强制走主库,读请求由负载均衡分发到多个从库,主库的压力骤减。

但复制不是瞬时的,binlog 从主库传到从库、再回放完成需要时间,称为“主从延迟”。若延迟 200ms,用户刚下单就去从库查订单,可能查不到新记录。应对方案有很多:把“关键读”强制路由到主库、从库延迟过高时自动摘除、使用半同步复制让主库等待至少一个从库确认收到 binlog 再提交。

登记和咨询分开后,借书队伍不再互相堵路。读写分离的本质是“读写分流、各司其职”——写路径保持强一致,读路径追求高吞吐,中间用一个小小的延迟窗口换来了整体系统数倍的并发能力。

ΔT延迟=T主库写入+T日志传输+T从库回放\displaystyle \Delta T_{\text{延迟}} = T_{\text{主库写入}} + T_{\text{日志传输}} + T_{\text{从库回放}}
公式通俗说明:从主库写一条数据,到从库能读到这条数据,中间隔着三步耗时。这个窗口就是主从延迟,也是读写分离架构的核心权衡点。
  1. T_主库写入主库执行写入并生成 binlog 的时间。
    Twrite=5ms\displaystyle T_{\text{write}} = 5 \text{ms}
  2. T_日志传输binlog 通过网络从主库传输到从库。
    Ttransfer=10ms\displaystyle T_{\text{transfer}} = 10 \text{ms}
  3. T_从库回放从库执行 binlog 中 SQL 的时间。
    Tapply=15ms\displaystyle T_{\text{apply}} = 15 \text{ms}
  4. 延迟窗口三者叠加,通常为几十毫秒。
    ΔT=30ms\displaystyle \Delta T = 30 \text{ms}

高可用架构

深夜,总馆突然电路起火,整个借阅登记系统瘫了。好在馆长早有预案:对面大楼里设了一个“备用总馆”,平时同步着总馆的全部数据。火灾发生后,监控系统 30 秒内自动探测到总馆不可用,立刻把备用总馆提升为新主馆,读者几乎无感知地继续借书——这就是故障转移。

高可用架构的核心是“消除单点”。双主互备(Master-Master)让两台机器互为主备,任一宕机另一台立即接管;更常见的是“一主多从 + 自动选主”,借助 ZooKeeper 或 Raft 协议从多个从库中选举出新主库,配合 VIP 漂移或配置中心实现应用层无感知切换。切换后从库自动重连新主库,继续同步数据。

备份与监控是高可用的另一条腿。备份策略通常是“全量+增量”:每周日全量备份,每天做 binlog 增量备份,既能恢复到任意时间点,又能缩短故障时数据丢失窗口。监控则盯住四个黄金指标:主从延迟、活跃连接数、慢查询数量、磁盘使用率——延迟突增往往预示着复制故障,连接数打满则是雪崩的前兆。

图书馆从火灾中毫发无损地站了起来,读者甚至不知道昨夜发生过意外。高可用架构的真正含义,是让系统故障从“事故”变成“演练”——用自动化的检测与切换,守住数据库作为数据底座的最后一道防线。

A=MTBFMTBF+MTTR×100%\displaystyle A = \frac{\text{MTBF}}{\text{MTBF} + \text{MTTR}} \times 100\%
公式通俗说明:可用性 = 平均无故障时间 ÷(平均无故障时间 + 平均修复时间)。MTBF 越长、MTTR 越短,系统越接近 99.999% 的高可用。
  1. MTBF(平均无故障时间)系统连续正常运行的平均时长。
    MTBF=720小时\displaystyle \text{MTBF} = 720 \text{小时}
  2. MTTR(平均修复时间)从故障发生到恢复服务的平均时长。
    MTTR=0.5小时\displaystyle \text{MTTR} = 0.5 \text{小时}
  3. 比值计算两者代入公式得到可用性。
    A=720720+0.5≈99.93%\displaystyle A = \frac{720}{720 + 0.5} \approx 99.93\%
  4. 目标逼近要达 99.99%,需把 MTTR 压到分钟级甚至秒级。
    A目标=99.99%\displaystyle A_{\text{目标}} = 99.99\%

AI 应用场景

电商大促订单系统

阶段:数据存储与高并发访问

解决的问题:秒杀场景下订单表写入量激增,单库单表无法承受读写压力;商品详情页高频读取却只有一份数据。传统单库方案在 618、双11 期间必然雪崩。

具体案例:某电商平台将订单表按 user_id 水平分成 64 张表,写入压力分散到 8 个数据库节点;商品详情走读写分离,主库负责库存扣减,8 个从库承接详情页流量。大促压测时,下单链路 P99 延迟从 800ms 降到 120ms。

大模型特征存储服务

阶段:在线推理前的特征读取

解决的问题:推荐模型在线打分需在 50ms 内读取用户的上千维特征。特征表按 user_id 分片存储,避免单表过大导致查询超时;读多写少场景用读写分离把训练产出的特征异步同步到从库。

具体案例:某短视频推荐系统将用户特征表按 user_id 水平拆分为 16 片,主库接收特征更新任务,4 个从库承接每秒 10 万次的推理读取请求。特征读取 P99 稳定在 20ms 以内,支撑模型实时个性化推荐。

金融交易流水查询

阶段:合规风控与审计数据服务

解决的问题:交易流水表每天新增上亿行,监管要求按账号快速拉取流水。按 account_id 分片保证单账号数据落在同一分片,配合读写分离让审计查询走从库,不干扰在线交易写入。

具体案例:某支付平台把 3 年交易流水按 account_id 分为 32 个分片,风控引擎批量拉取用户流水进行反欺诈分析时,直接定位到对应分片并行查询,全量审计任务耗时从小时级降到分钟级。

Python 演示

python62 行
import hashlibimport randomimport time  class UserTable:    """模拟 20 万行用户表的查找过程。"""     def __init__(self, size=200000):        self.size = size        self.rows = list(range(size))        self.index = {}        for position, user_id in enumerate(self.rows):            self.index[user_id] = position     def scan_lookup(self, user_id):        """全表扫描:从头逐个比较,相当于没有索引。"""        for position, current in enumerate(self.rows):            if current == user_id:                return position        return -1     def index_lookup(self, user_id):        """索引查找:字典相当于哈希索引,O(1) 定位。"""        return self.index.get(user_id, -1)  def shard_route(shard_key, shard_count):    """分片路由:对分片键做稳定哈希后取模。"""    digest = hashlib.md5(str(shard_key).encode("utf-8")).hexdigest()    return int(digest, 16) % shard_count  def bench_lookup(table, target_id, mode):    start = time.perf_counter()    if mode == "scan":        pos = table.scan_lookup(target_id)    else:        pos = table.index_lookup(target_id)    elapsed = (time.perf_counter() - start) * 1000    return pos, elapsed  def main():    table = UserTable(200000)    target = random.randint(0, table.size - 1)     pos1, t1 = bench_lookup(table, target, "scan")    pos2, t2 = bench_lookup(table, target, "index")     print("全表扫描: 找到位置 %d, 耗时 %.2f ms" % (pos1, t1))    print("索引查找: 找到位置 %d, 耗时 %.2f ms" % (pos2, t2))    print("索引比全表扫描快 %.1f 倍" % (t1 / t2))     print("--- 分片路由演示 ---")    for user_id in [1024, 65536, 199999]:        node = shard_route(user_id, 8)        print("用户 %d -> 分片 %d" % (user_id, node))  if __name__ == "__main__":    main()
目的:用 Python 模拟数据库全表扫描与索引查找的性能差距,并演示分片路由算法。
  1. self.index = {}用 Python 字典模拟数据库索引,键是 user_id,值是行号。
  2. for position, current in enumerate(self.rows)全表扫描需要逐行比较,时间复杂度 O(n)。
  3. self.index.get(user_id, -1)索引查找直接按哈希定位,时间复杂度 O(1)。
  4. hashlib.md5(str(shard_key).encode("utf-8"))对分片键做稳定哈希,保证同一键每次路由结果一致。
  5. int(digest, 16) % shard_count取模运算得到分片编号,使数据均匀散列到各分片。
  6. time.perf_counter()高精度计时,对比两种查找方式的实际耗时差异。

常见误区

误区既然索引能加速查询,那就给所有常用字段都建上索引,反正多建几个也不吃亏。

正解索引像图书馆的目录卡片,书本身只有 2000 万册,目录卡片却有 100 套,书架空间被大量挤占。每次插入、更新、删除数据,都要同步维护每一棵 B+ 树,写性能会急剧下降。正确的做法是只给高频查询路径建索引,用 EXPLAIN 验证索引真的被用到。

为何常见:初学者只看到索引对读的加速,忽略了写放大和存储开销,以为“免费午餐”。

误区数据库查询慢,第一时间就想着分库分表,觉得这是大厂标配的终极方案。

正解绝大多数慢查询问题出在索引设计或 SQL 写法上,先做索引优化和 SQL 改写,再看是否需要缓存。分库分表是“大手术”:分片后跨片查询、分布式事务、全局 ID 都变得复杂。单表未超过 500 万行且写入压力不大时,分库分表弊大于利。

为何常见:看到技术社区里大厂分享分库分表实践,误以为这是性能优化的第一步,其实是他们最后一步。

误区主从复制是同步的,主库写入成功,从库马上就能查到这条数据。

正解主从复制要经历“主库写 binlog → 网络传输 → 从库回放”三步,通常有几十毫秒延迟,高并发下可能更长。用户刚下单就去从库查,很可能查不到。解决方法是关键读路由到主库,或用半同步复制缩短延迟窗口。

为何常见:从主库视角看写入已成功,容易忽视从库回放还需要时间,这是分布式系统“最终一致性”直觉盲区。

误区EXPLAIN 里出现全表扫描(type=ALL)就一定该骂,必须想方设法加索引消灭它。

正解如果表只有几百行数据,全表扫描只读几个数据页,比走索引还快——索引查找本身也有根节点到叶子节点的多次 I/O 开销。优化器会基于表行数和数据分布做代价估算,小表全表扫描反而是最优解。要关注的是“大表上的全表扫描”。

为何常见:教程里反复强调“避免全表扫描”,被当成绝对真理,忽略了代价模型这个前提。

误区只要把数据散到多个分片就行,选哪个字段当分片键差别不大。

正解分片键必须同时满足“高频查询条件”和“数据分布均匀”。按 user_id 分片后,所有用户维度的查询都亲和;但如果业务主要按 order_no 查,而表按 user_id 分片,每个查询都要广播到所有分片再聚合,性能比不分片还差。选错分片键是分库分表最大的返工原因。

为何常见:只从“写入能不能分散”角度考虑,忽略了查询路由的亲和性才是分片键的第一使命。

面试题

Q1. 关于 B+ 树索引,下列说法正确的是?

单选题 · easy

  • A.非叶子节点也存储数据行
  • B.所有数据都存在非叶子节点
  • C.叶子节点链表相连可范围扫描← 正确答案
  • D.B+ 树本质是哈希索引结构

答案:C。B+ 树所有数据都在叶子节点,非叶子只存键与指针,故 A、B 错;B+ 树是树形结构而非哈希,D 错。叶子链表相连让范围查询可沿链表顺序扫描,C 正确,如同图书馆卡片目录按编号串成一串,连续取书不用回目录柜。

Q2. 数据库“慢查询日志”记录什么?

单选题 · easy

  • A.慢 SQL 语句的备份文件
  • B.执行超时阈值的 SQL 记录← 正确答案
  • C.数据库服务的错误日志
  • D.所有用户的操作记录

答案:B。慢查询日志记录执行时间超过 long_query_time 阈值的 SQL 语句,是性能优化的第一手线索。它不备份 SQL、不记录错误、也不记录所有操作,B 正确。就像图书馆的“找书超时登记本”,只记那些异常慢的找书过程。

Q3. 联合索引 (a,b,c) 中,查询 WHERE a=1 AND c=2 的效果?

单选题 · mid

  • A.完全无法利用该索引
  • B.只能利用索引的 a 列← 正确答案
  • C.索引的 a 和 c 列都用
  • D.索引全部列都能使用

答案:B。最左前缀原则要求从最左边开始连续匹配。a=1 能走索引,但 c=2 前面跳过了 b 列,无法继续匹配,因此只能用 a 列。如同查字典必须从首字母开始,跳级查询是查不到的。

Q4. 分片数从 8 扩展到 16,旧数据如何?

单选题 · mid

  • A.取模函数自动适应正确
  • B.需要重新路由分布数据← 正确答案
  • C.只把新增数据写入新片
  • D.将所有数据复制到全部分片

答案:B。取模路由是 hash(key) % N,N 从 8 变 16 后,几乎所有数据的余数都变了,必须重新计算并迁移旧数据(resharding)。这是分库分表扩容时最复杂的工程问题,常见解法是双写迁移加校验。

Q5. 主从复制延迟会造成什么?

单选题 · mid

  • A.主库的写入立刻失败
  • B.从库可能会读到旧数据← 正确答案
  • C.从库进程自动崩溃退出
  • D.主库拒绝所有的读请求

答案:B。binlog 从主库传输到从库并回放需要时间,这个窗口内读从库会读到旧数据,B 正确。主从延迟不影响主库写入,也不会让从库崩溃,更不会影响主库读服务。想象新书上架有时间差,早到的读者看不到最新的书。

Q6. 覆盖索引的最大优势是什么?

单选题 · hard

  • A.显著减少索引占空间
  • B.查询无需回表取整行← 正确答案
  • C.防止索引被意外删除
  • D.索引可自动修复数据

答案:B。覆盖索引指查询所需列全部在索引树中,查完索引直接返回,省去回表取完整行的磁盘 I/O。它并不减少索引空间,也不涉及索引删除保护或数据修复。如同目录卡片上直接印着摘要,读者不必再去书架搬书。

Q7. 垂直拆分与水平拆分的区别?

单选题 · mid

  • A.垂直按行拆分水平按列
  • B.垂直按列分水平按行分← 正确答案
  • C.两种拆分概念完全相同
  • D.垂直拆分无需分片键

答案:B。垂直拆分是把一张宽表按业务拆成多张窄表(按列拆),减少单行宽度;水平拆分是把大数据量按分片键散列到多张同构表(按行拆)。两者完全不同,垂直拆分也涉及业务边界设计。

Q8. 哪些情况会导致索引失效?

多选题 · mid

  • A.对索引列使用函数运算← 正确答案
  • B.发生隐式类型转换时← 正确答案
  • C.不满足最左前缀匹配← 正确答案
  • D.使用 SELECT * 查询时

答案:ABC。对索引列用函数(如 DATE(create_time))、隐式类型转换(如 varchar 列与数字比较)、不满足最左前缀,都会让优化器放弃索引。SELECT * 不会导致索引失效,只是可能增加回表和网络传输开销。

Q9. 分库分表后要面对哪些新问题?

多选题 · hard

  • A.跨分片的 JOIN 很难做← 正确答案
  • B.分布式事务复杂度上升← 正确答案
  • C.全局唯一 ID 需要方案← 正确答案
  • D.所有 SQL 无需任何改动

答案:ABC。数据分散后,跨分片 JOIN 需要应用层聚合,分布式事务需要引入 Seata 等方案,自增主键在各分片会重复、必须用雪花算法等方式生成全局唯一 ID。SQL 必须改造以支持分片键路由,D 错误——中间件只是让改造更透明,并非零改动。

Q10. 主从高可用方案通常包含?

多选题 · hard

  • A.故障检测与主从切换← 正确答案
  • B.定期备份与恢复演练← 正确答案
  • C.主从延迟与连接数监控← 正确答案
  • D.从库禁止承接读请求

答案:ABC。高可用需要故障检测与自动选主、备份与恢复演练、延迟和连接数监控三要素。从库承接读流量正是读写分离的核心,不仅能用,还应做负载均衡,D 错误。它让主库专注写,从库分摊读,整体可用性更高。