主键设计 #
一、主键概述 #
1.1 主键组成 #
主键由分区键(Partition Key)和聚簇列(Clustering Columns)组成。
text
主键结构:
┌─────────────────────────────────────────────────────────┐
│ │
│ PRIMARY KEY (分区键, 聚簇列1, 聚簇列2, ...) │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Partition Key │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 决定数据分布到哪个节点 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────┐ │
│ │ Clustering Columns │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ 决定分区内数据的排序 │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────┘
1.2 主键作用 #
| 组成 | 作用 |
|---|---|
| 分区键 | 决定数据分布到哪个节点 |
| 聚簇列 | 决定分区内数据排序 |
| 整体 | 唯一标识一行数据 |
二、分区键设计 #
2.1 简单分区键 #
sql
-- 单列分区键
CREATE TABLE users (
user_id UUID PRIMARY KEY,
name TEXT,
email TEXT
);
-- 数据分布
-- user_id -> Token -> Node
2.2 复合分区键 #
sql
-- 多列组成分区键
CREATE TABLE events_by_date (
event_type TEXT,
event_date DATE,
event_id TIMEUUID,
data TEXT,
PRIMARY KEY ((event_type, event_date), event_id)
);
-- 分区键: (event_type, event_date)
-- 同类型同日期的数据在同一分区
2.3 分区键选择原则 #
text
分区键选择原则:
┌─────────────────────────────────────────────────────────┐
│ │
│ 1. 高基数 │
│ ├── 值多样性高 │
│ ├── 数据均匀分布 │
│ └── 避免热点分区 │
│ │
│ 2. 匹配查询模式 │
│ ├── WHERE条件包含分区键 │
│ ├── 支持主要查询 │
│ └── 避免ALLOW FILTERING │
│ │
│ 3. 控制分区大小 │
│ ├── 建议<100MB │
│ ├── 避免>1GB │
│ └── 考虑数据增长 │
│ │
└─────────────────────────────────────────────────────────┘
2.4 分区键示例 #
sql
-- 好的设计:高基数
CREATE TABLE orders (
order_id UUID PRIMARY KEY,
user_id UUID,
amount DECIMAL
);
-- 好的设计:复合分区键
CREATE TABLE user_activity (
user_id UUID,
activity_date DATE,
activity_id TIMEUUID,
activity_type TEXT,
PRIMARY KEY ((user_id, activity_date), activity_id)
);
-- 不好的设计:低基数
CREATE TABLE bad_design (
status TEXT PRIMARY KEY, -- 只有几个值
user_id UUID
);
-- 问题:数据严重倾斜
三、聚簇列设计 #
3.1 聚簇列作用 #
sql
-- 聚簇列决定分区内排序
CREATE TABLE time_series (
device_id TEXT,
timestamp TIMESTAMP,
value DOUBLE,
PRIMARY KEY (device_id, timestamp)
) WITH CLUSTERING ORDER BY (timestamp DESC);
-- 同一device_id的数据按timestamp排序
-- 支持范围查询
SELECT * FROM time_series
WHERE device_id = 'device1'
AND timestamp >= '2024-01-01'
AND timestamp < '2024-02-01';
3.2 多列聚簇 #
sql
-- 多列聚簇排序
CREATE TABLE events (
device_id TEXT,
event_date DATE,
event_time TIMESTAMP,
event_type TEXT,
data TEXT,
PRIMARY KEY (device_id, event_date, event_time)
) WITH CLUSTERING ORDER BY (event_date ASC, event_time DESC);
-- 查询示例
SELECT * FROM events
WHERE device_id = 'device1'
AND event_date = '2024-01-15'
AND event_time >= '10:00:00';
3.3 聚簇列选择原则 #
text
聚簇列选择原则:
├── 支持范围查询
├── 有序性要求
├── 控制分区大小
└── 考虑查询顺序
四、主键设计模式 #
4.1 时间序列模式 #
sql
-- 按时间存储数据
CREATE TABLE sensor_readings (
device_id TEXT,
reading_time TIMESTAMP,
temperature DOUBLE,
humidity DOUBLE,
PRIMARY KEY (device_id, reading_time)
) WITH CLUSTERING ORDER BY (reading_time DESC);
-- 查询最新数据
SELECT * FROM sensor_readings
WHERE device_id = 'sensor1'
LIMIT 100;
-- 查询时间范围
SELECT * FROM sensor_readings
WHERE device_id = 'sensor1'
AND reading_time >= '2024-01-01'
AND reading_time < '2024-02-01';
4.2 分桶模式 #
sql
-- 使用分桶控制分区大小
CREATE TABLE user_events_bucketed (
user_id UUID,
bucket INT, -- 0-99
event_id TIMEUUID,
event_type TEXT,
event_data TEXT,
PRIMARY KEY ((user_id, bucket), event_id)
);
-- 插入时随机选择桶
INSERT INTO user_events_bucketed (user_id, bucket, event_id, event_type)
VALUES (uuid(), floor(random() * 100), now(), 'click');
-- 查询需要扫描所有桶
SELECT * FROM user_events_bucketed
WHERE user_id = ? AND bucket IN (0,1,2,...,99);
4.3 时间窗口模式 #
sql
-- 按时间窗口分区
CREATE TABLE logs_by_hour (
app_name TEXT,
log_hour TIMESTAMP, -- 精确到小时
log_id TIMEUUID,
level TEXT,
message TEXT,
PRIMARY KEY ((app_name, log_hour), log_id)
) WITH CLUSTERING ORDER BY (log_id DESC)
AND default_time_to_live = 86400; -- 1天TTL
-- 查询特定小时的日志
SELECT * FROM logs_by_hour
WHERE app_name = 'myapp'
AND log_hour = '2024-01-15 10:00:00';
4.4 预计算聚合模式 #
sql
-- 存储预计算结果
CREATE TABLE daily_stats (
metric_name TEXT,
stat_date DATE,
hour INT,
value COUNTER,
PRIMARY KEY ((metric_name, stat_date), hour)
);
-- 更新计数
UPDATE daily_stats
SET value = value + 1
WHERE metric_name = 'page_views'
AND stat_date = '2024-01-15'
AND hour = 10;
-- 查询统计
SELECT * FROM daily_stats
WHERE metric_name = 'page_views'
AND stat_date = '2024-01-15';
五、查询驱动设计 #
5.1 设计流程 #
text
查询驱动设计流程:
┌─────────────────────────────────────────────────────────┐
│ │
│ 1. 分析查询需求 │
│ ├── 需要哪些查询? │
│ ├── 查询条件是什么? │
│ └── 排序要求是什么? │
│ │
│ 2. 设计主键 │
│ ├── 分区键 = 查询的等值条件 │
│ ├── 聚簇列 = 查询的范围条件 │
│ └── 排序 = CLUSTERING ORDER BY │
│ │
│ 3. 验证设计 │
│ ├── 是否支持所有查询? │
│ ├── 是否避免了ALLOW FILTERING? │
│ └── 分区大小是否合理? │
│ │
└─────────────────────────────────────────────────────────┘
5.2 设计示例 #
text
需求:查询用户的订单
├── 查询1:按用户ID查询订单
├── 查询2:按用户ID和日期范围查询
└── 查询3:按用户ID查询最近订单
sql
-- 设计表
CREATE TABLE user_orders (
user_id UUID,
order_date DATE,
order_id TIMEUUID,
amount DECIMAL,
status TEXT,
PRIMARY KEY (user_id, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC, order_id DESC);
-- 查询1:按用户ID查询
SELECT * FROM user_orders WHERE user_id = ?;
-- 查询2:按用户ID和日期范围
SELECT * FROM user_orders
WHERE user_id = ?
AND order_date >= '2024-01-01'
AND order_date < '2024-02-01';
-- 查询3:查询最近订单
SELECT * FROM user_orders
WHERE user_id = ?
LIMIT 10;
5.3 多查询支持 #
sql
-- 一个表无法支持所有查询时,创建多个表
-- 表1:按用户查询订单
CREATE TABLE orders_by_user (
user_id UUID,
order_id TIMEUUID,
order_date DATE,
amount DECIMAL,
PRIMARY KEY (user_id, order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);
-- 表2:按日期查询订单
CREATE TABLE orders_by_date (
order_date DATE,
order_id TIMEUUID,
user_id UUID,
amount DECIMAL,
PRIMARY KEY (order_date, order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);
-- 同步写入两个表
BEGIN BATCH
INSERT INTO orders_by_user (user_id, order_id, order_date, amount)
VALUES (?, now(), ?, ?);
INSERT INTO orders_by_date (order_date, order_id, user_id, amount)
VALUES (?, now(), ?, ?);
APPLY BATCH;
六、热点分区避免 #
6.1 热点原因 #
text
热点分区原因:
├── 低基数分区键
├── 时间序列数据(最新数据)
├── 热门实体(热门用户/商品)
└── 顺序写入
6.2 解决方案 #
sql
-- 方案1:添加随机前缀
CREATE TABLE hot_data_solved (
prefix INT, -- 0-9
original_key TEXT,
data TEXT,
PRIMARY KEY ((prefix, original_key))
);
-- 方案2:使用分桶
CREATE TABLE events_bucketed (
entity_id TEXT,
bucket INT, -- 时间桶或随机桶
event_id TIMEUUID,
data TEXT,
PRIMARY KEY ((entity_id, bucket), event_id)
);
-- 方案3:时间窗口分区
CREATE TABLE time_windowed (
entity_id TEXT,
time_window TIMESTAMP, -- 小时/天级别
event_id TIMEUUID,
data TEXT,
PRIMARY KEY ((entity_id, time_window), event_id)
);
七、分区大小管理 #
7.1 分区大小影响 #
| 大小 | 影响 |
|---|---|
| < 100MB | 正常 |
| 100MB - 1GB | 需要监控 |
| > 1GB | 需要优化 |
| > 10GB | 严重问题 |
7.2 分区大小控制 #
sql
-- 使用时间维度控制
CREATE TABLE controlled_partitions (
entity_id TEXT,
date DATE, -- 按日期分区
event_id TIMEUUID,
data TEXT,
PRIMARY KEY ((entity_id, date), event_id)
);
-- 使用分桶控制
CREATE TABLE bucketed_data (
entity_id TEXT,
bucket INT, -- 固定大小桶
id UUID,
data TEXT,
PRIMARY KEY ((entity_id, bucket), id)
);
八、最佳实践 #
8.1 设计检查清单 #
text
主键设计检查清单:
├── [ ] 分区键是否高基数?
├── [ ] 是否匹配查询模式?
├── [ ] 是否避免了热点?
├── [ ] 分区大小是否合理?
├── [ ] 聚簇排序是否正确?
├── [ ] 是否支持范围查询?
├── [ ] 是否避免了ALLOW FILTERING?
└── [ ] 是否考虑了数据增长?
8.2 设计决策树 #
text
主键设计决策:
┌─────────────────────────────────────────────────────────┐
│ │
│ 1. 查询条件是什么? │
│ ├── 等值条件 → 分区键 │
│ └── 范围条件 → 聚簇列 │
│ │
│ 2. 分区键基数是否足够? │
│ ├── 是 → 使用 │
│ └── 否 → 添加更多列或使用分桶 │
│ │
│ 3. 分区大小是否合理? │
│ ├── 是 → 完成 │
│ └── 否 → 添加时间维度或分桶 │
│ │
└─────────────────────────────────────────────────────────┘
九、总结 #
主键设计要点:
| 要点 | 说明 |
|---|---|
| 分区键 | 高基数,匹配查询 |
| 聚簇列 | 支持范围查询 |
| 排序 | CLUSTERING ORDER BY |
| 分区大小 | 控制<100MB |
最佳实践:
- 基于查询模式设计主键
- 选择高基数分区键
- 控制分区大小
- 避免热点分区
- 一个查询需求一个表
下一步,让我们学习数据操作!
最后更新:2026-03-27