本节摘要:Cassandra 铁律:先列出所有查询,再建表——通常一查询一表,必要时物化视图或双写。本节用订单/Feed/IoT 三案例对照 Mongo 聚合管道与 PostgreSQL 范式化。
阅读完本节,你应当能够:
关系库 ER 图先定实体再查——JOIN 帮你在运行时组合。Cassandra 没有 JOIN:运行时组合 = 全环扫描。Netflix 播放历史按用户查、Apple 消息按对话查——表结构直接 mirror 访问路径。Mongo 可在应用层 $lookup,但大流量下同样不推荐;HBase 多表 Join 走 MapReduce 离线。
四步建模法(SOURCE 3.3):
案例:社交 Feed
| 查询 | 表设计 |
|---|---|
| 用户时间线 | PRIMARY KEY (user_id, post_time) |
| 帖子 by id | PRIMARY KEY (post_id) |
| 粉丝读作者 | 同上 user_id 表,写 fan-out 或 pull 模型 |
Fan-out on write:发帖时写每个粉丝 timeline——写放大,读快。Pull:读时聚合——读放大。Cassandra 偏 fan-out 因写吞吐便宜。
与 MongoDB:
| 维度 | Cassandra | MongoDB |
|---|---|---|
| 多维查询 | 多表/SAI | 单文档嵌套 + 索引 |
| 事务 | LWT 单行/小范围 | 4.x 多文档 TX |
| 模式变更 | ALTER 阻塞 | schema-less |
与 HBase:HBase 常一张宽表多 Column Family;Cassandra 倾向多表窄模型 + CQL 类型安全。
物化视图:自动维护派生表,底层 Paxos;3.x 后谨慎用于高写表——写放大 ×2。
双写一致性:应用写主表 + 派生表,失败用 Kafka 补偿;别指望跨 partition ACID。
反模式清单:
ALLOW FILTERING 上生产⚠️ 常见坑:从 PostgreSQL 迁移只搬表结构不搬查询清单——上线第一周全员
ALLOW FILTERING。
💡 关键直觉:表数量变多是 feature 不是 debt——在 Cassandra 里,少表往往意味着查询被牺牲。
第4章讨论表结构上线后的部署、repair 与备份——建模决策的运维后果。
以「设备温度监控」为例,完整走一遍查询驱动建模:
业务查询清单: Q1. 某设备最近 24 小时温度曲线(按时间倒序) Q2. 某设备某时间点的原始读数 Q3. 全厂今日超过阈值的设备列表(低频,可接受秒级延迟)
-- Q1/Q2 用同一张表:设备分区 + 时间聚类 CREATE TABLE temp_by_device ( device_id text, ts timestamp, temp double, PRIMARY KEY (device_id, ts) ) WITH CLUSTERING ORDER BY (ts DESC) AND compaction = {'class': 'TimeWindowCompactionStrategy'}; -- Q3 是低频全表过滤,不硬建模,用应用层定时扫描 + 缓存告警
这张表体现了三个决策:分区键选设备(Q1 等值条件)、聚类键选时间(Q1 排序)、Compaction 选 TWCS(时序 TTL 语义)。对比 MongoDB 用 $lt/$gt 范围查询同场景——文档模型查询更灵活,但无法把「设备+时间」物理聚簇,海量扫描时性能不可控。
CREATE TABLE post_by_id ( post_id uuid PRIMARY KEY, author_id uuid, content text, ts timestamp ); CREATE TABLE feed_by_user ( user_id uuid, post_id uuid, author uuid, ts timestamp, PRIMARY KEY (user_id, ts, post_id) ) WITH CLUSTERING ORDER BY (ts DESC);
# 发帖时双写:post_by_id 保证"按 id 取全文",feed_by_user 保证"首页时间线" def publish(post): session.execute("INSERT INTO post_by_id ...", (post.id, ...)) for follower in followers(post.author): session.execute("INSERT INTO feed_by_user ...", (follower, post.ts, post.id, ...)) # 失败补偿:写失败的任务进队列,由 worker 重试补齐
写放大明显(一个粉丝一条写),但读路径全是单分区点查。Cassandra 的定价逻辑是「写便宜、读贵」,所以 fan-out on write 是社区默认推荐;反过来如果粉丝列表巨大,则改 pull 模型读时聚合,用读放大换写放大。
| 反模式 | 症状 | 修正 |
|---|---|---|
| 全表 ALLOW FILTERING | 查询扫全环,P99 恶化 | 把过滤字段挪进 PRIMARY KEY |
| 高基数列建二级索引 | 索引写放大、扫描低效 | 建反查表 |
| 单分区存百万行 | Compaction/repair 退化 | 时间桶 / 加盐 |
| 应用层 GROUP BY | 读全量在内存聚合 | CDC 到 OLAP |
| BATCH 跨分区 | 协调者串行处理 | 拆单条 / 同分区批量 |
对照自查一遍,你的建模是否踩中了至少一条?Cassandra 的建模评审会(schema review)就是拿着这份清单逐表过——这也是 4.x 之后大型团队的标准流程。
-- 物化视图:由主表自动派生,无需应用层双写 CREATE MATERIALIZED VIEW temp_by_device_daily AS SELECT device_id, ts, temp FROM temp_by_device WHERE device_id IS NOT NULL AND ts IS NOT NULL PRIMARY KEY ((device_id, toDate(ts)), ts);
| 维度 | Materialized View | 应用层双表 |
|---|---|---|
| 一致性 | Paxos 保障主表/视图原子 | 靠重试/队列补偿 |
| 写放大 | 每次写多写一份 | 显式控制 |
| 调试成本 | 视图逻辑隐蔽 | 代码可读 |
| 高写表 | 不推荐(Paxos 开销) | 推荐 |
物化视图的维护走 Paxos,高写入表上每行写都会翻倍并引入串行化成本;社区共识是 3.x 后谨慎用于高频写表,优先应用层双表。选择时先量写入吞吐:每秒几万的表不碰 MV,每秒几百的表用 MV 省代码。
-- 新增列:ALTER 支持,但无默认值回填 ALTER TABLE temp_by_device ADD humidity double; -- 变更聚类键顺序:不支持,只能建新表 + 迁移
Cassandra 的 schema 变更能力有限:加列可以,改主键、改聚类排序都必须建新表、双写迁移、再切流。这反向强化了「先列查询、后建表」纪律——建模期的一次偷懒,上线后要用一次迁移偿还。MongoDB 的 schema-less 看似无痛,实则把结构约束推给应用层,查询错误在运行时才暴露;Cassandra 把约束前置到建表,代价是演进必须规划。