主键设计 #

一、主键概述 #

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
主键设计检查清单:

□ 分区键是否高基数?
□ 分区键是否均匀分布?
□ 分区大小是否可控?
□ 聚簇列是否满足排序需求?
□ 聚簇列是否支持范围查询?
□ 查询是否都能使用主键?
□ 是否需要反范式化?

九、总结 #

主键设计要点:

  1. 分区键:决定数据分布,选择高基数、均匀分布的列
  2. 聚簇列:决定分区内排序,支持范围查询
  3. 查询驱动:根据查询模式设计主键
  4. 分区大小:控制在100MB以内
  5. 反范式化:为不同查询创建不同表
  6. 避免热点:使用复合分区键或时间桶

下一步,让我们学习数据操作!

最后更新:2026-03-27