🏺 第11章 表分区与索引分区
📄 一句话概括

分区策略只有两个问题:按什么列切、切多细。 两个都能算:按查询条件定列,按「单分区一千万到一亿行」定粒度。 还有一条硬约束:谓词必须能用上分区键。 把分区键包进函数,再好的分区也白切。

📝 本章和前面几章的关系

11.2 讲五种切法。这一篇讲怎么选、选完怎么验。 选错了不是「慢一点」,是「分区白建、还多一层开销」。 这一篇的核心是三个算式:单分区行数、单次查询扫多少块、分区数上限。 都是除法和乘法,但算完就知道该不该切、切几刀。

一、按什么切:看查询,不看数据

第一条判据永远是「最常用的查询条件是什么」。 总按月份查 → 按月份切;总按客户号查 → 按客户号切。 「数据长什么样」不重要,「怎么查」才重要。 三条常见组合: ① 时间范围 + 定期删旧数据 → 范围分区(配 INTERVAL);② 单点查询 + 无自然区间 → 散列分区; ③ 状态/渠道分类 + 分区级归档 → 列表分区。 第二种和第三种容易混: 散列是「我不知道值会是什么,但要打散」;列表是「我明确知道有哪几个值」。 值能枚举就用列表,不能枚举就用散列。

二、切多细:单分区一千万到一亿行

经验值一句话:单分区控制在一千万到一亿行之间。 五亿行的表按 12 个月切,单分区约 4167 万行——在这个区间里,合适。 切太细的代价: 分区数爆炸。按天切一年就是 365 个,三年 1095 个—— Oracle 单表分区上限是 1024K 个,但几百个之后,解析 SQL、生成计划都要遍历这些元数据, 开销肉眼可见。 切太粗的代价: 等于没切。单分区几亿行,扫一个分区照样慢,删一个分区照样要处理几亿行。 所以粒度是「按数据量反算」的: 先估算每年/每月的增量,再算「切到哪个粒度,单分区刚好在千万到亿这个区间」。 数据量会长,所以粒度要留余量—— 今天切 12 个月刚好,三年后单分区就三亿行了,得改成按月或按周。

三、谓词能不能裁剪:三种能用,两种不能用

能裁剪的三种: where ym = 值、where ym between A and B、where ym in (值1, 值2)。 这三种都直接作用在分区键上,优化器一看边界就知道去哪些分区。 不能裁剪的两种: ① 分区键被函数包住(substr、to_char、trunc、算术运算); ② 分区键根本没出现在 where 里。 第一种是「写法问题」,改 SQL 就行;第二种是「设计问题」,改不了。 所以「按什么切」这件事,必须在上线前想清楚——表建好了、数据灌进去了,再改分区键就是重建表。 有个例外值得知道:INTERVAL 分区表上,where ym > 某个值 也能裁剪, 因为 INTERVAL 的分区边界是可推导的(每个月一个),优化器能算出「从哪个分区开始往后都要」。

四、算一遍:分区到底省了多少 I/O

240 万行、每块 40 行:不分区是 6 万块。 按 12 个分区切,每次查询只扫 5000 块——是原来的 1/12。 注意这个「1/12」不随表变大而变。 表从 240 万涨到 2.4 亿,只要分区数跟着涨,单次查询要扫的块数就不变。 这才是分区最本质的好处:把「随数据量线性增长」换成「只跟单分区大小有关」。 反过来说:如果分区数不跟着数据量涨,好处就慢慢没了。 这也是为什么按时间分区的表,最好用 INTERVAL 让它自动加分区——分区数自动跟数据量走。

五、动手 Demo:把三个算式跑一遍:粒度、块数、分区数上限

这一篇的 Demo 全是算,没有一条 DDL。 但算出来的三个数正好是决策依据: 单分区多少行、单次查询扫多少块、分区数到多少就该停。 数都是现算的,不是我写死的结论。 换一组输入(比如二十亿行、按天切),结论会跟着变——这正是重点。

# 分区策略的账:单分区多大、能省多少次 I/O
total = 500_000_000
print("五亿行,切法不同,单分区差多少:")
for n, unit in ((12, "月"), (365, "天"), (4, "季度")):
    print("  按 %3d 个%s切 -> 单分区 %s 行" % (n, unit, format(total // n, ",")))

rows, per_blk = 2_400_000, 40
print("")
print("240 万行、每块 40 行:")
print("  不分区:%s 块" % format(rows // per_blk, ","))
for parts in (12, 24):
    one = rows // parts
    print("  %d 个分区:单次查询只扫 %s 块(原来的 1/%d)"
          % (parts, format(one // per_blk, ","), parts))

print("")
print("分区数太多也不行:")
for parts in (12, 365, 3650):
    print("  %4d 个分区 -> 单分区 %s 行" % (parts, format(total // parts, ",")))

实跑输出:

--------------------------------------------------------------------
第四十九课:分区策略——按什么切,切多细
--------------------------------------------------------------------

  **分区策略就两个问题:按什么列切、切多细。**  **两个问题都有可算的答案。**

  一、按什么切:看你的查询怎么写
  --------------------------------------------------------------------
  **① 按时间切(范围分区):**  **适合「总是带时间条件」的查询**,比如「上个月」「今年」。
  **也适合「定期删旧数据」——`DROP PARTITION` 是秒级的。**
  **② 按业务键切(散列分区):**  **适合「查询总是带某个 ID」的场景**,比如按客户号。
  **缺点是范围查询要扫全部分区。**
  **③ 按状态切(列表分区):**  **适合「值和业务含义绑定」的列**,
  **好处是「归档已完成的订单」就是「搬走一个分区」。**

  二、切多细:单分区多大才算合适
  --------------------------------------------------------------------
  **五亿行按 12 个月分区:单分区 41,666,666 行。**
  **五亿行按 365 个天分区:单分区 1,369,863 行。**
  **五亿行按 4 个季度分区:单分区 125,000,000 行。**
  **经验值:单分区控制在一千万到一亿行之间。**  **太小了分区数量爆炸**——几千个分区,
  数据字典本身就成负担,  **Oracle 对分区数有硬上限(单个表 1024K 个),但实际几百个就该停。**
  **太大了等于没切**——单分区几亿行,扫一个分区还是慢。

  三、算一算:分区能省多少次 I/O
  --------------------------------------------------------------------
  **一张 240 万行的表,按 12 个分区切,每分区 20 万行。**
  **查一个月的数据:**  **不分区要扫 2,400,000 行,分区后只扫 200,000 行,是原来的 1/12。**
  **按每块 40 行算:**  **不分区 60,000 块,分区后 5,000 块。**
  **这就是分区的账:把「线性增长」换成「只跟单分区大小有关」。**
  **表从 240 万涨到 2.4 亿,只要分区数跟着涨,单次查询要扫的块数就不变。**

  四、不能裁剪的谓词:分区也会白切
  --------------------------------------------------------------------
  **裁剪的前提是「条件能和分区键对上」。**
  **`where ym = '2024-07'` 能裁剪;`where substr(ym, 1, 4) = '2024'` 就不能**——
  **因为条件作用在函数结果上,数据库不知道它对应哪个分区。**
  **同理,`where to_char(ym, 'YYYY') = '2024'`、`where ym + 1 > ...`,都会让裁剪失效。**
  **这一条和第 10 章「在列上算东西索引就废了」是同一类毛病**——
  **都是「把列包进函数,数据库就没法用它的有序性了」。**

第一段:五亿行三种切法,单分区从 1.37 亿到 4167 万到 137 万。 按季度切(1.25 亿行)已经超出「一亿」这个上限了,按天切(137 万行)又太细, 按月切(4167 万行)正好落在区间里。 第二段:240 万行不分区是 6 万块,12 个分区后单次只扫 5000 块。 这个 1/12 是「分区带来的确定性收益」——它不靠缓存、不靠硬件,纯粹是「少碰数据」。 第三段:分区数从 12 到 365 到 3650。 3650 个分区的单分区只剩 13 万行—— 这个粒度已经低于「一千万」的下限两个数量级了,分区数量带来的字典开销会盖过收益。 这就是「策略」这件事的全部内容: 先用第一段的账选粒度,再用第三段的账检查分区数是不是太多。 两边都过关,才叫合适的切法。

30 秒实验

  1. 把 total 改成 20 亿,重跑。 按月切的单分区变成 1.67 亿行,超出上限了——这时候该考虑组合分区(按月 × 按客户哈希)。
  2. 把 per_blk 从 40 改成 80(行长减半),重跑。 块数整体减半,但比例不变。所以「每块多少行」只影响绝对值,不影响「分区省几倍」。
  3. 算一下「按天切三年」的分区数:365 × 3 = 1095。 已经过了「几百个就该停」这条线。这就是为什么按天切通常要配组合分区或者只保留近期分区。

常见误区

  1. 「按数据特征切,比如按金额区间」。 除非查询真的总按金额范围查, 否则裁不掉。分区键要跟着查询走,不跟着数据走。
  2. 「分区数越多,查询越快」。 几百个之后,字典和解析开销上来,反而变慢。 单分区一千万到一亿行是常见区间。
  3. 「先建表,以后觉得不合适再改分区键」。 改分区键等于重建表 + 重灌数据, 而且要在停机窗口做。上线前想清楚。
  4. 「where 里带分区键就一定能裁剪」。 带是必要条件,不是充分条件。 包在函数里(to_char(ym,'YYYY'))照样裁不掉。
自测三问(点开对答案) 1. 选分区键的第一条判据是什么? —— **最常用的查询条件**,不是数据本身的特征。**分区键和查询条件不一致,分区就白切。** 2. 单分区多大算合适? —— **一千万到一亿行是常见经验值。** **太小分区数爆炸,太大等于没切。** 3. 哪些谓词会破坏裁剪? —— **把分区键包进函数的**(`substr`、`to_char`、算术),**以及不带分区键的**。 4. 为什么分区的收益不随表变大而消失? —— **因为单次查询只扫一个分区,而单分区大小由「分区数」控制。** **只要分区数跟着数据量涨,要扫的块数就不变。**

下一篇讲管理:加、合、删、换四个动作。 重点是「删」,因为它是分区最值钱的地方,也是唯一有数据丢失风险的地方。

相关笔记