主键设计 #
一、主键概述 #
1.1 主键组成 #
Cassandra主键由分区键(Partition Key)和聚簇列(Clustering Column)组成。
text
主键结构:
PRIMARY KEY (分区键, 聚簇列1, 聚簇列2, ...)
示例:
PRIMARY KEY (user_id, event_time, event_id)
└──┬──┘ └────────┬────────┘
分区键 聚簇列
1.2 主键作用 #
| 组成部分 | 作用 |
|---|---|
| 分区键 | 决定数据存储在哪个节点 |
| 聚簇列 | 决定分区内的数据排序 |
1.3 主键类型 #
text
主键类型:
1. 单列主键
PRIMARY KEY (column)
└── 单列既是分区键
2. 复合主键
PRIMARY KEY (pk, ck1, ck2)
└── pk是分区键,ck1/ck2是聚簇列
3. 复合分区键
PRIMARY KEY ((pk1, pk2), ck)
└── (pk1, pk2)组合是分区键
二、分区键设计 #
2.1 分区键作用 #
text
分区键决定数据分布:
数据写入
│
▼
计算分区键哈希值
│
▼
确定Token范围
│
▼
定位数据节点
│
▼
存储数据
2.2 分区键选择原则 #
text
分区键选择原则:
1. 高基数
✓ user_id, order_id, device_id
✗ status, gender, country
2. 数据均匀分布
✓ 哈希分布均匀的值
✗ 时间序列(可能热点)
3. 查询模式匹配
✓ 经常作为WHERE条件的列
✗ 很少查询的列
4. 分区大小可控
✓ 单分区 < 100MB
✗ 无限增长的分区
2.3 分区键示例 #
sql
-- 好的设计:用户ID作为分区键
CREATE TABLE users (
user_id UUID PRIMARY KEY,
name TEXT,
email TEXT
);
-- 好的设计:复合分区键分散数据
CREATE TABLE events (
device_id TEXT,
date DATE,
hour INT,
event_time TIMESTAMP,
data TEXT,
PRIMARY KEY ((device_id, date, hour), event_time)
);
-- 不好的设计:时间作为分区键(热点)
CREATE TABLE events_bad (
event_date DATE,
event_id UUID,
data TEXT,
PRIMARY KEY (event_date, event_id)
);
-- 问题:同一天所有数据在同一分区
2.4 避免热点 #
sql
-- 问题:时间序列热点
CREATE TABLE sensor_data_bad (
sensor_id TEXT,
reading_time TIMESTAMP,
value DOUBLE,
PRIMARY KEY (sensor_id, reading_time)
);
-- 问题:单个传感器数据无限增长
-- 解决方案1:时间桶
CREATE TABLE sensor_data_bucket (
sensor_id TEXT,
date DATE,
hour INT,
reading_time TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((sensor_id, date, hour), reading_time)
);
-- 解决方案2:随机桶
CREATE TABLE sensor_data_random (
sensor_id TEXT,
bucket INT, -- 0-9随机
reading_time TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((sensor_id, bucket), reading_time)
);
三、聚簇列设计 #
3.1 聚簇列作用 #
text
聚簇列决定分区内排序:
分区 (user_id = 001)
├── event_time: 2024-01-03 10:00 │
├── event_time: 2024-01-02 15:00 │ 按聚簇列排序
├── event_time: 2024-01-01 08:00 │
└── event_time: 2023-12-31 20:00 │
3.2 聚簇列选择原则 #
text
聚簇列选择原则:
1. 排序需求
└── 根据业务需要的排序选择
2. 范围查询需求
└── 支持范围查询的列
3. 访问模式
└── 经常按顺序访问的列
4. 唯一性
└── 组合后能唯一标识行
3.3 聚簇列示例 #
sql
-- 时间序列:按时间排序
CREATE TABLE events (
user_id UUID,
event_time TIMESTAMP,
event_type TEXT,
data TEXT,
PRIMARY KEY (user_id, event_time)
) WITH CLUSTERING ORDER BY (event_time DESC);
-- 订单:按订单ID排序
CREATE TABLE orders (
user_id UUID,
order_id UUID,
order_date DATE,
amount DECIMAL,
PRIMARY KEY (user_id, order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);
-- 多级排序:日期+时间
CREATE TABLE logs (
user_id UUID,
log_date DATE,
log_time TIMESTAMP,
log_level TEXT,
message TEXT,
PRIMARY KEY (user_id, log_date, log_time)
) WITH CLUSTERING ORDER BY (log_date DESC, log_time DESC);
3.4 排序规则 #
sql
-- 默认升序
CREATE TABLE asc_example (
pk INT,
ck INT,
data TEXT,
PRIMARY KEY (pk, ck)
); -- ck升序
-- 指定降序
CREATE TABLE desc_example (
pk INT,
ck INT,
data TEXT,
PRIMARY KEY (pk, ck)
) WITH CLUSTERING ORDER BY (ck DESC); -- ck降序
-- 多列排序
CREATE TABLE multi_sort (
pk INT,
ck1 INT,
ck2 INT,
data TEXT,
PRIMARY KEY (pk, ck1, ck2)
) WITH CLUSTERING ORDER BY (ck1 DESC, ck2 ASC);
四、主键设计模式 #
4.1 模式一:单分区键 #
sql
-- 适用场景:主键查询
CREATE TABLE users (
user_id UUID PRIMARY KEY,
name TEXT,
email TEXT,
created_at TIMESTAMP
);
-- 查询方式
SELECT * FROM users WHERE user_id = ?;
4.2 模式二:分区键+聚簇列 #
sql
-- 适用场景:一对多关系、时间序列
CREATE TABLE user_orders (
user_id UUID,
order_id UUID,
order_date DATE,
amount DECIMAL,
PRIMARY KEY (user_id, order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);
-- 查询方式
SELECT * FROM user_orders WHERE user_id = ?;
SELECT * FROM user_orders WHERE user_id = ? AND order_id = ?;
SELECT * FROM user_orders
WHERE user_id = ? AND order_id > ? AND order_id < ?;
4.3 模式三:复合分区键 #
sql
-- 适用场景:分散数据、避免热点
CREATE TABLE time_series (
device_id TEXT,
date DATE,
hour INT,
reading_time TIMESTAMP,
value DOUBLE,
PRIMARY KEY ((device_id, date, hour), reading_time)
) WITH CLUSTERING ORDER BY (reading_time ASC);
-- 查询方式
SELECT * FROM time_series
WHERE device_id = ? AND date = ? AND hour = ?;
4.4 模式四:多租户 #
sql
-- 适用场景:多租户应用
CREATE TABLE tenant_data (
tenant_id UUID,
user_id UUID,
created_at TIMESTAMP,
data TEXT,
PRIMARY KEY ((tenant_id, user_id), created_at)
) WITH CLUSTERING ORDER BY (created_at DESC);
-- 查询方式
SELECT * FROM tenant_data
WHERE tenant_id = ? AND user_id = ?;
五、查询与主键 #
5.1 查询规则 #
text
查询规则:
1. 必须包含分区键
✓ WHERE user_id = ?
✗ WHERE name = ?
2. 聚簇列按顺序使用
✓ WHERE user_id = ? AND order_id = ?
✗ WHERE user_id = ? AND order_date = ?(跳过了order_id)
3. 范围查询限制
✓ WHERE user_id = ? AND order_id > ?
✗ WHERE user_id = ? AND order_id > ? AND order_id < ?
(最后一个范围查询后不能有其他条件)
5.2 查询示例 #
sql
-- 表结构
CREATE TABLE orders (
user_id UUID,
order_date DATE,
order_id UUID,
amount DECIMAL,
PRIMARY KEY (user_id, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC, order_id ASC);
-- 有效查询
SELECT * FROM orders WHERE user_id = ?;
SELECT * FROM orders WHERE user_id = ? AND order_date = ?;
SELECT * FROM orders WHERE user_id = ? AND order_date = ? AND order_id = ?;
SELECT * FROM orders WHERE user_id = ? AND order_date > ?;
SELECT * FROM orders WHERE user_id = ? AND order_date = ? AND order_id > ?;
-- 无效查询
SELECT * FROM orders WHERE order_date = ?; -- 缺少分区键
SELECT * FROM orders WHERE user_id = ? AND order_id = ?; -- 跳过order_date
六、分区大小管理 #
6.1 分区大小限制 #
text
分区大小建议:
推荐:< 100MB
警告:> 100MB
最大:2GB
大分区问题:
├── 读取性能下降
├── 修复时间增加
├── 压缩效率降低
└── 内存压力增大
6.2 控制分区大小 #
sql
-- 问题:无限增长的分区
CREATE TABLE user_posts_bad (
user_id UUID,
post_id TIMEUUID,
content TEXT,
PRIMARY KEY (user_id, post_id)
);
-- 解决方案1:时间桶
CREATE TABLE user_posts_time (
user_id UUID,
post_month TEXT, -- YYYY-MM
post_id TIMEUUID,
content TEXT,
PRIMARY KEY ((user_id, post_month), post_id)
) WITH CLUSTERING ORDER BY (post_id DESC);
-- 解决方案2:数量桶
CREATE TABLE user_posts_bucket (
user_id UUID,
bucket INT, -- 0-99
post_id TIMEUUID,
content TEXT,
PRIMARY KEY ((user_id, bucket), post_id)
) WITH CLUSTERING ORDER BY (post_id DESC);
6.3 监控分区大小 #
bash
# 查看表统计
nodetool tablestats my_app.my_table
# 查看分区大小
SELECT * FROM system.size_estimates
WHERE keyspace_name = 'my_app' AND table_name = 'my_table';
七、反范式化设计 #
7.1 反范式化原则 #
text
Cassandra设计原则:
关系型数据库
├── 范式化设计
├── 减少数据冗余
└── 使用JOIN查询
Cassandra
├── 反范式化设计
├── 数据冗余换性能
└── 无JOIN,按查询设计表
7.2 反范式化示例 #
sql
-- 查询需求1:按用户ID查询订单
CREATE TABLE orders_by_user (
user_id UUID,
order_id UUID,
order_date DATE,
amount DECIMAL,
status TEXT,
PRIMARY KEY (user_id, order_id)
) WITH CLUSTERING ORDER BY (order_id DESC);
-- 查询需求2:按订单状态查询
CREATE TABLE orders_by_status (
status TEXT,
order_date DATE,
order_id UUID,
user_id UUID,
amount DECIMAL,
PRIMARY KEY (status, order_date, order_id)
) WITH CLUSTERING ORDER BY (order_date DESC, order_id DESC);
-- 数据写入时同时写入两个表
BEGIN BATCH
INSERT INTO orders_by_user (user_id, order_id, order_date, amount, status)
VALUES (?, ?, ?, ?, ?);
INSERT INTO orders_by_status (status, order_date, order_id, user_id, amount)
VALUES (?, ?, ?, ?, ?);
APPLY BATCH;
八、最佳实践 #
8.1 设计流程 #
text
主键设计流程:
1. 分析查询需求
└── 确定所有查询模式
2. 选择分区键
└── 高基数、均匀分布
3. 设计聚簇列
└── 排序和范围查询需求
4. 评估分区大小
└── 控制在100MB以内
5. 反范式化
└── 为不同查询创建不同表
8.2 设计检查清单 #
text
主键设计检查清单:
□ 分区键是否高基数?
□ 分区键是否均匀分布?
□ 分区大小是否可控?
□ 聚簇列是否满足排序需求?
□ 聚簇列是否支持范围查询?
□ 查询是否都能使用主键?
□ 是否需要反范式化?
九、总结 #
主键设计要点:
- 分区键:决定数据分布,选择高基数、均匀分布的列
- 聚簇列:决定分区内排序,支持范围查询
- 查询驱动:根据查询模式设计主键
- 分区大小:控制在100MB以内
- 反范式化:为不同查询创建不同表
- 避免热点:使用复合分区键或时间桶
下一步,让我们学习数据操作!
最后更新:2026-03-27