"你们组的新项目数据库已经卡了三回查询了!"当我接到CTO电话时,手里刚泡好的咖啡瞬间不香了。看着慢查询日志里那些超过10秒的请求,我知道是时候解决困扰我们已久的分区策略问题了...

一、什么是分区键的关键抉择?

在PolarDB的场景中,分区键就像快递分拣站的自动分拣机。假设每天要处理百万件快递,如果我们按照省份分区(河北省件放A区,广东省件放B区),但当遇到"查询北京市所有顺丰快递"的需求时,分拣机却能直接锁定北京分区快速处理——这就是分区剪枝的魔力。

但现实往往更复杂,比如当选择用户ID作为分区键时:

-- 示例使用PolarDB MySQL版 8.0.2
CREATE TABLE user_orders (
    order_id BIGINT AUTO_INCREMENT,
    user_id INT,
    order_date DATE,
    amount DECIMAL(10,2),
    PRIMARY KEY (order_id, user_id)  -- 必须包含分区键
) PARTITION BY HASH(user_id)
PARTITIONS 10;

此时执行用户订单查询时:

-- 好的情况
EXPLAIN SELECT * FROM user_orders 
WHERE user_id = 10086; -- 精确命中分区5

-- 坏的情况
EXPLAIN SELECT * FROM user_orders
WHERE order_date BETWEEN '2023-01-01' AND '2023-12-31'; -- 扫描全部分区

这就像把所有水果按颜色分类后,突然要按品种检索,就会导致遍历所有筐子。所以选择分区键的本质,是在查询模式的命中率与数据分布的合理性之间找平衡点。

二、典型业务场景的决策过程

2.1 时序数据场景:日志分析系统

每天产生10GB的日志文件,常见查询是时间范围检索:

CREATE TABLE app_logs (
    log_time DATETIME(6),
    app_name VARCHAR(50),
    log_content TEXT,
    INDEX idx_time (log_time)
) PARTITION BY RANGE COLUMNS(log_time) (
    PARTITION p202301 VALUES LESS THAN ('2023-02-01'),
    PARTITION p202302 VALUES LESS THAN ('2023-03-01'),
    PARTITION p202303 VALUES LESS THAN ('2023-04-01')
);

-- 每月自动增加分区
ALTER TABLE app_logs ADD PARTITION (
    PARTITION p202310 VALUES LESS THAN ('2023-11-01')
);

当查询最近三天日志时,优化器会智能排除三个月前的分区。但隐患在于,如果按app_name分区:

SELECT * FROM app_logs 
WHERE log_time >= NOW() - INTERVAL 3 DAY 
  AND app_name = 'payment_service'; -- 全分区扫描

这个取舍告诉我们:在时间序列场景下,优先保证高频查询的剪枝能力,其他条件通过二级索引弥补。

2.2 电商订单系统

当面对订单状态查询的高并发时:

CREATE TABLE orders (
    order_id VARCHAR(32),
    user_id BIGINT,
    status ENUM('pending','paid','shipped'),
    create_time TIMESTAMP,
    INDEX idx_status (status)
) PARTITION BY LIST COLUMNS(status) (
    PARTITION p_pending VALUES IN ('pending'),
    PARTITION p_paid VALUES IN ('paid'),
    PARTITION p_shipped VALUES IN ('shipped')
);

-- 但状态分布严重不均:
SELECT status, COUNT(*) 
FROM orders 
GROUP BY status;
/*
+---------+----------+
| status  | COUNT(*) |
+---------+----------+
| pending |   12,345 |
| paid    |  987,654 |
| shipped |    3,456 |
+---------+----------+

此时订单状态分区就会导致分区数据严重倾斜,paid分区的压力是其他分区的百倍。正确的做法应该是组合分区:

CREATE TABLE orders (
    ...
) PARTITION BY HASH(user_id)
SUBPARTITION BY LIST COLUMNS(status) (
    PARTITION p0 VALUES IN ('pending'),
    PARTITION p1 VALUES IN ('paid'),
    PARTITION p2 VALUES IN ('shipped')
) PARTITIONS 4;

2.3 社交平台用户画像

处理包含数十个标签字段的用户数据表时:

CREATE TABLE user_tags (
    user_id BIGINT,
    gender ENUM('M','F'),
    age_group TINYINT,
    city_code INT,
    ...
) PARTITION BY KEY()
PARTITIONS 16;  -- 全字段hash分区

看似中立的哈希分区却存在隐患。当执行基于城市的查询时:

SELECT * FROM user_tags
WHERE city_code = 1001;  -- 每个分区都有该城市的数据

解决这个矛盾需要使用双重策略:

CREATE TABLE user_tags (
    ...
) PARTITION BY LINEAR HASH(city_code)
PARTITIONS 64;

通过放大分区数,让每个城市对应足够多的分区,同时用HASH算法保证均匀分布。

三、技术实施的黄金法则

3.1 选择分区键的3个维度

  • 数据离散度:测试字段基数是否适合分区数量
SELECT COUNT(DISTINCT user_id) AS uid_count,
       COUNT(DISTINCT order_date) AS date_count
FROM orders;
  • 查询模式匹配度:抓取Slow Query Log分析WHERE条件
  • 业务扩展性:未来三年数据增长预估

3.2 检验数据分布的4种手段

  1. 查看分区数据量:
SELECT PARTITION_NAME, TABLE_ROWS 
FROM INFORMATION_SCHEMA.PARTITIONS 
WHERE TABLE_NAME = 'user_orders';
  1. 检查查询计划:
EXPLAIN PARTITIONS 
SELECT * FROM sales 
WHERE sale_date > '2023-09-01';
  1. 统计热点扫描:
SELECT partition_id, 
       SUM(rows_read) AS total_read
FROM polar_stat_activity 
GROUP BY partition_id
ORDER BY total_read DESC;
  1. 使用散列分布检测工具:
SELECT ABS(MAX(cnt) - MIN(cnt)) * 100.0 / AVG(cnt) AS imbalance_percent
FROM (
    SELECT PARTITION_NAME, TABLE_ROWS AS cnt
    FROM INFORMATION_SCHEMA.PARTITIONS
    WHERE TABLE_NAME = 'user_logs'
) t;

3.3 组合分区的实战案例

针对订单系统的复杂查询需求:

CREATE TABLE multi_part_orders (
    order_id VARCHAR(32),
    user_id BIGINT,
    order_date DATE,
    amount DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(order_date))  -- 第一级按年分区
SUBPARTITION BY HASH(user_id % 100)    -- 第二级按用户尾号
SUBPARTITIONS 10 (
    PARTITION p2020 VALUES LESS THAN (2021),
    PARTITION p2021 VALUES LESS THAN (2022),
    PARTITION p2022 VALUES LESS THAN (2023)
);

该设计的优势在于:

  • 按年份快速归档历史数据
  • 用户维度查询可锁定特定子分区
  • 数值尾号哈希避免用户聚集

四、避坑指南:血的教训总结

4.1 动态分区的陷阱

某个广告系统使用自增ID作为分区键:

CREATE TABLE click_logs (
    id BIGINT AUTO_INCREMENT,
    ad_id INT,
    click_time DATETIME,
    INDEX (ad_id)
) PARTITION BY RANGE (id) (
    PARTITION p0 VALUES LESS THAN (1000000),
    PARTITION p1 VALUES LESS THAN (2000000),
    ... 
);

三个月后的查询性能急剧下降。原因是广告主常查询某个ad_id的点击数据:

SELECT * FROM click_logs 
WHERE ad_id = 123 
  AND click_time BETWEEN ...;

解决方案是重新设计为组合分区:

ALTER TABLE click_logs 
PARTITION BY HASH(ad_id)
PARTITIONS 32;

4.2 数值溢出的幽灵事件

某金融系统按交易额区间分区:

CREATE TABLE fund_trans (
    trans_id BIGINT,
    amount DECIMAL(16,2),
    ...
) PARTITION BY RANGE (amount) (
    PARTITION p0 VALUES LESS THAN (1000),
    PARTITION p1 VALUES LESS THAN (10000),
    PARTITION p_max VALUES LESS THAN MAXVALUE
);

当出现单笔超过1亿的转账时,所有超大金额交易都集中在p_max分区,导致该分区爆炸式增长。改进方案是定期动态调整分区阈值。

五、工具链的巧妙使用

5.1 自动化监控脚本示例

#!/bin/bash
# 检查分区均衡度
mysql -uadmin -p$PASSWORD -N <<< "SELECT 
  (MAX(TABLE_ROWS)-MIN(TABLE_ROWS))*100/AVG(TABLE_ROWS) AS imbalance 
FROM INFORMATION_SCHEMA.PARTITIONS 
WHERE TABLE_NAME='$1'" | awk '{if($1 > 30) exit 1}'

5.2 自动重建倾斜分区

-- 使用存储过程动态调整
DELIMITER $$
CREATE PROCEDURE rebalance_partitions()
BEGIN
  DECLARE done INT DEFAULT FALSE;
  DECLARE p_name VARCHAR(50);
  DECLARE cur CURSOR FOR 
    SELECT PARTITION_NAME 
    FROM INFORMATION_SCHEMA.PARTITIONS 
    WHERE TABLE_NAME = 'hot_table' 
    ORDER BY TABLE_ROWS DESC
    LIMIT 3;
  
  OPEN cur;
  part_loop: LOOP
    FETCH cur INTO p_name;
    IF done THEN LEAVE part_loop; END IF;
    
    -- 拆分最大分区
    SET @split_value = (SELECT MAX(user_id) FROM hot_table PARTITION (p_name));
    SET @sql = CONCAT('ALTER TABLE hot_table REORGANIZE PARTITION ', p_name,
                      ' INTO (PARTITION ', p_name,'_1 VALUES LESS THAN (', @split_value/2, '),',
                      ' PARTITION ', p_name,'_2 VALUES LESS THAN MAXVALUE)');
    PREPARE stmt FROM @sql;
    EXECUTE stmt;
  END LOOP;
END$$
DELIMITER ;

六、总结与最佳实践

经过多个项目的实战打磨,我们总结出黄金法则:

  1. 双80原则:80%的查询能够命中分区键,分区间数据量差异不超过20%
  2. 动态调整机制:通过定时任务监控并自动优化分区策略
  3. 组合拳策略:采用HASH+RANGE等组合分区应对复杂场景
  4. 预防性设计:预留足够的分区容量和重组方案

在未来的数据库设计中,随着机器学习的发展,我们有理由相信会出现更智能的自动分区调优系统。但在此之前,理解业务特征、掌握数据规律,仍然是我们驾驭PolarDB这匹性能怪兽的关键所在。