CHAPTER 14 / DATABASES

第 14 篇 · 数据库与存储深度

本篇用负载形状选引擎:文档/向量、分布式 SQL 与列存 OLAP,再到图/时序/CDC——引擎边界先于 ORM 习惯。

骨架见 M10。本篇只补领域细枝,框在 核心方法论 · 开篇。

题目数:14 | 高级 ×4 | 架构 ×7 | 专家 ×3 | 预计复习:270 分钟
核心知识链

多模存储主线是「文档与向量检索 → HTAP/分布式 SQL 与列存分析 → 图与时序边界 → CDC 管道与慢 SQL 值班」。

  1. 文档模型与事务/分片:MongoDB 文档建模、分片路由与多文档事务边界(C14.01)。
  2. 向量检索族:Atlas Vector Search、Milvus 十亿级索引、Qdrant 过滤检索——ANN 参数与元数据过滤决定召回(C14.02–04)。
  3. 分布式 SQL / HTAP / 列存:TiDB Raft HTAP、Cockroach/OceanBase 等 NewSQL 选型、ClickHouse 与 Doris/StarRocks OLAP 取舍(C14.05/06/08/11)。
  4. 图、时序与 PG 基座:Neo4j/Cypher 关系遍历、时序建模、PostgreSQL 体系化能力边界(C14.07/09/10)。
  5. CDC 与值班:端到端变更数据捕获管道;慢 SQL 标准动作与证据链(C14.12/13)。
工程化要点
  • 选型材料必须写负载形状(点查/检索/HTAP/OLAP/图遍历/时序写入)与一致性/延迟目标,禁止「默认用熟悉的库」。
  • 向量索引参数、过滤字段与重建窗口写入变更单;Embedding 维度变更按双写/重算治理。
  • 分片键/分区键评审强制;错误分片导致的热点按架构缺陷管理,不靠无限扩容。
  • HTAP/OLAP 引擎的副本、压缩、物化与查询队列有容量看板;大查询可隔离可杀。
  • CDC 管道要有位点、重放、幂等下沉与延迟 SLO;断流告警可行动。
  • 跨引擎事务默认最终一致+对账;强一致路径单独论证成本。
  • 慢 SQL 值班固定:指纹 → 计划 → 锁/IO → 限流或索引变更;变更可回滚。
  • 多模并存时明确「系统 of record」;缓存与派生库不得冒充源。
本篇思维模式
  • 负载形状选引擎:先画像再选型,引擎能力边界大于品牌偏好。
  • 向量不是万能索引:过滤、召回率、重建成本与 generational Embedding 一起设计。
  • 分片键是架构决策:选错分片比选错实例规格更伤。
  • OLTP/OLAP 心智分离:分析型大扫描不得压垮在线事务尾延迟。
  • CDC 是契约:位点与幂等比「能连通」更重要。
  • 证据驱动慢查询:无执行计划与等待事件对照,禁止盲目加索引。
C14.01
MongoDB 文档模型、分片与事务如何协同?BSON、分片键、副本集与多文档事务的底层机制?高级P1
本质问题

这道题考察对 "MongoDB 如何用文档模型解决 schema 灵活性、用分片解决水平扩展、用副本集解决高可用、用多文档事务解决一致性" 的体系化认知。核心要回答:BSON 文档模型相比关系表的优势与代价?分片键如何决定数据分布与查询路由?副本集 Primary/Secondary/Arbiter 如何选举(Raft 变体)?4.0+ 多文档事务如何跨分片保证(2PC)?WiredTiger 存储引擎为何同时用 B+ 树与 MVCC?这是 NoSQL 方向最核心的"模型+扩展+一致性"三连问。

演进脉络

MongoDB 能力演进:

flowchart LR A["文档型 DB
MongoDB 1.x
(2009)
单机 BSON"] --> B["分片集群
1.6引入/2.2成熟
(2010/2012)
水平扩展"] B --> C["副本集+WiredTiger
3.0(2015)
高可用+MVCC"] C --> D["多文档事务
4.0(2018)
跨文档 ACID"] D --> E["时间窗事务
5.0(2021)
长事务支持"] E --> F["Queryable Encryption
7.0(2023)
密文查询"] style A fill:#dbeafe,stroke:#2563eb style C fill:#dcfce7,stroke:#059669 style D fill:#fef3c7,stroke:#d97706 style F fill:#f3e8ff,stroke:#7c3aed
图 14.1-1 MongoDB 能力演进:文档模型 → 分片 → 副本集 → 多文档事务 → 时间窗事务 → 可查询加密

MongoDB 1.x(2009):单机文档数据库,BSON 文档模型解决关系库 schema 僵化问题,但无水平扩展、无事务、无高可用,定位"高性能读写的灵活存储"。

分片集群(1.6 引入于 2010,2.2-3.x 生产成熟 2012-2014):引入 Sharded Cluster,通过 mongos 路由 + Config Server 元数据 + Shard 分片实现水平扩展。数据按分片键分到不同 Shard,Chunk 迁移自动均衡。从此 MongoDB 可承载 TB 级数据。

副本集 + WiredTiger(3.0 引入 2015,3.2 成为默认):存储引擎从 MMAPv1 切换到 WiredTiger(B+ 树 + MVCC + 文档级锁),并发与压缩大幅提升;副本集(Replica Set)提供自动故障转移,弥补早期单点问题。

多文档事务(4.0,2018):副本集内支持多文档 ACID 事务;4.2 扩展到分片集群跨分片事务(两阶段提交 2PC)。这是 MongoDB 从" NoSQL"向"NewSQL 兼容"的关键转身,但仍不鼓励大事务。

时间窗事务(5.0,2021):放宽事务执行时间限制,支持更长窗口的事务(如对账、ETL),同时引入在线分片集合重新分片(resharding)。

Queryable Encryption(7.0,2023):支持对加密字段做等值、范围查询而无需解密,密钥仅在客户端持有,满足金融/医疗合规场景。

演进驱动力:① 灵活建模——业务 schema 频繁变化,文档模型免去 DDL;② 水平扩展——单机容量见顶,分片线性扩容;③ 一致性补课——纯最终一致性无法满足金融场景,补齐 ACID;④ 安全合规——数据加密从静态加密演进到可查询加密。

第一性原理

BSON 文档模型:BSON(Binary JSON)是二进制编码的 JSON 扩展,在 JSON 类型基础上增加日期、ObjectId、BinData、Decimal128 等类型。一个文档即一条记录,支持嵌套对象与数组,天然表达"聚合根"(如订单含多个商品行)。单个文档最大 16MB(防止文档过大拖垮内存与网络)。文档模型的核心优势是"单文档读写原子性"——一个文档的更新天然原子,无需事务;代价是 join 能力弱,复杂关联需靠聚合管道 $lookup 或冗余反范式。

分片集群架构:

flowchart TD C["客户端 Client"] --> M["mongos 路由
(无状态,可水平扩展)"] M --> CS["Config Server
元数据: 分片键范围→Shard"] M --> S1["Shard 1
(副本集)
chunk A-B"] M --> S2["Shard 2
(副本集)
chunk B-C"] M --> S3["Shard 3
(副本集)
chunk C-D"] CS -.路由表.-> M style M fill:#dbeafe,stroke:#2563eb style CS fill:#fef3c7,stroke:#d97706 style S1 fill:#dcfce7,stroke:#059669 style S2 fill:#dcfce7,stroke:#059669 style S3 fill:#dcfce7,stroke:#059669
图 14.1-2 MongoDB 分片集群:mongos 路由 + Config Server 元数据 + 多个 Shard 副本集

分片键(Shard Key):决定数据分布的字段,分两种策略:

  • 范围分片(Range):按分片键值范围划分 Chunk,利于范围查询,但易热点(自增 ID/时间戳集中写入同一 Chunk)。
  • 哈希分片(Hash):对分片键取哈希均匀分布,写入分散抗热点,但范围查询需广播到所有 Shard(scatter-gather)。

Chunk 迁移:Balancer 后台检测各 Shard Chunk 数量不均,按分片键范围迁移 Chunk 到目标 Shard。迁移期间源 Shard 仍服务读,写双重写入,迁移完成更新 Config Server 路由表。历史集合的分片键是强约束:上线前必须审慎设计;MongoDB 5.0+ 支持 resharding 在线重分片,但代价高(全量数据搬迁),不宜当作随意变更手段。

副本集与选举:每个 Shard 是一个副本集,含 1 个 Primary(读写)+ N 个 Secondary(同步复制,可读)+ 可选 Arbiter(仅投票不存数据)。Primary 故障时,具备投票权的节点发起选举(Raft 变体),多数派同意后新 Primary 上位。关键参数:writeConcern(写多少节点才确认,如 w:majority 防回滚)、readPreference(读主还是读从)。脑裂通过"多数派投票"避免——少数派分区无法选出 Primary,自动降级为只读。

WiredTiger 存储引擎:默认引擎,核心三点:① B+ 树索引——文档按 _id 与二级索引组织成 B+ 树,查询走索引定位;② MVCC 多版本并发控制——每个事务看到一致性快照,读写不阻塞,结合 snapshot 隔离级别;③ 文档级锁——锁粒度到单文档(早期 MMAPv1 是集合级甚至全局锁),并发大幅提升;④ 压缩——Snappy/Zstd 压缩减少磁盘占用,检查点(checkpoint,默认 60s)刷盘。

多文档事务(4.0+):副本集事务由 Primary 协调,写操作记录到 oplog 同步到 Secondary;跨分片事务(4.2+)采用两阶段提交(2PC)——协调者(Primary 所在 Shard)先 prepare 各参与 Shard,全部 prepare 成功后 commit,任一失败则 abort。事务隔离级别为 snapshot(快照隔离)。事务约束:① 事务超时默认 60s(transactionLifetimeLimitSeconds 可调),超时自动 abort;② 单条 oplog 条目上限 16MB(BSON 文档限制),大事务产生多条 oplog 条目导致复制延迟增大;③ 跨分片事务 2PC 协调阶段持锁时间长,易引发锁竞争与协调者单点压力。因此 MongoDB 事务适合短事务 OLTP 场景,不适合大批量 ETL 或长分析查询。

维度 MongoDB(文档型) MySQL(关系型)
数据模型 BSON 文档,嵌套/数组,schema 灵活 行列二维表,强 schema,需 DDL
扩展方式 原生分片(分片键 + Chunk 迁移)水平扩展 单机为主,分库分表需中间件(ShardingSphere)
高可用 副本集 + Raft 选举自动故障转移 主从复制 + MHA/Orchestrator 切换
事务 4.0+ 多文档 ACID(snapshot),跨分片 2PC 原生强 ACID,多种隔离级别
存储引擎 WiredTiger(B+树 + MVCC + 文档级锁) InnoDB(B+树 + MVCC + 行级锁)
JOIN 弱($lookup 聚合,性能一般) 强(Nested Loop/Hash Join 优化成熟)
适用场景 schema 多变、读写均衡、文档聚合根、海量水平扩展 强一致、复杂关联、事务密集型业务
工程实践

Spring Data MongoDB 示例:用 @Document 映射集合,MongoRepository 提供 CRUD,复杂查询用 MongoTemplate。

// 1. 文档实体:订单聚合根(嵌套商品行)
@Document(collection = "orders")
@CompoundIndex(name = "idx_user_status", def = "userId:1,status:1")
public class Order {
    @Id
    private String id;
    private Long userId;          // 分片键(哈希分片抗热点)
    private String status;
    private List<OrderItem> items;  // 嵌套数组,单文档原子更新
    private LocalDateTime createdAt;
}

public interface OrderRepository extends MongoRepository<Order, String> {
    List<Order> findByUserIdAndStatus(Long userId, String status);
}

// 2. 聚合管道:统计每个用户订单数与总金额
Aggregation agg = Aggregation.newAggregation(
    Aggregation.match(Criteria.where("status").is("PAID")),
    Aggregation.group("userId")
        .count().as("orderCount")
        .sum(Operators.size("$items")).as("itemTotal"),
    Aggregation.sort(Sort.by(Sort.Direction.DESC, "orderCount"))
);
List<UserOrderStat> stats = mongoTemplate.aggregate(agg, "orders", UserOrderStat.class).getMappedResults();

// 3. 多文档事务:扣库存 + 写订单(需配置 MongoTransactionManager)
@Transactional
public void placeOrder(Long userId, List<OrderItem> items) {
    stockRepository.decrement(items);        // 扣减库存
    orderRepository.save(new Order(userId, "PAID", items));  // 写订单
}

分片键设计要点:① 高基数——值域要广,避免少量 Chunk 装满无法分裂;② 低频率——单个值不应集中大量文档(如 status 只有几个值,做分片键必热点);③ 非单调递增——避免 ObjectId/时间戳单独做范围分片键导致写热点,可用"哈希分片"或"前缀+哈希"组合;④ 查询覆盖——大部分查询应带分片键,否则 mongos 需广播到所有 Shard(scatter-gather)。

最佳实践

MongoDB 工程四要点:① 文档建模优先反范式——把频繁一起读的子对象嵌套进父文档,单文档原子更新免去事务;② 分片键选"高基数+查询热点字段"哈希分片——如 userId 哈希,兼顾分布均匀与查询定位;③ 事务只用于跨文档强一致——单文档更新天然原子无需事务,大事务会撑爆 oplog;④ 写关注用 w:majority——防 Primary 故障导致已确认写丢失回滚。

深度追问

追问 1:分片键选错了如何迁移?

5.0 之前需停服重建集合:导出数据 → 按新分片键重新分片导入 → 切换应用。5.0+ 支持在线 resharding——后台按新分片键克隆数据到新分片集合,期间双写保证一致,完成后原子切换。但 resharding 仍是重操作(消耗额外存储与带宽、拖慢写入),应尽量上线前定好分片键。选键原则:组合键(如 region + userId)兼顾分布与查询,避免纯单调字段。

追问 2:MongoDB 事务与 MySQL 事务的差异?

① 隔离级别——MongoDB 固定 snapshot 快照隔离,MySQL 默认 RR(可配 RC/Serializable);② 跨分片机制——MongoDB 跨分片用 2PC,性能损耗大,MySQL 单机事务无此开销(分布式靠 XA/Seata);③ 事务限制——MongoDB 事务有 60s 时间与 oplog 大小上限,不适合大事务,MySQL 大事务能力更强但仍需控制;④ 锁粒度——MongoDB 文档级锁,MySQL InnoDB 行级锁;⑤ 使用哲学——MongoDB 鼓励"文档内聚"减少跨文档事务,MySQL 鼓励规范化与事务保障。

常见陷阱
易错点
  • 分片键基数过低:用 status(仅几个值)做分片键,Chunk 无法分裂,所有数据堆在一个 Shard,水平扩展失效。
  • 无索引查询全表扫描:以为 NoSQL 不用建索引——MongoDB 同样依赖索引,无索引查询会 COLLSCAN 全集合扫描,大数据量下直接拖垮。
  • 大文档突破 16MB 限制:把日志/附件塞进文档,超过 16MB 报错,大对象应改用 GridFS 分块存储。
  • 副本集选举脑裂:网络分区时两个分区都以为对方挂了,但 MongoDB 多数派投票机制会让少数派分区无法选出 Primary,理论上防脑裂;隐患是 Arbiter 配置不当或奇偶节点数错误导致无法形成多数派。
  • 把 MongoDB 当关系库做复杂 JOIN:$lookup 性能远不如 MySQL JOIN,频繁关联应反范式冗余或换关系库。
  • writeConcern 用默认 w:1:Primary 写入即返回,Primary 故障时该写未同步到 Secondary 会丢失,关键写应用 w:majority。
C14.02
为何用 MongoDB 做向量检索?Atlas Vector Search 的 ANN 原理与混合查询如何优化?架构P0
本质问题

这道题考察对 "为什么在文档库上叠加向量检索,以及近似最近邻(ANN)与混合查询的工程取舍" 的架构判断。核心要回答:既然有专用向量库(Milvus/Qdrant/Pinecone),为何还要用 MongoDB 做向量检索?Atlas Vector Search 的 HNSW 与 $vectorSearch 如何工作?向量与结构化过滤(pre/post-filter)的执行顺序如何影响召回与性能?什么场景下 MongoDB 向量检索是更优解?这是 AI 应用存储选型的关键决策题。

演进脉络

向量检索技术演进:

flowchart LR A["精确 KNN
暴力计算
O(N)"] --> B["LSH
局部敏感哈希
(2000s)"] B --> C["IVF
倒排聚类
FAISS(2017)"] C --> D["HNSW
分层小世界图
(2016-2018)"] D --> E["专用向量库
Milvus/Qdrant/Pinecone"] E --> F["MongoDB Atlas
Vector Search
(2023)"] style A fill:#fee2e2,stroke:#dc2626 style C fill:#fef3c7,stroke:#d97706 style D fill:#dcfce7,stroke:#059669 style F fill:#dbeafe,stroke:#2563eb
图 14.2-1 向量检索演进:精确 KNN → LSH → IVF → HNSW → 专用向量库 → 文档库内嵌向量检索

精确 KNN(暴力):查询向量与库中所有 N 个向量算距离再排序,召回 100% 但复杂度 O(N×d),百万级以上不可行。

LSH(局部敏感哈希,2000s):用哈希函数把"相近向量"哈希到同一桶,只搜同桶。速度快但召回率不稳定,高维下效果一般,已逐渐被图索引取代。

IVF(FAISS,2017):k-means 聚成 nlist 簇,查询只搜最近 nprobe 簇。可与 PQ 量化组合省内存,是亿级检索的主力。

HNSW(2016-2018):分层可导航小世界图,查询 O(log N) 且召回率高,成为现代向量库的默认索引。

专用向量库(Milvus 2.x / Qdrant / Pinecone):为向量检索从头设计,存算分离、分布式分片、丰富的量化与过滤能力,支撑十亿级规模。

MongoDB Atlas Vector Search(2023):在文档库上叠加 HNSW 向量索引与 $vectorSearch 聚合阶段,让"业务文档+向量"共存于一个库,免去多库数据同步。代表"把向量检索能力内嵌进已有数据库"的趋势(类似 PgVector、Redis Vector)。

演进驱动力:① 运维收敛——少维护一个专用向量库组件;② 数据一致性——文档与其向量同库同事务,免去双写不一致;③ 混合查询——向量检索+结构化过滤一站式完成,AI 应用(RAG、推荐)天然需要这种组合。

第一性原理

为何用 MongoDB 做向量检索:核心价值是"数据同库,免去同步"。RAG/推荐场景中,文档正文、元数据(作者/时间/权限)、向量 embedding 是强相关的——分库存需双写保持一致(文档更新要同步重建向量),增加系统复杂度与一致性风险。Atlas Vector Search 让三者同存于一个 MongoDB 集合,$vectorSearch 直接在文档上做向量检索并用元数据过滤,省去同步管道。代价是规模与极致性能不及专用向量库。

HNSW 图结构(多层跳表):Atlas Vector Search 底层用 HNSW(Hierarchical Navigable Small World)。构建多层图,顶层稀疏(入口少、长程跳转)、底层稠密(精细近邻)。查询从顶层入口点贪心向更近节点跳,逐层下降到底层,在底层邻域返回 top-k。复杂度 O(log N),在合理参数与数据分布下召回率通常可达 95%+。关键参数 efConstruction(建图质量)、numCandidates(查询候选队列大小,越大越准越慢)。

$vectorSearch 聚合阶段:Atlas 把向量检索封装为聚合管道的首个阶段,与后续 $match/$project 等阶段组合:

// $vectorSearch: 在向量字段上做 ANN 检索
{
  "$vectorSearch": {
    "index": "vector_index",        // 向量索引名
    "path": "embedding",          // 文档中向量字段
    "queryVector": queryVec,       // 查询向量
    "numCandidates": 200,       // 候选数(越大召回越高)
    "limit": 10,                // 返回 top-k
    "filter": { "status": "published" }  // 预过滤
  }
}

ANN vs KNN:KNN(K-Nearest Neighbors)是精确检索——遍历全部向量算距离,召回 100% 但 O(N) 不可扩展。ANN(Approximate Nearest Neighbor)用索引结构(HNSW/IVF)近似搜索,牺牲少量召回(典型区间约 95%-99%,取决于索引与数据分布)换取 O(log N) 速度,是大规模向量检索的工程必然。AI 检索场景 ANN 召回损失可被后续 LLM 容忍(top-k 多取几条再重排即可)。

混合查询:向量+结构化过滤:实际 RAG 常需"语义相似且满足权限/时间过滤"。两种执行策略:

  • Pre-filter(预过滤):先用结构化条件筛出候选集,再在候选集上做向量检索。优点是召回精准(不漏掉被过滤项),缺点是候选集过小时 HNSW 图被过度剪枝,性能下降甚至退化。
  • Post-filter(后过滤):先做向量检索取较大候选(如 top-100),再用条件过滤取 top-k。优点是向量检索效率高,缺点是过滤后可能不足 k 条(召回不足)。

Atlas Vector Search 默认用 pre-filter(在 $vectorSearch 的 filter 字段),其内部对过滤做优化(索引下推),但极度选择性的过滤仍可能影响性能。

维度 MongoDB Atlas Milvus Pinecone
定位 文档库内嵌向量检索 专用向量库(存算分离) 托管 Serverless 向量库
索引 HNSW IVF/HNSW/DiskANN HNSW(封装,不可调)
规模上限 百万~千万级(受文档库约束) 十亿级 十亿级(按 Pod/Serverless 扩展)
混合查询 强(filter 与文档同库,免同步) 中(标量字段过滤) 中(元数据过滤)
运维 复用 MongoDB 运维栈 自托管,组件多 全托管,零运维
事务一致性 文档与向量同库 ACID 向量库无强事务 无强事务
成本 中等(Atlas 按量) 低(自建) 高(托管溢价)
适用 已有 MongoDB、文档+向量强一致、中小规模 RAG 超大规模、性能极致、可自运维 快速上线、免运维、弹性
工程实践

Spring AI + MongoDB VectorStore 集成 RAG:Spring AI 提供 MongoDBAtlasVectorStore,自动建索引、存文档与向量、支持相似度检索与过滤。

// 1. 依赖: org.springframework.ai:spring-ai-mongodb-atlas-store (Spring AI 1.0)
// 2. 配置 VectorStore(向 embedding 字段写入向量)
@Configuration
public class VectorStoreConfig {
    @Bean
    public VectorStore vectorStore(MongoTemplate mongo, EmbeddingModel model) {
        return MongoDBAtlasVectorStore.builder(mongo, model)
            .collectionName("kb_docs")
            .pathName("embedding")        // 向量字段
            .vectorIndexName("vector_index")        // 注意 builder 方法名为 vectorIndexName(非 indexName)
            .metadataFieldsToFilter(List.of("source", "status"))
            .build();
    }
}

// 3. 写入知识库(自动调用 EmbeddingModel 生成向量)
Document doc = new Document(text, Map.of("source", "policy", "status", "published"));
vectorStore.add(List.of(doc));

// 4. RAG 检索:相似度 + 元数据过滤
List<Document> hits = vectorStore.similaritySearch(
    SearchRequest.builder()
        .query(question)
        .topK(5)
        .filterExpression("status == 'published' && source == 'policy'")
        .build()
);

// 5. 拼接上下文交给 LLM
String context = hits.stream().map(Document::getText).collect(Collectors.joining("\n"));
String answer = chatClient.prompt().user(question + "\n资料:\n" + context).call().content();
最佳实践

MongoDB 向量检索四要点:① 向量字段与文档同集合——文档更新时同步重建向量,保证一致;② 建向量索引前先定好维度——索引维度必须与 embedding 模型输出一致,改模型需重建索引;③ 过滤字段建索引——$vectorSearch 的 filter 字段应建常规索引,否则 pre-filter 全表扫;④ numCandidates 调召回——先设为 limit 的 10-20 倍作为基线,结合具体数据集评测召回率,再按 QPS 权衡下调。

深度追问

追问 1:MongoDB 向量检索的性能边界在哪?

Atlas Vector Search 用 HNSW,图结构需常驻内存,单节点向量数受内存约束(768 维向量千万条量级约几十 GB 内存)。规模上限与集群分片数、节点内存正相关,在合理内存配置下千万级向量量级内表现良好;上亿级则专用向量库(Milvus 存算分离/DiskANN 落盘)更合适。延迟上,毫秒级 top-10 检索可达,但高 QPS 下需关注 HNSW 的 CPU 占用与 numCandidates 调参。若数据量超 MongoDB 单分片内存承载或 QPS 极高,应迁移专用向量库。

追问 2:向量与结构化过滤的执行顺序如何优化?

关键看过滤的选择性:① 低选择性过滤(命中率高)——用 pre-filter,候选集大,HNSW 仍高效;② 高选择性过滤(命中率低)——pre-filter 会让候选集过小、HNSW 图剪枝严重退化,此时应 post-filter 取更大候选(如 top-100)再过滤,或对过滤字段建复合索引加速;③ 极端选择性——先结构化查询拿候选 ID 集合,再在 ID 集合上做向量检索。Atlas 的 filter 内部已做一定优化,但架构师应评测不同选择性下的召回与延迟,必要时调 numCandidates 或改查询结构。

常见陷阱
易错点
  • 向量维度过高导致内存爆炸:3072 维向量千万条占用巨大内存,应优先选满足召回的最低维度(如 768/1024),必要时用量化压缩。
  • 未建向量索引:直接 $vectorSearch 不建 vector index 会报错或退化为暴力扫描,必须先创建 HNSW 索引并指定正确维度。
  • 混合查询未用复合索引:filter 字段不建索引,pre-filter 退化为全表扫描,延迟飙升。
  • 把 MongoDB 当十亿级向量库:超规模使用导致内存撑爆、HNSW 性能下降,应迁移 Milvus/Qdrant。
  • 向量与文档双写不一致:虽同库但若用外部 embedding 异步写入,文档更新与向量更新可能错位,应同步生成或用变更流补偿。
  • numCandidates 设过小:为追求 QPS 把候选设成 limit 的 1-2 倍,召回率暴跌,RAG 漏掉关键文档。
生产实践

上线门禁:备份恢复 RTO/RPO 演练、连接池与慢查询。表达提纲:存储引擎特征→容量证据→切换预案。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C14.03
Milvus 如何支撑十亿级向量?存算分离架构与 IVF/HNSW/DiskANN 索引如何权衡?专家P0
本质问题

本题从数据库视角聚焦 Milvus 内部架构;十亿级 RAG 的向量索引调参与召回率治理详见 C12.21。这道题考察对 "专用向量库如何从架构层面解决超大规模向量检索" 的深度理解。核心要回答:Milvus 凭什么支撑十亿级向量而 MongoDB/PgVector 做不到?存算分离(QueryNode/DataNode/Coordinator)的职责划分与代价?IVF、HNSW、DiskANN 三种索引在召回率、内存、延迟上的权衡?Segment 分段如何支撑水平扩展?这是向量库架构的"天花板"题。

演进脉络

Milvus 架构演进:

flowchart LR A["FAISS
(2017)
单机库
C++算法集"] --> B["Milvus 1.x
(2019)
分布式封装
FAISS+MQ"] B --> C["Milvus 2.x
(2021)
云原生
存算分离"] C --> D["Milvus 2.4+
(2024)
DiskANN
GPU加速"] style A fill:#fee2e2,stroke:#dc2626 style B fill:#fef3c7,stroke:#d97706 style C fill:#dcfce7,stroke:#059669 style D fill:#dbeafe,stroke:#2563eb
图 14.3-1 Milvus 演进:FAISS 单机库 → 1.x 分布式封装 → 2.x 云原生存算分离 → 2.4+ DiskANN/GPU

FAISS(2017,Facebook):纯算法库(C++),提供 IVF/HNSW/PQ 等索引实现,单机高性能,但无分布式、无高可用、无数据管理。适合嵌入应用内做单机检索。

Milvus 1.x(2019):在 FAISS 之上封装分布式服务,用消息队列(如 Pulsar)+ 对象存储做数据流转,实现读写分离与水平扩展。但存储与计算耦合——QueryNode 既存数据又算查询,扩缩容需迁移数据,运维复杂。

Milvus 2.x(2021,云原生重构):彻底存算分离——数据全部存对象存储(S3/MinIO),计算节点(QueryNode)无状态,按需加载索引到内存。引入 Coordinator 协调、Segment 分段、etcd 元数据。这是支撑十亿级的关键——存储无限扩、计算弹性扩、故障恢复快。

Milvus 2.4+(2024):引入 DiskANN(磁盘索引,超大规模省内存)、GPU 加速(CUDA 索引构建与检索)、Scalar 量化、多向量/混合检索。向"超大规模+低成本+高性能"演进。

演进驱动力:① 规模——AI 应用向量数从百万涨到十亿,单机内存装不下;② 弹性——流量峰谷要求计算节点可弹性扩缩,存算分离是前提;③ 成本——内存贵,DiskANN 把索引落 SSD 降成本;④ 云原生——K8s 部署、对象存储解耦,符合云架构范式。

第一性原理

存算分离架构:

flowchart TD CL["客户端 SDK"] --> PROXY["Proxy
接入/鉴权/路由"] PROXY --> QC["Query Coordinator
查询协调"] PROXY --> DC["Data Coordinator
数据协调"] PROXY --> RC["Root Coordinator
元数据(DDL/DML)"] QC --> QN["QueryNode
(无状态)
加载Segment索引
执行检索"] DC --> DN["DataNode
消费日志
刷盘Segment"] DN --> OS["对象存储
S3/MinIO
Segment数据+索引"] QN -.加载索引.-> OS RC --> ET["etcd
元数据/拓扑"] style PROXY fill:#dbeafe,stroke:#2563eb style QN fill:#dcfce7,stroke:#059669 style OS fill:#fef3c7,stroke:#d97706
图 14.3-2 Milvus 2.x 存算分离:Coordinator 协调 + QueryNode/DataNode 无状态计算 + 对象存储持久化

核心组件职责:① Root Coordinator——元数据与 DDL/DML 协调,存于 etcd;② Query Coordinator + QueryNode——QueryNode 无状态,按需从对象存储加载 Segment 索引到内存执行检索,可弹性扩缩;③ Data Coordinator + DataNode——DataNode 消费写入日志,合并刷盘成不可变 Segment 存对象存储;④ 对象存储——所有数据与索引的持久层,容量无限、成本低。计算与存储解耦,QueryNode 挂掉不丢数据,重启重新加载索引即可。

Segment 分段:Collection 数据被切成多个 Segment(类似分片/分区),每个 Segment 是一段向量+索引的不可变单元。Growing Segment(写入中,内存)→ Sealed Segment(刷盘,可加载检索)。查询并行扇出到所有含数据的 QueryNode,各算各的 Segment 再合并 top-k。水平扩展靠增加 QueryNode 并分配 Segment。

三大索引权衡:

  • IVF(倒排聚类 + PQ 量化):k-means 聚成 nlist 簇,查询搜最近 nprobe 簇;PQ 把向量分段量化压缩省内存。内存省、构建快、易增量,但召回略低于 HNSW。适合亿级、成本敏感。
  • HNSW(层级图):多层小世界图,查询 O(log N)、召回高(95%+),但内存占用大(图结构常驻)、构建慢、难删除。适合千万级、内存充足、精度优先。
  • DiskANN(磁盘索引):把图索引与向量存 SSD,内存仅缓存入口点与部分图,查询时按需读盘。内存占用极低,支撑十亿级低成本,但延迟比纯内存高(SSD 随机读)。

索引加载机制:Milvus 查询前必须把索引 load 到 QueryNode 内存(除 DiskANN 外)。未 load 的 Collection 查询会报错或失败。load 是异步的,大索引加载耗时,应预热。

维度 Milvus Qdrant Pinecone
语言 Go + C++(核心算子) Rust 闭源(托管)
架构 存算分离(对象存储) 分布式分片+副本(内存为主,对象存储可选快照持久化) 托管云原生
索引 IVF/HNSW/DiskANN/GPU HNSW + 量化 HNSW(封装)
规模 十亿级(存算分离+DiskANN) 亿级(分片可横向扩展,单分片内存为瓶颈,分片越多规模越大) 十亿级(弹性)
过滤 标量字段过滤 强(Payload pre/post-filter) 元数据过滤
运维 自托管,组件多(etcd/MinIO/Pulsar) 自托管,单二进制轻量 全托管,零运维
成本 低(对象存储+按需计算) 中(内存为主) 高(托管溢价)
适用 超大规模、可自运维、成本敏感 中等规模、过滤强、Rust 性能 快速上线、免运维
工程实践

Milvus Java SDK 示例:建 Collection → 建索引 → load → 插入 → 检索。

// Milvus Java SDK v2(io.milvus:v2-client)示例:建集合 → 建索引 → load → 检索
// 1. 建集合:主键 + 向量(1024维) + 标量字段(v2 用 AddFieldReq + .field())
CreateCollectionReq schema = CreateCollectionReq.builder()
    .collectionName("kb_vectors")
    .field(AddFieldReq.builder().fieldName("id").isPrimaryKey(true).dataType(DataType.Int64).autoID(true).build())
    .field(AddFieldReq.builder().fieldName("embedding").dataType(DataType.FloatVector).dimension(1024).build())
    .field(AddFieldReq.builder().fieldName("source").dataType(DataType.VarChar).maxLength(64).build())
    .build();
client.createCollection(schema);

// 2. 建 HNSW 索引(千万级、精度优先)
IndexParam index = IndexParam.builder()
    .fieldName("embedding")
    .indexName("vector_index")
    .indexType(IndexParam.IndexType.HNSW)
    .metricType(IndexParam.MetricType.COSINE)
    .extraParams(Map.of("M", 16, "efConstruction", 200))
    .build();
client.createIndex(CreateIndexReq.builder().collectionName("kb_vectors").indexParams(List.of(index)).build());

// 3. load 到内存(查询前必须)
client.loadCollection(LoadCollectionReq.builder().collectionName("kb_vectors").build());

// 4. 检索:向量 + 标量过滤,调 ef 调召回
SearchReq req = SearchReq.builder()
    .collectionName("kb_vectors")
    .data(List.of(new FloatVec(queryVec)))
    .topK(10)
    .filter("source == 'policy'")           // 标量过滤
    .searchParams(Map.of("ef", 128))      // HNSW 查询参数
    .build();
SearchResp resp = client.search(req);   // v2 返回 SearchResp

索引选型指南:索引参数调优实战经验与 RAG 场景治理详见 C12.21。无论哪种,上线前都需离线评测召回率(与暴力检索对比,目标 95%+)。

最佳实践

Milvus 工程四要点:① load 预热——查询前必须 load 索引到内存,大集合 load 耗时,应上线/重启后预热避免首查超时;② Collection 按业务隔离——不同业务分 Collection,避免大混合查询相互影响;③ 用 Partition 做时间/租户切分——按天/租户建 Partition,查询带分区键减少扫描范围;④ 存算分离下关注延迟——对象存储读取有延迟,热数据应常驻 QueryNode 内存,冷数据用 DiskANN。

深度追问

追问 1:IVF 与 HNSW 如何选?nlist/M 与召回率关系?

索引参数调优实战经验与 RAG 场景治理详见 C12.21。务必离线评测召回率定参。

追问 2:存算分离的延迟与成本如何权衡?

存算分离把数据放对象存储,QueryNode 按需加载,好处是存储便宜(S3 比 SSD 便宜一个数量级)、计算弹性(按 QPS 扩 QueryNode)、故障恢复快(无状态重加载)。代价是冷查询需从对象存储读索引,延迟比纯内存高(首查可能秒级)。优化:① 热数据常驻内存——load 常用 Segment 到 QueryNode 内存;② DiskANN 混合——热入口点放内存、向量落 SSD,兼顾成本与延迟;③ 缓存层——QueryNode 本地缓存热点 Segment。成本上存算分离总拥有成本低于纯内存耦合架构,尤其适合向量数大但 QPS 不极端的场景。

常见陷阱
易错点
  • 索引未加载到内存查询失败:HNSW/IVF 索引必须 load 到 QueryNode 内存才能查,忘记 load 或 load 未完成就查询会报错。
  • nlist/nprobe 参数不当:nlist 设太大簇内向量少、设太小扫描范围大;nprobe 设太小召回暴跌,需按数据量与召回率调参。
  • 无数据分片导致单节点瓶颈:Collection 不分 Partition/Segment,所有数据堆单 QueryNode,QPS 打不上去,应按业务/时间分片并行。
  • 把 DiskANN 当内存索引用:DiskANN 延迟比纯内存高,对延迟敏感场景应用 HNSW。
  • 改 embedding 模型不重建索引:维度或语义变化后旧索引失效,必须重建。
  • 忽视召回率评测:未与暴力检索对比,实际召回可能只有 70%,应离线建 ground truth 评测。
扩展补全

本题聚焦向量库内部架构,但真实 AI 应用中向量库几乎不会独立存在——业务数据在 MySQL(关系库),文本向量在 Milvus/Pinecone(向量库),两者如何保持一致是架构师必须答好的落地问题。

  • 双写场景:AI 应用中,业务数据写入 MySQL 后,需同步将文本向量写入 Milvus/Pinecone。商品标题、描述变更时,Embedding 结果也必须同步更新,否则 RAG 检索命中旧内容。
  • 同步双写:应用层先写 MySQL,成功后写向量库。实现最简单,但向量库写入失败时数据不一致,且 Embedding 耗时(100ms+)会拖长主链路事务,仅适合低并发、可容忍短时不一致的场景。
  • 事务 + 消息(最终一致):MySQL 事务提交后发 MQ,消费者异步执行 Embedding 并写向量库。延迟秒级,支持重试;配合向量库 upsert 幂等语义,消息重复消费也不会产生脏数据。
  • CDC 方案(推荐):Debezium 监听 MySQL Binlog,变更事件写入 Kafka,Embedding Worker 消费后更新向量库。对业务零侵入,不依赖应用侧发 MQ,改造存量系统最平滑。
  • 补偿机制:定时任务全量校验——扫描 MySQL 数据重新生成向量并与向量库比对,发现差异自动修复,兜底消息丢失、消费者宕机等异常场景。

向量库更新的特殊性:

  • 不支持事务:向量库写入是 upsert 语义(存在则更新、不存在则插入),天然幂等,但无法与 MySQL 事务原子提交,只能靠最终一致。
  • Embedding 耗时长:大文本重新向量化通常 100ms 以上,必须在异步管道中处理,不能同步阻塞主业务链路。
  • 删除也要同步:MySQL 删除记录时,Binlog 事件触发向量库按主键删除,否则成为检索死角。
深入解析

以商品系统为例:商品表(MySQL)→ Debezium 捕获 UPDATE → Kafka topic → Embedding Worker 批量向量化 → Milvus upsert。链路任一环节失败都能通过消息重投 + 补偿任务兜底,达到秒级最终一致。对比三种方案:同步双写一致性最差但最简单;事务 + 消息是通用折中;CDC 对业务无侵入但引入 Debezium/Kafka 运维成本——应给出"按一致性要求与改造成本取舍"的判断。

生产实践

上线门禁:备份恢复 RTO/RPO 演练、连接池与慢查询。表达提纲:存储引擎特征→容量证据→切换预案。

P0 表达提纲

先给可观测证据与门禁,再谈实现细节;避免只报组件名。

C14.04
Qdrant 为何用 Rust?过滤检索(filtering)与 Payload 索引如何高效?架构P1
本质问题

本题从数据库视角聚焦 Qdrant 架构与选型;十亿级 RAG 的向量库选型矩阵与调参实战详见 C12.21。这道题考察对 "向量库实现语言选型与结构化过滤检索的工程优化" 的理解。核心要回答:Qdrant 为什么选 Rust 而非 C++/Go?Rust 的零成本抽象与内存安全如何转化为向量检索的性能与稳定性优势?Payload(JSON 结构化字段)索引如何加速过滤?pre-filter 与 post-filter 的性能差异与适用场景?Qdrant 与 Milvus 的选型分界?这是向量库"过滤检索"维度的对比题。

演进脉络

向量库实现语言演进:

flowchart LR A["C++
FAISS(2017)
极致性能
内存不安全"] --> B["Go
Weaviate(2019)
并发友好
GC开销"] B --> C["Rust
Qdrant(2021)
零成本抽象
内存安全+无GC"] style A fill:#fee2e2,stroke:#dc2626 style B fill:#fef3c7,stroke:#d97706 style C fill:#dcfce7,stroke:#059669
图 14.4-1 向量库语言演进:C++(FAISS)→ Go(Weaviate)→ Rust(Qdrant),性能与安全兼顾

C++ 时代(FAISS,2017):用 C++ 实现底层算法(SIMD/BLAS),性能极致,但内存手动管理易出 bug(缓冲区溢出/野指针),且无内置并发抽象,分布式能力需上层封装。

Go 时代(Weaviate,2019):Go 的 goroutine 与 GC 适合写分布式服务,开发效率高,但 GC 停顿在高 QPS 向量检索下会成为延迟毛刺来源,且 Go 性能不及 C++/Rust。

Rust 时代(Qdrant,2021):Rust 提供零成本抽象(性能媲美 C++)+ 所有权与借用检查(编译期内存安全,无 GC)+ 无数据竞争并发(Send/Sync)。Qdrant 用 Rust 实现整个向量库(含 HNSW、Payload 索引、过滤),既保性能又保稳定性,单二进制部署轻量。

演进驱动力:① 性能与安全兼得——向量检索是 CPU/内存密集型,C++ 快但易崩,Rust 二者兼顾;② 无 GC 低延迟——高 QPS 下 GC 停顿影响 P99,Rust 无运行时 GC;③ 工程可维护——Rust 强类型与编译期检查降低运维故障率。

第一性原理

Qdrant 核心模型:Collection(集合)下存 Points(点),每个 Point 含 id + vector + Payload(JSON 结构化字段,类似 MongoDB 文档)。Payload 用于过滤检索,如 {"color":"red","price":100}。这种"向量+结构化 Payload"模型让 Qdrant 天生擅长带元数据过滤的向量检索。

HNSW 索引:Qdrant 默认用 HNSW 做向量索引(参数 m/ef_construct/ef_search),与 Milvus/MongoDB 的 HNSW 原理相同——多层图、O(log N) 查询。Qdrant 的优势在于 Rust 实现的内存布局与并发控制,使单机 HNSW 性能很高。

Payload 索引:对 Payload 字段建索引(类似数据库二级索引),加速过滤。建索引后过滤走索引快速定位候选点,而非全量扫描。Qdrant 支持对 Payload 的关键字段(数值/关键字/bool/geo)建索引,查询时下推过滤条件。

过滤检索:pre-filter vs post-filter——Qdrant 的核心差异化能力:

  • pre-filter(默认推荐):先用 Payload 过滤出候选点集合,再在候选集上做 HNSW 检索。Qdrant 对此优化——若过滤候选数较少(< 全集一定比例),直接在候选集上做精确 KNN(跳过 HNSW 图遍历),避免 HNSW 在小候选集上退化。适合高选择性过滤。
  • post-filter:先 HNSW 检索取 top-k 候选,再用 Payload 过滤。适合低选择性过滤(过滤掉的比例小),但可能过滤后不足 k 条。

Qdrant 根据过滤选择性自动选择 pre/post 策略(也可强制指定),这是它过滤性能优于许多向量库的关键。

量化压缩:Qdrant 支持两种量化降内存:

  • Scalar 量化:把 float32 向量量化到 int8(每维 4 字节→1 字节),内存省 75%,精度损失极小(召回影响 <1%)。适合大规模省内存。
  • Binary 量化:量化到 1 bit/维,内存省 32 倍,用汉明距离检索,速度快但精度损失大。适合初筛召回+精排两阶段。

向量库选型矩阵(Qdrant/Milvus/Pinecone 多维度对比)详见 C12.21。

工程实践

Qdrant Java SDK 示例:建 Collection + Payload 索引 → 插入带 Payload 的点 → 过滤检索。

// 1. 建集合:向量(1024维) + 距离余弦
QdrantClient client = new QdrantClient(QdrantGrpcClient.newBuilder(host, port, useTls).build());
client.createCollectionAsync("kb_points",
    VectorParams.newBuilder().setSize(1024).setDistance(Distance.Cosine).build()).get();

// 2. 对 Payload 字段建索引(加速过滤)
client.createPayloadIndexAsync("kb_points", "source",
    PayloadSchemaType.Keyword, null).get();
client.createPayloadIndexAsync("kb_points", "ts",
    PayloadSchemaType.Integer, null).get();

// 3. 插入点:向量 + Payload(结构化元数据)
PointStruct point = PointStruct.newBuilder()
    .setId(PointId.newBuilder().setNum(1L).build())
    .setVectors(Vectors.newBuilder().setVector(
        FloatVectors.newBuilder().addAllData(vecList).build()).build())
    .putPayload("source", value("policy"))
    .putPayload("ts", value(1700000000))
    .build();
client.upsertAsync("kb_points", List.of(point)).get();

// 4. 过滤检索:source=policy 且 ts>某值,自动 pre/post-filter
Filter filter = Filter.newBuilder()
    .addMust(FieldCondition.newBuilder().setKey("source")
        .setMatch(Match.newBuilder().setKeyword("policy").build()).build())
    .addMust(FieldCondition.newBuilder().setKey("ts")
        .setRange(Range.newBuilder().setGt(1699000000).build()).build())
    .build();
List<ScoredPoint> hits = client.searchAsync(
    SearchPoints.newBuilder()
        .setCollectionName("kb_points")
        .addAllVector(vecList)
        .setFilter(filter)
        .setLimit(10)
        .build()).get();

Collection 与分区设计:① 按租户/业务分 Collection——隔离资源与权限;② 用 Payload 分区而非物理分片——Qdrant 通过 Payload 过滤实现逻辑分区,配合索引高效裁剪;③ 向量+Payload 同点写入——保证一致,避免双写。

最佳实践

Qdrant 工程四要点:① 过滤字段必建 Payload 索引——否则过滤全量扫描,延迟飙升;② 用 Scalar 量化省内存——大规模时把 float32 量化 int8,召回损失极小但内存省 75%;③ 高选择性过滤让 Qdrant 自动 pre-filter——它会按候选数自适应切精确 KNN/HNSW;④ 二阶段检索用 Binary 量化初筛——先 Binary 快速取大候选,再 float 精排,兼顾速度与精度。

深度追问

追问 1:pre-filter 与 post-filter 的性能差异?

差异源于过滤选择性。低选择性(过滤命中率高,如 90% 数据满足):post-filter 更优——HNSW 在全集上检索高效,过滤掉少量即可,召回足。高选择性(命中率低,如 1%):pre-filter 更优——先用 Payload 索引快速拿到 1% 候选(几千个),在候选上直接做精确 KNN,比让 HNSW 在被剪枝的稀疏图上遍历快得多;若此时用 post-filter,HNSW 取 top-k 大概率多数被过滤掉,需放大候选数(如取 top-1000)才够 k 条,性能反而差。Qdrant 的杀手锏是自动按选择性切换,开发者无需手调。

追问 2:Qdrant 与 Milvus 如何选型?

从规模与运维分界。① 规模——亿级以内、过滤检索为主、要求低延迟 → Qdrant(Rust 无 GC、过滤强、单二进制轻量);十亿级、成本敏感、需落盘 → Milvus(存算分离 + DiskANN,对象存储便宜)。② 运维——团队小、怕多组件 → Qdrant(单二进制,Docker 一行起);团队强、要深度定制 → Milvus(组件多但灵活)。③ 过滤场景——业务大量依赖元数据过滤(如权限/租户/时间) → Qdrant 的 Payload pre-filter 优势明显;纯相似度检索为主 → 二者均可。结论:中小规模+强过滤+轻运维选 Qdrant,超大规模+低成本选 Milvus。

常见陷阱
易错点
  • Payload 过大:把大段正文塞进 Payload,单点体积膨胀,索引与检索内存暴涨,正文应单独存对象存储只留必要元数据。
  • 过滤条件未建索引:Payload 字段不建索引,过滤退化为全量扫描,高选择性场景延迟飙升。
  • 量化精度损失未评估:Binary 量化召回损失大,直接用于最终检索会漏结果,应配合精排或仅用 Scalar 量化。
  • 把 Qdrant 当十亿级库:存算耦合+单机内存,十亿级内存装不下,应选 Milvus 存算分离。
  • 忽视 HNSW 参数:ef_search 设太小召回暴跌,应按召回率评测调参。
  • Payload 与向量双写不一致:虽同点写入但若异步生成向量,Payload 更新与向量更新可能错位,应同点原子写入。
C14.05
TiDB 如何实现 HTAP 同时支持 OLTP 与 OLAP?Raft 共识与 Percolator 分布式事务如何保证一致性?专家P1
本质问题

这道题考察对 "NewSQL 如何用一套系统同时扛住事务型与分析型负载" 的深度理解。核心要回答:HTAP(Hybrid Transactional/Analytical Processing)如何避免 OLTP 与 OLAP 互相干扰?TiDB(SQL 层)、TiKV(行存)、TiFlash(列存)三者如何协同?Raft 在 TiKV 中如何做多副本共识与 Leader 选举?Percolator 两阶段提交如何实现跨 Region 分布式事务?Region 分裂与迁移如何支撑弹性扩展?这是分布式数据库的"天花板"题。

演进脉络

分布式数据库演进:

flowchart LR A["单机 MySQL
强一致
难扩展"] --> B["分库分表
ShardingSphere
应用侵入"] B --> C["NewSQL
Spanner(2012)
Google全球分布式"] C --> D["TiDB(2015)
开源NewSQL
兼容MySQL"] D --> E["HTAP
TiFlash列存
行列共存"] style A fill:#fee2e2,stroke:#dc2626 style B fill:#fef3c7,stroke:#d97706 style D fill:#dcfce7,stroke:#059669 style E fill:#dbeafe,stroke:#2563eb
图 14.5-1 分布式数据库演进:单机 MySQL → 分库分表 → NewSQL(Spanner)→ TiDB → HTAP 行列共存

单机 MySQL:强 ACID、生态成熟,但单机容量与扩展性受限,TB 级以上需分库分表。

分库分表(ShardingSphere/Vitess):中间件层把数据按分片键路由到多个 MySQL 实例。水平扩展了,但应用侵入强(跨库 JOIN/事务难)、运维复杂、扩容需 reshard 迁移数据。

NewSQL(Spanner,2012):Google 提出用分布式存储 + 共识协议 + 分布式事务实现"像单机 MySQL 一样用、却天然分布式扩展"的数据库。Spanner 用 Paxos + TrueTime(原子钟)+ 全局事务,奠定 NewSQL 范式。

TiDB(2015,PingCAP):开源 NewSQL,架构分三层——TiDB(SQL 层,无状态)+ TiKV(分布式 KV 行存,Raft 多副本)+ PD(Placement Driver,调度)。兼容 MySQL 协议,应用迁移成本低。用 Raft 替代 Paxos(更易理解实现),用 Percolator 替代 TrueTime(无需原子钟)。

HTAP(TiFlash):引入 TiFlash 列存节点,通过 Raft Learner 异步复制 TiKV 行存数据成列存。OLTP 走 TiKV(行存点查快),OLAP 走 TiFlash(列存扫描快),行列物理隔离互不干扰,强一致(TiFlash 可配强一致读)。

演进驱动力:① 透明扩展——分库分表应用侵入大,NewSQL 对应用透明;② 一致性——分库分表跨库事务难,NewSQL 原生分布式 ACID;③ 行列共存——OLTP 与 OLAP 分库(MySQL+ClickHouse)数据同步复杂,HTAP 一套搞定。

TiDB 版本演进:④ TiDB 4.x(2020)——TiFlash 列存 GA,HTAP 起步(行列共存,分析走列存);⑤ TiDB 5.x(2021-2022)——HTAP 成熟(MPP 引擎增强)、TiDB Cloud 上线;⑥ TiDB 6.x(2022-2023)——Placement Rules(按标签将 Region 调度到指定节点,实现冷热数据分离与多租户资源隔离)、TiFlash 资源隔离(独立计算资源池)、并行 DDL(大表加索引速度提升数倍);⑦ TiDB 7.x(2023-2024)——Resource Control(资源组限流,按租户/优先级分配 CPU/IO 配额)、Global Sort(分布式排序解决大规模 DDL 回填的内存瓶颈)、TiDB Cloud Serverless(按量付费的 Serverless HTAP);⑧ TiDB 8.x(2024+)——Vector Search(向量检索,支持 AI/RAG 场景)、Distributed Task Framework(分布式后台任务调度)。选型要点:4.x 的 TiFlash 是 HTAP 起点;6.x 的 Placement Rules + 资源隔离是生产级 HTAP 里程碑;7.x 的 Resource Control 使多租户隔离可行;8.x 的 Vector Search 使 TiDB 兼具关系 + 向量检索能力。

第一性原理

TiDB 整体架构:

flowchart TD APP["应用/JDBC"] --> TDB["TiDB Server
(无状态 SQL 层)
解析/优化/执行"] TDB --> PD["PD
Placement Driver
调度/元数据/Region 路由"] TDB --> TKV1["TiKV
(行存, Raft 多副本)
OLTP 点查/事务"] TDB --> TF1["TiFlash
(列存, Raft Learner)
OLAP 扫描分析"] TKV1 -.Raft 异步复制.-> TF1 PD -.调度.-> TKV1 PD -.调度.-> TF1 style TDB fill:#dbeafe,stroke:#2563eb style TKV1 fill:#dcfce7,stroke:#059669 style TF1 fill:#fef3c7,stroke:#d97706 style PD fill:#f3e8ff,stroke:#7c3aed
图 14.5-2 TiDB 架构:TiDB 无状态 SQL 层 + TiKV 行存(Raft)+ TiFlash 列存(Learner)+ PD 调度

TiDB(SQL 层,无状态):解析 SQL、生成执行计划、下推算子到 TiKV/TiFlash 执行。无状态可水平扩展,前面挂负载均衡。它不存数据,只做 SQL 处理与协调。

TiKV(行存 + Raft):分布式事务 KV 引擎,数据按 Key 范围切成 Region(默认 96MB),每个 Region 是一个 Raft Group(多副本,通常 3 副本)。Raft 共识保证:① Leader 读写——写请求到 Leader,复制到 Follower,多数派确认才提交;② Leader 选举——Leader 故障,Follower 超时发起选举,多数派同意成为新 Leader;③ 日志复制——写操作记 Raft Log,按序复制应用,保证多副本一致。Region 过大自动分裂,PD 调度 Region 在节点间迁移均衡负载。

TiFlash(列存 + Raft Learner):作为 TiKV 的 Raft Learner(不参与投票,异步接收日志),把行存数据转成列存。OLAP 查询走 TiFlash 列存扫描快,OLTP 走 TiKV 行存点查快,物理隔离互不干扰。通过"强一致读"配置(等 Learner 追平)可让 TiFlash 读到最新数据。

分布式事务:Percolator 两阶段提交:TiDB 事务基于 Google Percolator 模型,核心是单点协调 + MVCC + 两阶段:

  • Prewrite 阶段:对涉及的所有 Key 加锁(写 primary lock + secondary locks),写数据为 MVCC 多版本。任一 Key 加锁失败则回滚。
  • Commit 阶段:先提交 primary lock(原子),再异步清理 secondary locks。primary 提交成功即事务成功,secondary 由后续事务或异步清理。

事务隔离用 MVCC + Snapshot Isolation(快照隔离,SI),每个事务读一个一致性快照。冲突检测靠锁与时间戳(TSO,PD 提供全局单调递增时间戳)。

在线 DDL:TiDB DDL 是异步在线的——状态机驱动(DeleteOnly→WriteOnly→WriteReorganization→Public),全程不锁表,大表加索引在后台分批回填,业务无感。这是相比 MySQL(DDL 可能锁表)的显著优势。

维度 TiDB MySQL 分库分表 CockroachDB
架构 TiDB+TiKV+TiFlash+PD 中间件+多 MySQL 对等节点(无分层)
共识 Raft(Region 多副本) 无(主从复制) Raft(Range 多副本)
事务 Percolator 两阶段 + SI 跨库 XA/Seata(弱) 类似 Spanner + Serializable
HTAP 强(TiFlash 列存隔离) 无(需外挂 ClickHouse) 弱(无独立列存)
MySQL 兼容 高(协议+语法) 原生 MySQL 中(PostgreSQL 风味)
扩展 Region 自动分裂迁移 reshard 迁移 Range 自动分裂迁移
运维 组件多(TiDB/TiKV/PD) 中间件+多实例 对等节点统一
适用 大规模 OLTP+OLAP、强一致、MySQL 兼容 已有 MySQL 栈、可接受侵入 多地域强一致、PostgreSQL 生态
工程实践

TiDB 兼容 MySQL 协议,JDBC 直连——把 TiDB 当 MySQL 用,只需改连接 URL。HTAP 通过 hint 或自动路由让 OLAP 走 TiFlash。

// Spring Boot 配置:把 TiDB 当 MySQL(驱动用 mysql-connector-j)
spring.datasource.url=jdbc:mysql://tidb-cluster:4000/db?useSSL=false
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver

// JDBC 直连 TiDB,普通事务/点查走 TiKV(行存)
@Transactional
public Order getOrder(Long id) {
    return jdbcTemplate.queryForObject(
        "SELECT * FROM orders WHERE id=?", new OrderRowMapper(), id);
}

// OLAP 分析:用 hint 强制走 TiFlash 列存,加速大范围扫描
public List<RegionStat> statByRegion() {
    return jdbcTemplate.query(
        "SELECT region, COUNT(*) cnt, SUM(amount) total " +
        "FROM orders WHERE create_time > ? " +  // 大范围扫描
        "GROUP BY region /*+ read_from_storage(tiflash[orders]) */",  // 走列存
        rs -> { /* 映射结果 */ }, weekAgo);
}

HTAP 行列同步:TiFlash 作为 Raft Learner 异步接收 TiKV 日志,存在同步延迟(秒级)。对实时性要求高的分析可配强一致读(等 Learner 追平),但会增延迟;容忍秒级延迟则用默认异步,OLAP 查询走 TiFlash 性能高。设计上把实时点查/事务放 TiKV,离线/准实时分析放 TiFlash。

最佳实践

TiDB 工程四要点:① 主键避免自增 ID——自增主键写热点集中在一个 Region,用雪花 ID/随机前缀打散;② OLAP 查询显式 hint 走 TiFlash——大范围扫描走列存快几十倍,但接受秒级延迟;③ 大事务拆分——TiDB 事务有大小区限制(默认 100MB),ETL 类大批量写应分批;④ 在线 DDL 错峰执行——虽然不锁表,但回填索引消耗资源,应在低峰执行。

深度追问

追问 1:TiDB 与 MySQL 的兼容性边界在哪?

TiDB 高度兼容 MySQL 协议与大部分语法,但仍有边界:① 存储过程/触发器/部分函数——TiDB 支持有限或行为不同;② 事务隔离——TiDB 默认 SI(快照隔离),MySQL 默认 RR,且 TiDB 无 gap lock,某些防幻读写法行为不同;③ 外键——TiDB 支持但性能开销大,通常建议应用层保证;④ 大事务——TiDB 事务有大小与时间限制,不适合 MySQL 那种大事务;⑤ 自增 ID——TiDB 自增 ID 不保证连续不回退(分布式生成),且会热点。迁移时应充分回归测试,重点验证存储过程、复杂 JOIN、事务语义。

追问 2:Raft 与 Paxos 的工程取舍?

Paxos 是共识算法的理论基础(更通用但难理解、难实现),Raft 是为"可理解性"设计的 Paxos 简化版。TiDB/CockroachDB 选 Raft 的原因:① 易实现易维护——Raft 强 Leader 模型与清晰的状态机,工程实现 bug 少;② 日志复制直观——Leader 单点顺序写日志复制,比 Paxos 的多 Proposer 简单;③ 成员变更简单——Raft 有成熟的联合共识/单步成员变更。代价是 Raft 强 Leader 在 Leader 切换时有短暂不可用(毫秒级选举),且不如 Multi-Paxos 灵活。对工程数据库,可维护性 > 理论通用性,Raft 是更优取舍。

常见陷阱
易错点
  • 热点 Region(自增主键):自增 ID 集中写入一个 Region 导致单点热点,应改用雪花 ID/SHARD_ROW_ID_BITS 打散。
  • 大事务限制:TiDB 事务默认 100MB 上限,ETL 大批量写报错,应分批或用 TiDB Lightning 批量导入。
  • TiFlash 列存延迟同步:异步复制有秒级延迟,强实时分析未配强一致读会读到旧数据。
  • 把所有查询都走 TiFlash:点查走列存反而慢(列存不擅长点查),OLTP 点查应走 TiKV 行存。
  • 以为完全兼容 MySQL:存储过程、部分函数、外键、隔离级别有差异,迁移需回归测试。
  • 忽视 PD 调度:Region 分布不均导致热点,应监控 PD 调度与 Region 分布,必要时手动调度。
C14.06
ClickHouse 列存为何适合 OLAP?MergeTree 引擎与物化视图如何加速分析?架构P1
本质问题

这道题考察对 "列存为什么在分析场景碾压行存,以及 ClickHouse 的工程极致" 的理解。核心要回答:列存相比行存在分析查询上的优势来源(压缩、缓存、向量化)?ClickHouse 为什么快(向量化执行 + SIMD + MergeTree)?MergeTree 的"有序+分区+Part 合并"如何组织数据?物化视图如何预聚合加速?ClickHouse 为什么不适合高频更新与高并发点查?这是 OLAP 引擎的核心原理题。

演进脉络

分析存储演进:

flowchart LR A["行存 OLTP
MySQL/PG
事务强
分析慢"] --> B["列存 OLAP
列式存储
压缩+向量化"] B --> C["ClickHouse(2016)
Yandex
极致列存分析"] C --> D["物化视图/预聚合
ReplacingMergeTree
实时数仓"] style A fill:#fee2e2,stroke:#dc2626 style B fill:#fef3c7,stroke:#d97706 style C fill:#dcfce7,stroke:#059669 style D fill:#dbeafe,stroke:#2563eb
图 14.6-1 分析存储演进:行存 OLTP → 列存 OLAP → ClickHouse → 物化视图实时数仓

行存(OLTP,MySQL/PostgreSQL):数据按行连续存储,适合点查与事务(一次读整行),但分析查询只取几列却要读整行,IO 浪费大,且行存压缩率低(同行各列类型不同)。

列存(OLAP):数据按列连续存储,分析查询只读所需列,IO 大幅减少;同列数据类型一致,压缩率极高;列式布局天然适合向量化执行。代价是单行写入/更新慢(要拆到多列文件)。

ClickHouse(2016,Yandex 开源):为 OLAP 分析极致优化的列存 DBMS。核心特点:向量化执行(SIMD)、MergeTree 引擎(有序+分区+Part 合并)、高压缩比、单机查询极快。原为 Yandex.Metrika 网站分析设计,后成为开源 OLAP 标杆。

物化视图/预聚合(实时数仓):用物化视图预聚合(如按分钟/小时聚合),查询命中物化视图秒级返回;ReplacingMergeTree 利用版本去重支持幂等更新。使 ClickHouse 从"离线分析"走向"实时数仓"。

演进驱动力:① 分析 IO 效率——行存分析读整行浪费,列存只读所需列;② 压缩降成本——列存同类型压缩率高,PB 级数据省存储;③ 向量化榨干 CPU——SIMD 一次处理一批数据,分析查询吞吐量百倍提升;④ 实时性——物化视图让分析从 T+1 走向秒级实时。

ClickHouse 版本演进:⑤ 22.x(2022)——Window 函数增强(新增 GROUP BY 修饰符 WITH ROLLUP/CUBE)、Object 类型实验性支持、S3 表函数成熟、Lightweight DELETE(22.8 起,轻量删除不再依赖 ALTER TABLE … DELETE 突变机制);⑥ 23.x(2023)——Union Merge 引擎、Query Cache(结果级缓存,重复查询秒级命中)、Parallel Replicas(并行副本读取,实验性)、JSON 类型实验性预览;⑦ 24.x(2024)——JSON 类型(原生 JSON 列存,替代用 String 手动 parse)、Variant 类型(动态类型列,24.3 实验性、24.8 正式)、Async Insert 批量化(降低小批量写入开销)、Tiered Storage(冷热分层存储,SSD + HDD + S3 多层)、CLUSTER BY(自动分片均衡);⑧ 25.x(2025)——进一步优化 Vector Search(向量检索能力)、改进 PROJECTION(预投影,类似物化视图的自动路由)、强化 Streaming(Kafka/Vector 引擎集成成熟)。选型要点:22.x 的 Lightweight DELETE 与 23.x 的 Query Cache 是从"纯分析"走向"准实时数仓"的关键;24.x 的 JSON/Variant 类型 + 分层存储是 PB 级降本的重要手段。

第一性原理

行存 vs 列存对比:

flowchart TD subgraph ROW["行存(OLTP)"] R1["行1: id,name,age"] R2["行2: id,name,age"] R3["行3: id,name,age"] end subgraph COL["列存(OLAP)"] C1["id列: 1,2,3"] C2["name列: a,b,c"] C3["age列: 20,30,40"] end ROW -.查age需读全部列.-> Q1["IO 浪费"] COL -.查age只读age列.-> Q2["IO 精准+高压缩+向量化"] style ROW fill:#fee2e2,stroke:#dc2626 style COL fill:#dcfce7,stroke:#059669
图 14.6-2 行存 vs 列存:行存查单列读整行浪费 IO,列存只读目标列、压缩高、向量化友好

列存压缩:同列数据类型一致,可用针对性编码:① 字典编码——低基数列(如 status)存字典+整数索引,极大省空间;② RLE(Run-Length Encoding)——连续重复值存(值,长度),有序数据压缩率极高;③ Delta 编码——时间戳/自增列存差值;④ LZ4/ZSTD——通用压缩。典型列存压缩比 5-10 倍,PB 级数据省巨额存储与 IO。

向量化执行(SIMD):ClickHouse 把数据按列批量(如 65536 行一批)送入 CPU,用 SIMD 指令(AVX2/AVX-512)一次处理一批。如 SUM(age) 不再逐行循环,而是一条指令算一批。配合编译期代码生成与循环展开,单核吞吐量是逐行执行的数十倍,这是 ClickHouse "快"的核心。

MergeTree 引擎——ClickHouse 最常用引擎,核心三要素:

  • 分区(PARTITION BY):按分区键(如 toYYYYMM(create_time))把数据分到不同分区目录,查询按分区裁剪,跳过无关分区。
  • 排序键(ORDER BY):数据按排序键有序存储,查询利用有序性快速定位(类似聚集索引),范围查询极快。
  • Part 合并:写入产生新 Part(数据块),后台合并小 Part 成大 Part(合并保持排序)。Part 过多影响查询(要合并多 Part 结果),需控制 Part 数。

物化视图(Materialized View):本质是预聚合的自动维护表。定义物化视图(如按小时聚合订单),原始表写入时 ClickHouse 自动把数据聚合后写入物化视图。查询命中物化视图则秒级返回(已预聚合)。与普通视图(仅存查询定义,每次执行)不同,物化视图物化了结果数据,是用空间换时间的典型。

ReplacingMergeTree 去重:MergeTree 变种,合并时按 ORDER BY 键 + VERSION 去重,保留最新版本。支持幂等写入(重复写同主键合并去重),但去重只在合并时发生(异步),查询时仍可能有重复,需用 FINAL 强制去重(有性能损耗)。适合"更新不频繁、可接受最终一致"的场景。

维度 ClickHouse MySQL Doris
定位 OLAP 列存分析 OLTP 行存事务 OLAP 列存(MPP)
存储 列存 + MergeTree 行存 + InnoDB B+树 列存 + Segment
压缩 极高(字典/RLE/Delta) 低(行存) 高(列存)
执行 向量化 SIMD 逐行 向量化 + MPP
更新 弱(批量写,不适合高频更新) 强(事务更新) 中(支持更新但弱于 MySQL)
点查 差(高并发点查慢) 强(索引点查快) 中
JOIN 弱(大 JOIN 性能一般) 强 中(MPP JOIN)
并发 低(不适合高并发查询) 高 中高
适用 海量数据分析、日志/埋点、实时大屏 事务型业务 统一分析、实时数仓、JOIN 多
工程实践

ClickHouse JDBC 示例 + 分区/排序键设计:

-- 1. 建表:MergeTree,按月分区,按(user_id, event_time)排序
CREATE TABLE events ON CLUSTER cluster (
    id          UInt64,
    user_id     UInt64,
    event_time  DateTime,
    event_type  LowCardinality(String),  -- 低基数用字典编码
    amount      Decimal(18,2)
) ENGINE = MergeTree()
PARTITION BY toYYYYMM(event_time)        -- 月分区,便于分区裁剪与TTL
ORDER BY (user_id, event_time)            -- 排序键,按用户+时间查询快
SETTINGS index_granularity = 8192;

-- 2. 物化视图:按小时预聚合订单金额(查询命中秒级返回)
CREATE MATERIALIZED VIEW mv_order_hourly
ENGINE = SummingMergeTree()
PARTITION BY toYYYYMM(hour_ts)
ORDER BY (hour_ts, user_id)
AS SELECT
    toStartOfHour(event_time) AS hour_ts,
    user_id,
    sum(amount) AS total_amount,
    count() AS cnt
FROM events WHERE event_type = 'order'
GROUP BY hour_ts, user_id;

-- 3. 查询:大屏统计走物化视图(已预聚合)
SELECT hour_ts, sum(total_amount) FROM mv_order_hourly
WHERE hour_ts >= '2026-07-01 00:00:00'
GROUP BY hour_ts ORDER BY hour_ts;
// Spring Boot JDBC 查询 ClickHouse(驱动 clickhouse-jdbc)
spring.datasource.url=jdbc:ch://clickhouse-host:8123/analytics
spring.datasource.driver-class-name=com.clickhouse.jdbc.ClickHouseDriver

public List<HourlyStat> queryHourly(LocalDateTime from) {
    return jdbcTemplate.query(
        "SELECT hour_ts, sum(total_amount) FROM mv_order_hourly " +
        "WHERE hour_ts >= ? GROUP BY hour_ts ORDER BY hour_ts",
        (rs, i) -> new HourlyStat(rs.getString(1), rs.getBigDecimal(2)),
        from);
}
最佳实践

ClickHouse 工程四要点:① 排序键选高频查询列——ORDER BY 是聚集索引,按业务高频过滤/排序列设计(如 user_id+time);② 分区按时间粗粒度——月/周分区,配合 TTL 自动过期,避免分区过细导致 Part 爆炸;③ 用物化视图预聚合——大屏/报表查询走预聚合视图秒级返回,避免每次扫明细;④ 批量写而非单条写——ClickHouse 擅长批量插入,单条高频写会产生大量小 Part 拖垮合并。

深度追问

追问 1:ClickHouse 为什么不适合高频更新?

列存 + MergeTree 的设计前提是批量追加。每次写入产生一个新 Part(数据块),高频单条写会产生海量小 Part,后台合并(merge)跟不上,导致 Part 数爆炸(查询要合并多 Part 结果,性能暴跌,甚至报 "Too many parts")。更新(UPDATE/DELETE)在 ClickHouse 是异步重写 Part(ALTER UPDATE),代价极高。所以 ClickHouse 适合"批量写入 + 少量更新",高频更新应走 OLTP 库(MySQL)再同步到 ClickHouse,或用 ReplacingMergeTree 做最终一致去重。

追问 2:物化视图与普通视图的差异?

普通视图(View)只存查询定义(SQL),每次查询时展开执行底层 SQL,不存数据,不加速——本质是"命名查询"。物化视图(Materialized View)物化了结果数据——原始表写入时触发聚合写入物化视图表,查询直接读已聚合的数据,秒级返回,是用空间换时间。ClickHouse 的物化视图是增量自动维护的(新数据按聚合规则增量写入),不是全量重建。代价是占额外存储、且只支持特定聚合(SUM/COUNT 等),复杂转换可能需定时刷新的物化视图。

常见陷阱
易错点
  • 分区粒度过细导致 Part 爆炸:按天/小时分区且频繁写入,产生海量小 Part,合并跟不上报错,应选月/周粗分区。
  • 无排序键查询慢:不设计 ORDER BY 或查询不带排序键前缀,退化为全分区扫描,丧失列存有序优势。
  • 高并发点查性能差:ClickHouse 不擅长高并发点查(无行级索引点查慢、并发能力低),点查应走 MySQL/Redis。
  • 把 ClickHouse 当 OLTP 用:高频单条写/更新/事务都不是它的强项,会拖垮合并。
  • 大 JOIN 性能差:ClickHouse JOIN 优化弱于 MPP 引擎,大表 JOIN 应预先在 ETL 宽表化或用 Doris。
  • ReplacingMergeTree 不加 FINAL 查到重复:去重只在合并时发生,查询时未加 FINAL 可能读到旧版本重复行。
C14.07
图数据库如何建模?Cypher 模式匹配如何高效?Neo4j 何时该用?高级P1
本质问题

这道题考察对 "图数据库如何用属性图建模、Cypher 如何用模式匹配免 JOIN 高效遍历、以及何时该用图数据库" 的理解。核心要回答:属性图模型(节点/关系/属性)相比关系表多表 JOIN 的优势?Cypher 的 MATCH 模式匹配为什么比 SQL 递归 CTE 直观高效?图数据库通过"免 JOIN 邻接遍历"为何在多跳关系查询上碾压关系库?什么业务(推荐/风控/知识图谱)该用图数据库?这是 NoSQL 建模的"关系密集型"题。

演进脉络

关系密集型数据建模演进:

flowchart LR A["关系型
多表 JOIN
深遍历慢"] --> B["图数据库
Neo4j(2007)
属性图+免JOIN"] B --> C["Cypher 查询语言
模式匹配
声明式图遍历"] C --> D["GQL 标准
(2024)
图查询统一标准"] style A fill:#fee2e2,stroke:#dc2626 style B fill:#dcfce7,stroke:#059669 style C fill:#fef3c7,stroke:#d97706 style D fill:#dbeafe,stroke:#2563eb
图 14.7-1 关系密集型建模演进:关系型 JOIN → Neo4j 属性图 → Cypher → GQL 标准

关系型(多表 JOIN):用外键关联多张表表达关系。查"朋友的朋友的朋友"需多次 JOIN,每跳 JOIN 数据量指数膨胀,深遍历(3 跳以上)性能急剧下降。

Neo4j(2007):原生图数据库,用属性图模型——节点存实体、关系存连接(带方向与属性)、属性挂节点/关系上。关系不是外键,而是物理指针——遍历时直接跳到邻接节点,免 JOIN,多跳遍历性能恒定。

Cypher 查询语言:Neo4j 的声明式图查询语言,用 MATCH 模式匹配(如 (a)-[:KNOWS]->(b))直观表达图模式。比 SQL 递归 CTE 简洁得多,让图查询"像画图一样写"。

GQL 标准(2024,ISO/IEC 39075):图查询语言国际标准,融合 Cypher 与 PGQL,成为图数据库的"SQL"。Neo4j 等图库逐步向 GQL 靠拢,统一图查询生态。

演进驱动力:① 关系密集——社交/风控/知识图谱关系多且深,关系库 JOIN 退化;② 遍历效率——图库物理指针免 JOIN,深遍历恒定性能;③ 表达直观——Cypher 模式匹配比 SQL 递归 CTE 易写易读;④ 标准统一——GQL 让图查询摆脱方言,走向标准化。

Neo4j 版本演进:⑤ Neo4j 4.x(2020)——Multi-database(单实例支持多库隔离)、Reactive Driver(响应式驱动,支持背压)、Causal Cluster 改进(读写路由自动分流);⑥ Neo4j 5.x(2022-2023)——APOC Core 转正(Awesome Procedures On Cypher 核心过程进入官方发行包,提供 450+ 图算法/工具函数)、GDS(Graph Data Science)库成熟(PageRank/Louvain/Node Similarity/Link Prediction 等图算法内置,支持图嵌入与机器学习)、Fabric(跨库联邦查询,一个查询遍历多个图库)、Cypher 5 语法增强(子查询改进、EXISTS/COUNT 子句);⑦ Neo4j 2024+(GQL 适配)——逐步支持 GQL 标准(ISO/IEC 39075),Neo4j Aura Cloud(全托管图数据库)、向量索引(Vector Index,支持 RAG + 图增强检索)。选型要点:5.x 的 GDS 是从"图存储"走向"图智能"的关键——PageRank/社区发现等算法无需自实现,GDS 库直接调用;APOC 转正降低了部署依赖(4.x 需手动安装插件,5.x 核心功能内置)。

第一性原理

属性图模型:

flowchart TD A((Alice
Person)) -->|KNOWS
since:2020| B((Bob
Person)) B -->|WORKS_AT| C[Google
Company] A -->|LIKES| D((Post1
Post)) B -->|LIKES| D style A fill:#dbeafe,stroke:#2563eb style B fill:#dbeafe,stroke:#2563eb style C fill:#dcfce7,stroke:#059669 style D fill:#fef3c7,stroke:#d97706
图 14.7-2 属性图模型:节点(Alice/Bob/Google/Post)+ 关系(KNOWS/WORKS_AT/LIKES)+ 属性(since:2020)

属性图三要素:① Node(节点)——实体,可有多个 Label(如 Person),挂属性;② Relationship(关系)——连接两节点,有方向、有类型(如 KNOWS),可挂属性(如 since);③ Property(属性)——键值对,挂在节点或关系上。关键区别:关系库的关系是"外键引用"(遍历需 JOIN 查表),图库的关系是物理指针(遍历直接跳指针,免 JOIN)。

Cypher MATCH 模式匹配:用 ASCII 艺术表达图模式:

// 找 Alice 的 2 跳朋友(朋友的朋友)
MATCH (a:Person {name:'Alice'})-[:KNOWS*2..2]-(fof)
RETURN fof.name

// 推荐:与 Alice 有共同兴趣的人
MATCH (a:Person {name:'Alice'})-[:LIKES]->(p:Post)<-[:LIKES]-(other:Person)
WHERE other <> a
RETURN other.name, count(p) AS common
ORDER BY common DESC LIMIT 5

(a)-[:KNOWS]->(b) 直观表达"A 认识 B",*2..2 表示变长路径 2 跳。Cypher 把图遍历声明式化,引擎自动优化遍历顺序与剪枝。

索引与遍历(免 JOIN):Neo4j 用免索引邻接(index-free adjacency)——每个节点维护指向邻接节点的物理指针,遍历关系时直接跳指针,无需查索引表。这意味着遍历深度增加,性能几乎不退化(关系库每多一跳 JOIN 数据量指数膨胀)。节点属性可建索引加速初始定位(如按 name 找起点),但关系遍历靠物理指针。

ACID 事务:Neo4j 是完整 ACID 的图数据库——事务保证原子性/一致性/隔离性/持久性,这与多数 NoSQL 的最终一致不同。事务以节点/关系为锁粒度,适合需要强一致的图业务(如金融风控关系)。

维度 Neo4j MySQL 递归 CTE RedisGraph
模型 属性图(原生图) 关系表 + 外键 + 递归 CTE 属性图(Redis 模块)
遍历 免 JOIN 物理指针(深遍历恒定) 多 JOIN(深遍历指数退化) 免 JOIN(内存图)
查询语言 Cypher / GQL SQL 递归 CTE(繁琐) Cypher 子集
事务 完整 ACID 完整 ACID 弱(Redis 内存)
持久化 磁盘持久化 磁盘持久化 内存为主(可持久化)
规模 亿级(单机为主,Fabric 分片) 亿级 千万级(受内存)
适用 关系密集、深遍历、强一致图业务 关系简单、以事务为主 小图、低延迟、可接受最终一致
工程实践

Spring Data Neo4j 示例:用 @Node 映射节点,@Relationship 映射关系,Repository 自动生成 Cypher。

// 1. 节点实体
@Node("Person")
public class PersonNode {
    @Id @GeneratedValue
    private Long id;
    private String name;

    @Relationship(type = "KNOWS", direction = Direction.OUTGOING)
    private List<PersonNode> friends = new ArrayList<>();

    @Relationship(type = "LIKES", direction = Direction.OUTGOING)
    private List<PostNode> likedPosts = new ArrayList<>();
}

// 2. Repository + 自定义 Cypher(推荐系统:共同兴趣好友)
public interface PersonRepository extends Neo4jRepository<PersonNode, Long> {

    @Query("MATCH (a:Person {name:$name})-[:LIKES]->(p:Post)<-[:LIKES]-(other:Person) " +
           "WHERE other.name <> $name " +
           "RETURN other.name AS name, count(p) AS common " +
           "ORDER BY common DESC LIMIT $limit")
    List<Recommendation> recommendByCommonInterest(String name, Integer limit);

    // 2 跳朋友(朋友的朋友)
    @Query("MATCH (a:Person {name:$name})-[:KNOWS*2..2]-(fof) " +
           "RETURN DISTINCT fof.name")
    List<String> friendsOfFriends(String name);
}

// 3. 服务调用
public List<Recommendation> recommend(String user) {
    return personRepository.recommendByCommonInterest(user, 5);
}

推荐系统应用:图数据库天然适合推荐——"用户-商品-标签-用户"的多跳关系在图里一次 MATCH 搞定,而关系库需多次 JOIN 性能差。其他典型场景:社交关系、金融风控(资金流向/团伙识别)、知识图谱、权限继承(角色-权限链路)。

最佳实践

Neo4j 工程四要点:① 热点节点属性建索引——初始定位(如按 name/userId 找起点)必须建索引,否则全图扫描;② 限制遍历深度——无界 MATCH 会路径爆炸,应限制跳数(如 *1..3)与结果数;③ 关系建模带方向与属性——方向语义化(KNOWS/WORKS_AT),属性记录关系元数据(since/weight);④ 大图用 Fabric 分片——亿级节点单机受限,用 Neo4j Fabric 按子图分片跨库查询。

深度追问

追问 1:图数据库与关系型的边界在哪?

核心看关系密度与遍历深度。① 用图数据库——关系密集(实体间多对多关联多)、需要深遍历(3 跳以上,如朋友的朋友、资金穿透)、关系本身有属性/类型(如 KNOWS/TRANSFER)。典型:社交、推荐、风控、知识图谱。② 用关系库——关系简单(少量外键)、以事务与精确查询为主、关系不需要深度遍历。典型:订单、账户、库存。③ 混合用——业务主体数据存关系库(MySQL 保证事务),关系子图同步到 Neo4j 做关系分析(CDC/ETL 同步),各取所长。判断标准:若一个查询在关系库要 4+ 次 JOIN 或递归 CTE,且性能差,就该考虑图数据库。

追问 2:超大图(亿级节点)如何分片?

Neo4j 单机版(Community/Enterprise)是单机图——所有节点关系在一台机器,靠免 JOIN 邻接保证遍历性能,但规模受单机内存/磁盘限制(典型亿级节点)。超大规模分片用 Neo4j Fabric——把图按子图切分到多个 Neo4j 实例,Fabric 驱动跨实例查询合并。挑战:① 切分策略——按业务子图(如按地区/租户)切分,减少跨分片遍历;② 跨分片遍历——跨实例遍历需 Fabric 协调,性能不如单机;③ 分布式事务——跨分片写事务复杂。替代方案:分布式图库(如 NebulaGraph/JanusGraph)原生支持分片但生态弱于 Neo4j。原则:能单机就单机(免 JOIN 优势最大),必须分片时按业务子图切。

常见陷阱
易错点
  • 全图遍历无限制:MATCH 不限深度/数量,遍历整个图导致超时/OOM,应限制跳数与 LIMIT。
  • 无索引的节点扫描:起点定位不建索引(如 name),退化为全图扫描所有节点,应建属性索引。
  • 深度过大导致路径爆炸:3-4 跳以上路径组合指数增长,应限制深度或用 APOC 过程分批遍历。
  • 把所有数据塞进图库:图库不擅长大量属性存储与事务型 CRUD,主体业务数据应留关系库,只把关系子图放图库。
  • 忽视 Fabric 跨分片性能:分片后跨实例遍历性能下降,应按业务子图切分减少跨分片查询。
  • 关系建模无方向/属性:关系不带方向与属性丢失语义,应明确方向(OUTGOING/INCOMING)与关系属性。
  • 不看执行计划就上线 Cypher:复杂查询应先用 EXPLAIN/PROFILE 验证是否走节点索引(NodeIndexSeek)而非全图扫描(AllNodesScan),深遍历需确认 Planner 的 Expand 策略,避免误以为索引生效实则全图扫描。
生产量化证据(压测 / 命中率 / 容量)

选用 Neo4j 的核心量化结论:图遍历延迟随跳数指数增长,能否满足业务 RT 取决于图规模、page cache 命中率与遍历深度上限。

指标 参考基准(业界常见量级) 说明
图规模上限 亿级节点 / 十亿级边(单机 Enterprise) 受单机 page cache(堆外)内存约束,超规模需 Fabric 分片
1 跳查询延迟 ms 级(1-10ms,命中 cache) 免 JOIN 物理指针,邻接遍历恒定开销
3-4 跳查询延迟 s 级(0.5-3s,深遍历易劣化) 路径组合指数膨胀,>3 跳需限深度/加索引
page cache 命中率 目标 95%+(决定 RT 量级) 未命中退化为磁盘随机 IO,延迟上升约一个数量级
关键结论

压测务必量化 1/2/3/4 跳 RT 与 page cache 命中率;深查询(>3 跳)默认加 *1..3 深度上限与 LIMIT,避免路径爆炸。以上为参考基准,需以自身生产数据替换。

C14.08
分布式 SQL/NewSQL(CockroachDB/YugabyteDB/OceanBase/TiDB)与 TiDB 选型边界架构P1
本质问题

这道题考察对 NewSQL(分布式 SQL)选型边界 的判断:它用"SQL 接口 + 自动分片 + 分布式事务 + 弹性扩展"试图兼得 RDBMS 的易用与 NoSQL 的扩展,但代价是延迟与复杂度。本质是"业务透明的强一致分布式事务 vs 中间件分库分表 vs NoSQL"的分工。

演进脉络

单机 RDBMS:强一致、事务完备,但单机容量/连接数有天花板。

分库分表(ShardingSphere):中间件层把数据按分片键打散,业务需感知分片、跨分片事务弱(依赖柔性事务)。

NoSQL:为扩展性放弃 SQL 与跨行事务(Cassandra/MongoDB),适合简单 KV/文档。

NewSQL:受 Google Spanner 启发,兼容 SQL + 强一致分布式事务 + 弹性扩缩;国内 OceanBase(蚂蚁)、TiDB(PingCAP)、海外 CockroachDB、YugabyteDB 成熟。

第一性原理
产品 兼容协议 存储/事务 最新版本特性 亮点
TiDB MySQL TiKV(RocksDB+Raft)+PD;Percolator 事务 8.x:Placement Rules + Resource Control + Vector Search HTAP(TiFlash 列存)、生态成熟、AI 向量检索
OceanBase MySQL/Oracle Paxos 多副本;LSM 4.x(2023+):MySQL 8.0 兼容增强、HTAP 列存副本(4.0 起支持列存副本 + 向量化执行) 金融级、TPC-C 冠军、多租户
CockroachDB PostgreSQL Range 分片 + Raft 23.x(2024+):Zone Config 多区域策略、Unbounded Datadriven Transactions 地理分布友好、强一致、Survive 机制(区域级故障自愈)
YugabyteDB PostgreSQL DocDB(tablet+Raft) 2024.x:YSQL PG 15 兼容、Cluster-level Read Replicas、xCluster 跨集群异步复制 PG 兼容度高、云原生、多区域同步复制

横向对比要点:① HTAP 能力——TiDB 最强(独立 TiFlash 列存),OceanBase 次之(只读副本),CockroachDB/YugabyteDB 弱(需外挂 OLAP);② 地理分布——CockroachDB 的 Zone Config + Survive 最成熟,YugabyteDB 的 xCluster 适合多区域同步,TiDB 通过 PD 调度实现但不如前两者原生;③ 生态兼容——TiDB/OceanBase 兼容 MySQL(国内生态优势),CockroachDB/YugabyteDB 兼容 PostgreSQL(海外生态优势);④ 运维复杂度——TiDB 组件最多(TiDB/TiKV/TiFlash/PD),CockroachDB/YugabyteDB 对等节点架构更简单;⑤ AI 扩展——TiDB 8.x Vector Search 使其兼具关系+向量检索,适合 RAG 场景,其余产品暂无原生向量能力。

与 ShardingSphere 的关键差异:中间件分库分表要求业务显式设计分片键、跨分片 JOIN/事务受限;NewSQL 对业务透明——自动分片、跨节点分布式事务由存储层保证,应用像用单机库。

关键结论

选型边界:强一致分布式事务 + SQL + 弹性扩展 → NewSQL;海量写入无跨行事务 → NoSQL;中等规模、可控分片 → ShardingSphere(更轻、改动小)。NewSQL 的多副本网络 RT 使其单点延迟高于单机,不适合极致低延迟场景。

工程实践

场景匹配:金融核心/全球化多活/超大规模 OLTP → OceanBase/TiDB/CockroachDB;需HTAP(同一份数据兼顾 TP 与分析)→ TiDB(TiFlash)。

兼容度优先:已有 MySQL 生态选 TiDB/OceanBase(MySQL 协议);PG 生态选 CockroachDB/YugabyteDB,迁移成本更低。

资源隔离:HTAP 场景用独立列存副本/资源组隔离分析负载,避免拖垮 TP。

与云托管对比:Aurora/PolarDB 是"单机库上云+存储计算分离",并非真正分布式事务 NewSQL,扩展上限与跨地域能力弱于原生 NewSQL。

深度追问

追问 1:NewSQL 分布式事务如何保证强一致? 多基于 Percolator(两阶段提交 + 全局时间戳)+ 多副本 Raft/Paxos;OceanBase 用 Paxos 组提交。代价是跨节点网络 RT,吞吐/延迟逊于单机。

追问 2:分库分表何时仍优于 NewSQL? 数据量中等、分片键清晰、团队熟悉 ShardingSphere 时,引入 NewSQL 的运维与迁移成本不划算。

追问 3:HTAP 怎么做到一份数据两用? 行存服务 TP,异步同步到列存(TiFlash)服务分析,读写资源隔离,避免分析查询影响交易。

常见陷阱
易错点
  • 以为 NewSQL 性能 ≈ 单机:多副本网络开销,点查延迟更高,不适合极致低延迟单点。
  • 盲目替代分库分表:小中型用 ShardingSphere 更轻,引入 NewSQL 运维/迁移成本高。
  • 忽略兼容度差异:MySQL 与 PG 协议生态不同,选错增加迁移成本。
  • 跨地域多活未评估网络延迟:地理分布式下提交延迟受限于副本距离。
  • HTAP 不隔离资源:分析查询吃满 CPU/IO,拖垮在线交易。
扩展补全

前述对比表只给出一行 OceanBase 摘要;作为蚂蚁自研的金融级数据库(TPC-C 榜首),其 Paxos 强一致、跨机房容灾与 MySQL 兼容性值得单独深挖。

  • Paxos 多副本强一致:每个 Partition 有 3+ 副本(1 Leader + 多 Follower),写入需多数派确认,任一节点故障数据不丢。
  • 跨机房部署:3 副本分布在不同机房,任一机房整体故障 RPO=0、RTO 秒级(自动切换),满足金融容灾要求。
  • 多租户:底层共享存储、上层逻辑隔离(类似 Oracle CDB/PDB),适合云上多业务共库降低成本。

金融级特性:

  • 两阶段提交(2PC):Coordinator 协调多个 Participant,保证跨 Partition / 跨库事务原子性。
  • 全局时间戳(GTS):提供全局一致性读,解决跨 Partition 事务的可见性顺序问题。
  • 透明故障切换:Leader 故障后 Follower 秒级当选,应用经 OBProxy 无感切换,无需改连接配置。
  • MySQL 兼容:OB MySQL 模式兼容大部分 MySQL 语法与协议,应用改造成本低,是蚂蚁对外输出的主推形态。

NewSQL 边界:适合 OLTP 高并发写入、金融级强一致、海量数据分库分表替代;不适合复杂 OLAP 分析(列存能力不如 ClickHouse/Doris)、超低延迟单点查询(Paxos 多数派确认增加 RTT)。

深入解析

OceanBase vs TiDB 对比:

维度 OceanBase TiDB
一致性协议 Paxos Raft
存储引擎 LSM-Tree LSM-Tree(TiKV)
分布式事务 2PC + GTS 2PC + TSO
MySQL 兼容 高(OB MySQL 模式) 高(TiDB 协议层兼容)
HTAP 支持(只读副本) 支持(TiFlash 列存)
运维成熟度 商业支持完善 开源社区活跃

选型落点:强一致要求极高、预算充足的金融核心倾向 OceanBase;需要活跃开源生态与灵活 HTAP 的互联网业务倾向 TiDB。

生产实证:信创与国产库迁移边界

信创场景常见路径:MySQL/Oracle → OceanBase / TiDB / openGauss 等。决策不只看协议兼容,还要看:

  • SQL 方言与工具链:存储过程、序列、HINT、备份恢复、监控导出是否齐套。
  • 一致性与多活:金融核心偏强一致多副本(OceanBase/Paxos 等);互联网 HTAP 可看 TiDB+TiFlash。
  • 迁移方法:结构转换 → 全量+增量(CDC)→ 双写/影子流量 → 切流回滚;禁止"停机一天全量导入"作为唯一方案。
  • 应用改造:去掉强依赖的 Oracle 私有包/自治事务;隐式类型与排序规则一次性治理。

与平台工程配合:把"兼容性矩阵 + 回归用例集"固化为 Golden Path,避免每个项目重复踩坑。

C14.09
时序数据库(InfluxDB/TimescaleDB)适用场景与建模高级P1
本质问题

这道题考察 时序数据的特征与专用存储选型:监控指标、IoT 传感、行情 tick 等"写多读少、按时间范围查询、只追加、天然分区"的数据,用关系库或通用 KV 存储会遭遇写入瓶颈与压缩低效;时序数据库(TSDB)为此而生。

演进脉络

RRDTool:轮转数据库,固定大小环形存储,早期监控用。

OpenTSDB:架在 HBase 上的时序方案,依赖 Hadoop 重。

InfluxDB:专用 TSDB,自研 TSM 引擎,写入/压缩极致优化。

TimescaleDB:PostgreSQL 扩展,用 hypertable 自动时间分区,复用完整 SQL 与分析生态。

Prometheus:云原生监控事实标准(单机 TSDB + 拉模型),长期存储需 Thanos/Mimir。

第一性原理

时序数据特征:时间戳为轴、高写入吞吐、按时间范围扫描、数据只追加(immutable)、高基数(tag 组合爆炸)是核心挑战。

存储优化:① 按时间分片(shard/chunk),旧分片转冷/降采样;② 列式 + 时间戳压缩(delta-of-delta 编码时间戳、Gorilla 压缩浮点);③ tag 建倒排/索引加速多维过滤。

产品 模型 适用
InfluxDB measurement + Tag(索引)/Field(值) 极致写入专用场景
TimescaleDB PG hypertable(自动时间分区) 需 SQL/分析、复用 PG 生态
Prometheus 拉模型 TSDB 云原生监控(配长期存储)
TDengine 超级表 + 子表 国内 IoT 高写入
关键结论

选型:监控指标 + 云原生 → Prometheus + 长期存储(Thanos/Mimir);通用时序 + 需 SQL/分析 → TimescaleDB;极致专用写入 → InfluxDB/TDengine。降采样(原始+聚合分层)是控制存储成本的关键。

工程实践

数据建模:metric 作为 measurement,维度(host/region/app)作为 tag(索引、低基数),指标值作为 field;避免把高基数字段(如 user_id)当 tag,否则索引爆炸。

降采样:用连续查询(CQ)/物化视图把原始数据聚合成分钟/小时级,热数据存原始、冷数据只留聚合,大幅省空间。

分层存储:Prometheus 配远程写(Thanos/Mimir/Cortex)做长期存储与全局查询;TimescaleDB 用数据保留策略自动清理旧 chunk。

深度追问

追问 1:高基数(high cardinality)为何是 TSDB 噩梦? tag 每种组合生成一个时间序列,user_id 当 tag 会产生千万级序列,索引与内存暴涨、写入骤降。必须把高基数字段放 field 或采样。

追问 2:Prometheus 长期存储怎么解决? 单机 TSDB 不适合长期/全局,用 Thanos(对象存储+Sidecar)、Mimir/Cortex(分布式)做远程写与全局查询。

追问 3:TSDB 与 OLAP(ClickHouse)区别? TSDB 为时序写入/范围扫描深度优化;ClickHouse 是通用 OLAP,擅长任意维度聚合分析,二者场景互补,常与 TSDB 原始 + ClickHouse 分析并存。

常见陷阱
易错点
  • 高基数 tag 设计不当:把 user_id 等当 tag → 序列爆炸、写入崩。
  • 只存原始不降采样:存储成本指数膨胀,应原始+聚合分层。
  • 用关系库存时序:写入/范围查询性能灾难,应上专用 TSDB。
  • 误把 Prometheus 当长期存储:需远程写 + Thanos/Mimir。
  • TimescaleDB chunk 大小不当:过大/过小都影响分区效率与清理。
生产量化证据(压测 / 命中率 / 容量)

选用 TSDB 的核心量化结论:写入吞吐与压缩比决定写入瓶颈,连续聚合 + 降采样 + 保留策略将查询加速与存储成本同时压到可控量级。

指标 参考基准(业界常见量级) 说明
单机写入吞吐 ~100w points/s(批量写入) 批量+时序引擎优化,远高于关系库逐行 INSERT
压缩比 10-50x(TTL+降采样后) delta-of-delta 时间戳 + Gorilla 浮点压缩
连续聚合查询加速 10-100x 预计算分钟/小时级聚合,免扫原始数据
保留策略(容量) 热 7d 原始 + 冷 1y 降采样 存储增长随保留策略线性可控,单节点 TB 级
关键结论

压测聚焦写入吞吐(points/s)与压缩比,落库必须批量;查询用连续聚合预计算而非扫原始。以上为参考基准,需以自身生产数据替换。

C14.10
PostgreSQL 相对 MySQL 的体系化差异与选型边界?MVCC/VACUUM、扩展生态与生产陷阱如何应对?架构P1
本质问题

本题把 PostgreSQL 从「对比表里的一列」提升为可独立选型的 OLTP/分析基座:与 MySQL 的差异不只在语法,而在 MVCC 实现(堆元组 + VACUUM)、扩展生态(PostGIS/Timescale/pgvector)、优化器与锁模型。分库分表工程见 C07.07;NewSQL/PG 协议系见 C14.08;时序扩展见 C14.09。

演进脉络

对象-关系起步:强调标准 SQL、扩展类型与规则系统,长期走「内核稳定 + 扩展丰富」路线。

MVCC 成熟:更新产生新元组版本,旧版本靠 VACUUM/autovacuum 回收;可见性靠 xmin/xmax 与快照。

扩展爆发:PostGIS(空间)、TimescaleDB(时序)、Citus(分布式)、pgvector(向量)、FDW(外部表)把 PG 做成多模平台。

云与托管:RDS/Aurora PostgreSQL、云厂商分支强化高可用与只读副本;逻辑复制成为 CDC/蓝绿切换标配。

第一性原理
维度 PostgreSQL MySQL/InnoDB
MVCC 堆表多版本 + VACUUM 回收 Undo + Purge,更新常原地改
锁/死锁 行锁粒度细,DDL 锁策略需谨慎 生态工具与运维套路更统一
SQL/类型 标准 SQL、JSONB、数组、自定义类型强 简单 OLTP 友好,生态偏业务中间件
复制 物理流复制 + 逻辑复制(CDC 友好) Binlog 生态(Canal/Debezium)极成熟
扩展 扩展是一等公民 能力多在外围中间件/引擎插件
关键结论

复杂查询/分析扩展/地理/JSONB/向量同库 → 优先 PG;超高并发简单交易 + 国内中间件栈 → MySQL 仍强。选 PG 就必须把 autovacuum、膨胀、长事务 纳入容量与 SRE 指标。

工程实践

VACUUM 治理:监控 n_dead_tup、表膨胀、autovacuum 滞后;大表用调参 + 分区降低单次回收成本;避免长事务拖住 xmin horizon。

指标与演练清单(进生产必盯):① pg_stat_user_tables 的 dead/live 比、last_autovacuum;② pg_stat_activity 长事务与 idle in transaction;③ 复制延迟(写放大时 VACUUM 与 checkpoint 争 IO);④ 季度做一次「大表膨胀演练」——观测 autovacuum workers 打满时的 RT。目标:核心表 dead 比例与 vacuum 滞后有告警,而不是等磁盘满。

连接池:PG 每连接私有内存(常数十 MB 级,视 work_mem/临时对象而定),应用侧 200 直连极易拖垮小规格实例。生产默认 PgBouncer(transaction 模式) 或云托管连接池;把 max_connections 留给池与超管,应用池大小按「活跃事务数」而非「Tomcat 线程数」估算。

逻辑复制演练:上线前至少演一次「表过滤 + DDL 变更」:发布端加列、订阅端兼容默认值;验证冲突表/复制槽增长告警。蓝绿切流用逻辑复制追平后改流量,回滚预案写清槽与快照保留时间。

索引与查询:善用部分索引、表达式索引、GIN(JSONB/全文);复杂 SQL 看 EXPLAIN (ANALYZE, BUFFERS),警惕错误选择度与统计信息过期。大批量导入后强制 ANALYZE。

高可用:主从流复制 + 同步/异步策略;切换用 Patroni/云托管;逻辑复制服务蓝绿与在线 DDL 配套。

多模边界:pgvector 适合中小规模同库检索;十亿级向量仍看 C14.03/C14.04。时序重度用 Timescale(见 C14.09),勿把 PG 当万能仓。

深度追问

追问 1:为何 PG 更新多了会「表胀」? 更新写新版本,旧行暂留;VACUUM 不及时或长事务阻止回收 → 堆与索引膨胀,缓存命中下降。

追问 2:逻辑复制 vs 物理复制? 物理复制字节级、延迟低、库级;逻辑复制表级、可过滤、适合 CDC/异构同步,但 DDL/大对象有边界。

追问 3:与 Citus/NewSQL 如何分工? 单库垂直扩展 + 只读副本不够时:清晰分片键可 Citus;要强透明分布式事务再评估 Yugabyte/Cockroach(C14.08)。

常见陷阱
易错点
  • 关掉或忽视 autovacuum:短期写快,长期性能雪崩。
  • 用 PG 硬扛分库分表全套:无清晰分片与治理时,复杂度不低于 MySQL+ShardingSphere。
  • 把连接池当无限资源:PG 每连接内存成本高,需 PgBouncer 等池化。
  • 统计信息过期:大批量导入后不 ANALYZE,计划劣化。
C14.11
Apache Doris / StarRocks 与 ClickHouse 如何选型?MPP、明细查询与实时数仓边界在哪?架构P1
本质问题

C14.06 已覆盖 ClickHouse 列存机理;本题补齐国内实时数仓主航道 Doris / StarRocks:二者以 MPP + 向量化 + 实时导入见长,强调高并发明细点查、标准 SQL JOIN、物化视图与湖仓一体。选型本质是 「宽表极致扫描」vs「多表关联 + 并发服务」 的取舍。

演进脉络

离线数仓:Hive/Spark 批处理,T+1,难支撑看板秒级。

ClickHouse 崛起:单表宽表、极致压缩与扫描,适合日志/行为分析。

Doris/StarRocks:继承 MPP 思想,强化 FE/BE、实时导入(Stream Load/Routine Load)、物化视图与 MySQL 协议兼容,面向「实时数仓 + 高并发查询」。

湖仓一体:外表/Catalog 对接 Iceberg/Hudi,冷热分层,与 S11.05 湖仓场景呼应。

第一性原理
维度 ClickHouse Doris / StarRocks
执行模型 散列表/管道,大 JOIN 偏弱 MPP 分布式 JOIN 更友好
查询形态 宽表聚合、扫描吞吐极致 多表关联、高并发点查/看板
导入 批/异步为主 Stream/Routine Load 实时友好
协议/生态 自有 SQL 方言 MySQL 协议,BI 接入成本低
运维心智 表引擎族繁多 FE/BE、分桶/分区模型统一
关键结论

日志/行为宽表、极致吞吐 → ClickHouse;实时数仓、多表 JOIN、高并发明细与 MySQL 协议 BI → Doris/StarRocks。二者都不是 OLTP 替代品。

工程实践

模型:分区(时间)+ 分桶(高基数维)控制剪枝;明细与汇总分层,关键看板用异步/同步物化视图。

表模型速查:明细模型适合追加日志;聚合模型适合固定指标预聚合;主键/UNIQUE 模型适合「最新状态」与部分更新。选错模型会在导入阶段埋下「无法纠正的语义债」——上线前用真实 SQL 集压测 JOIN 与点查。

导入:Kafka Routine Load 做近实时;大批次用 Broker/Stream Load;Exactly-Once 靠幂等与去重模型(UNIQUE KEY / 主键模型)。容量经验:先定「日增量行数 × 行宽 × 副本」估 BE 磁盘,再按并发查询 QPS 估 FE/BE CPU;导入与查询抢同一集群时要做资源组隔离。

物化视图:看板热查询优先异步刷新 MV,避免每次扫明细;刷新延迟写入 SLO(如 P95 < 5min)。变更基表 schema 前检查 MV 依赖,防止发布后查询打回明细拖垮集群。

资源隔离:FE 元数据与 BE 计算存储分离思维;大查询限流、资源组,避免打爆在线看板。小文件/空分区定期合并治理。

与 CDC 衔接:业务库变更经 Debezium/Kafka 入仓(见 C14.12),避免应用双写。入仓作业与看板集群配额分开,防止追数作业挤占在线查询。

深度追问

追问 1:Doris 与 StarRocks 怎么分? 同源演进、生态与版本节奏不同;选型看团队存量、云厂商支持与具体版本特性(物化视图、存算分离),用压测与 SQL 兼容矩阵说话。

追问 2:为何大 JOIN 仍可能慢? 数据倾斜、广播/Shuffle 选择失误、统计信息不准;需分桶对齐、Colocate、预聚合。

追问 3:能否替代 TiDB HTAP? 不能简单替代:TiDB 强在 TP+轻量分析一体;Doris/SR 是分析面专用,TP 仍在 MySQL/PG/TiDB。

常见陷阱
易错点
  • 用数仓扛 OLTP:高并发行更新、事务语义不匹配。
  • 无分区/分桶设计就上线:全表扫与小文件/小分片问题。
  • 只对标 ClickHouse 吞吐:忽略并发点查与 JOIN 才是 Doris/SR 主场。
  • 导入链路无幂等:重复消费导致指标翻倍。
C14.12
如何设计端到端 CDC 管道?Debezium/Canal → Kafka → 湖仓/ES/缓存的一致性、顺序与回放怎么保证?专家P0
本质问题

C09.16 解决的是 Outbox + CDC 作为可靠消息投递;本题升级为 数据面 CDC:把 OLTP 变更稳定输送到数仓、搜索、缓存、向量索引等下游。核心矛盾是 至少一次投递 vs 下游幂等、单表顺序 vs 全局并发、schema 演进 vs 消费兼容。

演进脉络

应用双写:业务同时写 DB 与 MQ/ES——一致性差、漏写难追。

定时 ETL:批抽简单,实时性差、删改难捕获。

Binlog 订阅:Canal/Debezium 读日志,近实时、对业务侵入低。

流式入湖入仓:Kafka Connect / Flink CDC → Iceberg/Doris/CH;与流处理(C11.24/C11.25)和向量同步(C14.03)交汇。

第一性原理

日志是真相源:以 binlog/WAL 为变更序列;下游是物化视图。端到端不承诺跨系统分布式事务,而承诺 可重放 + 可对账 + 幂等收敛。

环节 关键设计 失败模式
捕获 GTID/LSN、快照+增量、心跳 位点丢失、DDL 中断
传输 按主键分区保序、Schema Registry 乱序、毒消息、积压
应用 幂等键、版本戳、回放窗口 重复写、旧事件覆盖新值
治理 延迟 SLA、对账、DLQ 静默不一致
关键结论

CDC 管道 = 位点可信 + 分区保序 + 下游幂等/版本 + 延迟与对账可观测。Outbox(C09.16)解决「业务事件是否发出」;本卡解决「库变更是否正确流向数据面」。

工程实践

参考链路:MySQL/PG → Debezium → Kafka(按表.主键哈希分区)→ Flink/Connect → Doris/Iceberg/ES;缓存失效走旁路主题。

顺序:同一主键进入同一分区;跨表业务顺序用聚合根 ID 或最终对账,不幻想全局总序。

Schema 演进:兼容变更(加列默认值)优先;破坏性变更用双写窗口/版本化 topic。DDL 兼容窗口:先加可空列 → 消费者升级 → 再收缩。

幂等落地:下游以 (table, pk, binlog_pos/gtid) 或业务 version 单调比较;乱序/重放丢弃旧位点。

if (event.sourcePos() <= state.lastPos(pk)) return; // 旧事件
repo.upsert(pk, event.payload(), event.sourcePos());
// ALTER TABLE orders ADD COLUMN risk_score INT NULL; -- 旧消费者可忽略

回放:保留日志与 Kafka 保留期;按位点重放前冻结下游或写修正主题;全量用快照引导 + 增量追平。

SLO:监控抓取延迟、分区 lag、DDL 失败、DLQ 速率、抽样对账差异率。

深度追问

追问 1:Exactly-Once 能做到吗? 捕获与 Kafka 常至少一次;下游用幂等表/主键 upsert/版本比较达到「效果一次」。Flink 两阶段提交可对特定 sink 加强,但不能替代业务版本语义。

追问 2:DDL 怎么处理? 连接器需可解析的 DDL 策略(忽略/演进/停服);在线改大表配合 gh-ost 等,避免锁表导致抓取中断。

追问 3:与 Outbox 并存吗? 并存:领域事件(业务语义)走 Outbox;数据面镜像(行级变更)走 CDC。勿用一种管道硬扛两种语义。

常见陷阱
易错点
  • 按表随机分区:同主键乱序,状态机被旧事件打回。
  • 无 DLQ/无对账:错误静默,数仓与线上长期漂移。
  • 用 CDC 替代领域事件:行变更难表达业务意图与聚合边界。
  • 忽略大事务/热点表:单表 binlog 洪峰打爆下游,需限流与拆分。
C14.13
慢 SQL 值班动作:从告警到限流与索引的最短路径?高级P1
本质问题

慢 SQL 值班先止血再优化:定位语句 → 评估锁与复制延迟 → 限流/杀会话 → 再谈索引。

原理映射

优化前先控制爆炸半径。索引变更按数据变更门禁(G04),禁止生产无单 CREAT INDEX。

落地排查

顺序:

  1. 慢日志/insight 取指纹。
  2. explain 与行数。
  3. 是否持锁/长事务。
  4. 限流该接口或杀会话。
  5. 加索引/改写走变更门禁。
参数与开关

表:fingerprint、avg_rt、rows_examined、锁等待、影响接口。关联 G04.02。

失败模式与回滚

失败模式:未止血先改代码;错误索引锁表;忽略复制延迟。回滚点:撤销错误索引变更或恢复旧 SQL。

慢 SQL 最短路径:告警→执行计划→是否新发布/新索引缺失→限流/杀会话→补索引或改写→回放。

禁止只调超时;要有慢查询闭环单与回归用例。

量化分级:avg_rt > 1s 且 rows_examined > 10 万为一级;锁等待 > 5s 或影响接口 > 3 个先止血。索引变更按数据变更门禁走,生产环境禁止无单执行 CREATE INDEX(MySQL 8.0 起 Online DDL 仍会短时持元数据锁)。演进:把慢查询指纹 + 回归用例沉淀为 SQL 基线,发布前 diff 新增指纹;rows_examined / rows_sent 比值 > 100 视为索引失效信号。

C14.14
数据库存储与文件系统:从页缓存到 WAL 的双向指导架构P1
本质问题

任何数据库最终都落盘到文件系统:存储引擎的页/块 → 文件系统的 inode/extent → 块设备的扇区。文件系统的特性(页缓存、顺序写、块对齐、fsync 语义、IO 调度)不是底层细节,而是反向约束数据库设计的硬边界。

原理映射
  • 双层缓存与双写。OS page cache 缓存文件页,数据库 buffer pool 缓存数据页,二者可能缓存同一页(双缓存浪费内存且语义分裂)。InnoDB 用 innodb_flush_method=O_DIRECT 绕过 page cache,但仍靠 fsync 落盘;WAL/redo/undo 用 append-only 顺序写,天然契合文件系统与 SSD 的高顺序吞吐。
  • 块对齐与写放大。数据库页(通常 16K)必须与 FS block(4K)对齐;未对齐会触发 read-modify-write,把一次写放大成多次。XFS/ext4 的 stripe/太阳对齐、大页顺序写对吞吐影响显著。
  • fd / inode 上限。数据库打开大量表与数据文件受 ulimit -n、fs.file-max、table_open_cache/innodb_open_files 约束;单实例文件数受 FS 与 inode 上限限制。
  • fsync 语义与持久化。fdatasync 只刷数据不刷元数据(更快);O_DIRECT 绕过 page cache 但仍需 fsync 才真正落盘;group commit 合并多次 fsync 降低提交延迟。durability 级别(如 sync_binlog、innodb_flush_log_at_trx_commit)本质是对「fsync 频率」的权衡。
  • 双重日志。ext4 journal(ordered/data 模式)与 InnoDB redo 是两套日志:FS journal 只保 FS 元数据一致,崩溃恢复以数据库自身 WAL 为准,理解二者关系避免重复开销与误判。
落地排查
  1. IO 瓶颈先看队列深度与 await(iostat/iowait),区分随机 vs 顺序。
  2. 确认 buffer pool 是否双缓存(O_DIRECT 是否生效)。
  3. 大表/分区评估 inode 与单文件大小上限,必要时拆分或对象存储归档。
  4. durability 调优先定 fsync 频率,再评估 group commit 收益。
  5. SSD 场景开 fstrim/discard、对齐写入,避免写放大吞噬寿命。
参数与开关

关键开关:innodb_flush_method(O_DIRECT/O_DSYNC)、innodb_flush_log_at_trx_commit、sync_binlog、table_open_cache、innodb_open_files。分布式/对象存储:WAL 归档到 S3/Ceph、计算存储分离、备份落到对象存储——文件系统的一致性/副本模型延伸到分布式层,反向约束部署(如 CephFS/HDFS 的副本与一致性语义影响主从延迟与恢复点)。

失败模式与回滚

失败模式:误以为 buffer pool 已落盘(实际还在 page cache,掉电丢数据);O_DIRECT 未开导致双缓存把内存吃满;大事务长事务让 fsync 堆积、提交 RT 飙升;单表过大撑爆 inode/文件上限。回滚点:调低 fsync 频率需先确认副本/备份能兜住 RPO;分离存储先验证网络与一致性窗口。

反向设计要点:顺序写优先(LSM-tree 比 B+树更贴文件系统顺序写特性)、页对齐、批量 fsync、借 OS 预读但更靠自身预读避免 page cache 污染、mmap 大文件会挤占 page cache 反而拖慢。