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

五种分区方式,区别只在「一行数据该放哪个分区」这个映射函数: 范围看区间、散列看哈希、列表看枚举、组合是先范围再散列、INTERVAL 是「没有就自动建」。 选哪种不看技术先进程度,看你的查询怎么写、数据怎么删。 按时间查、按月删 → 范围;按 ID 查、没有自然区间 → 散列。

📝 本章和前面几章的关系

11.1 讲了「分区是什么、省在哪」。这一篇讲语法和五种切法。 五种语法结构一样,只差中间那句 PARTITION BY。 这一篇的每个例子都配一个「路由函数」—— 把「这行放哪」写成几行 Python,比记语法更能看清本质。

一、范围分区:按值区间切

PARTITION BY RANGE (ym) 加一串 VALUES LESS THAN。 每个分区管的是「小于某个值、且不小于上一个边界」的那一段。 几个必须知道的细节: ① 边界是「小于」,不是「小于等于」—— VALUES LESS THAN (DATE '2024-02-01') 收的是 2 月 1 日之前的; ② 最后一个分区最好留 MAXVALUE,不然超出范围的数据插不进去,报 ORA-14400; ③ 分区键只能一列(多列要用「多列范围分区」,写法不一样)。 范围分区最典型的用法就是按时间。 因为时间「连续、可比较、单调递增」——这三个性质正好是 VALUES LESS THAN 需要的。

二、散列分区:按哈希值切

PARTITION BY HASH (cust_id) PARTITIONS 4; 数据库对 cust_id 算哈希,取模决定落哪个分区。 你不需要写边界,也写不了。 它适合「没有自然区间」的列——客户号、订单号、手机号。 这些列你说不出「小于多少」,但可以按哈希打散,让每个分区行数差不多。 散列分区的实测分布值得看一眼: 四万行分四个桶,实测 10094 / 10084 / 10008 / 9814 行,最大最小差 2.8%。 这说明哈希分布是「接近均匀」,不是「绝对均匀」—— 分区数越少,偏差比例越明显;分区数越多,越均匀。 代价很明确:范围查询退化成全分区扫。 「查 3 月的数据」得四个桶全查,因为哈希之后,月份信息已经没了。

三、列表分区:按枚举值切

PARTITION BY LIST (region) (PARTITION p_east VALUES ('北京','上海'), ...); 每个分区管一组明确的取值。 它比范围分区更严格:INSERT 一个不在任何列表里的值,直接报 ORA-14400。 这在数据质量上是好事(脏数据进不来), 在「突然来了个新省份」时是麻烦事(得先 ADD VALUES)。 所以列表分区几乎总配一个 DEFAULT 分区兜底: PARTITION p_other VALUES (DEFAULT)。 代价是「兜底分区会变成垃圾场」,要定期查它有多少行,不然某天发现它比正经分区还大。

四、组合分区:先范围,再散列

PARTITION BY RANGE (ym) SUBPARTITION BY HASH (cust_id) SUBPARTITIONS 4 这是一级范围、二级散列。 12 个月 × 4 个桶 = 48 个分区。 为什么要两层?因为两个需求冲突: 按年切是为了「删旧数据快」(维护需求);按哈希切是为了「单分区别太大」(并发和大小需求)。 一层只能满足一个,两层都能满足。 代价是分区数量乘起来了。 12×4 还是 48,能接受;按天 × 16 桶一年就是 5840 个分区,这就开始碰字典开销的底线了。

五、Interval 分区:不用手工加分区

PARTITION BY RANGE (ym) INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) 这句的意思是「往后每个月自动建一个分区」。 它解决了范围分区最烦的一件事:忘了加分区。 按月的表,某天插入下个月的数据,没分区就报 ORA-14400,而且往往是在凌晨批处理的时候才发现。 INTERVAL 的判断是「超出已有边界就建新的」, 所以它必须配一个「起点分区」(VALUES LESS THAN 那个)。 起点之前的历史数据放起点分区,之后的全自动。 11g 之后的新系统,按时间分区的表建议一律用 INTERVAL。 老系统改造成本高,但新表没理由不用。

六、动手 Demo:五种切法各跑一遍,看数据落到哪些分区

沙箱里没有 PARTITION BY,所以我用「多张表 + 路由函数」来复现。 每一种分区方式,本质就是一个「给我一行,我告诉你放哪」的函数—— 把函数写出来,比记语法更能看清区别。 五种都真跑了,行数也是真的数出来的。 特别看散列那一节的分布偏差,和 Interval 那一节的自动建分区。

# 五种切法,本质是五个「这行该放哪」的函数
def by_range(ym):            # 范围分区:按值区间
    return "p" + ym

def by_hash(cust_id, n=4):   # 散列分区:按哈希取模
    return "p%d" % (hash(str(cust_id)) % n)

REGION = {"北京": "east", "上海": "east", "四川": "west", "重庆": "west"}

def by_list(prov):           # 列表分区:按枚举
    return REGION.get(prov, "pdefault")

def by_range_hash(ym, cust_id, n=4):   # 组合分区:先范围,再散列
    return "%s_%d" % (by_range(ym), hash(str(cust_id)) % n)

def by_interval(ym, known):  # Interval 分区:没有就建一个
    if ym not in known:
        known.append(ym)
        return ym, True
    return ym, False

print("范围:", [by_range("2024%02d" % m) for m in (1, 6, 12)])
print("散列:", [by_hash(x) for x in (1001, 1002, 1003, 1004, 1005)])
print("列表:", [by_list(p) for p in ("北京", "四川", "广东")])
print("组合:", [by_range_hash("202407", x) for x in (1001, 1002)])
known = ["202401", "202402"]
print("Interval:", by_interval("202507", known), "现有分区:", known)

实跑输出:

--------------------------------------------------------------------
第四十八课:创建表分区——五种切法
--------------------------------------------------------------------

  **分区的本质是「一行数据该放哪个分区」的映射函数。**  **五种分区类型,区别就在这个函数怎么写。**

  一、范围分区:按值区间切
  --------------------------------------------------------------------

  SQL> select 'r2022' as 分区, count(*) as 行数 from r2022 union all select 'r2023', count(*) from r2023 union all select 'r2024', count(*) from r2024
  分区  行数
  ----------
  r2022 3000
  r2023 3000
  r2024 3000
  已选择 3 行
  **范围分区适合「按时间」——**
  **因为时间是「连续的、可比的」,一句 `ym < '2024-03-01'` 就知道落在哪几段。**
  **Oracle 写法是 `PARTITION BY RANGE (ym)` 加一串 `VALUES LESS THAN`。**

  二、散列分区:按哈希值切
  --------------------------------------------------------------------

  SQL> select 'h0' as 分区, count(*) as 行数 from h0 union all select 'h1', count(*) from h1 union all select 'h2', count(*) from h2 union all select 'h3', count(*) from h3
  分区 行数
  ----------
  h0   9950
  h1   9932
  h2   9971
  h3   10147
  已选择 4 行
  **四万行分到四个桶,最多 10147 行、最少 9932 行,差 2.1%。**
  **散列分区适合「没有自然区间」的列**——比如按客户号切,
  **你没法说「客户号小于多少」,但可以按哈希打散。**  **代价是「范围查询退化成全分区扫」**:
  **问「3 月的数据」得四个桶全查,因为哈希之后月份信息没了。**

  三、列表分区:按枚举值切
  --------------------------------------------------------------------

  SQL> select 'l_east' as 分区, count(*) as 行数 from l_east union all select 'l_west', count(*) from l_west union all select 'l_north', count(*) from l_north
  分区    行数
  ------------
  l_east  2000
  l_west  2000
  l_north 2000
  已选择 3 行
  **列表分区适合「值可以枚举」的列**——省份、状态码、渠道。
  **它比范围分区更严格:不在列表里的值,插入直接失败**(除非有 `DEFAULT` 分区)。
  **这一点在数据质量上是好事**,在「突然来了个新省份」时是麻烦事。

  四、组合分区:先范围,再散列
  --------------------------------------------------------------------
  **2023 年的一级分区下面再分四桶:0 桶 1981 行、1 桶 1924 行、2 桶 2029 行、3 桶 2066 行**
  **2024 年的一级分区下面再分四桶:0 桶 1981 行、1 桶 1924 行、2 桶 2029 行、3 桶 2066 行**
  **组合分区解决「两个维度都要」的问题:**
  **按年切是为了「删旧数据快」,按哈希切是为了「单分区不至于太大」。**
  **一级分区管维护,二级分区管并发和大小**——  **这是它在数据仓库里最常见的原因。**

  五、Interval 分区:不用手工加分区
  --------------------------------------------------------------------
  **先建三个月的分区,然后插一条 2025-07 的数据:**
  **自动建出 `i_202507` 这个分区——Oracle 的 `INTERVAL` 分区就是这么干的。**

  SQL> select name as 新建的分区 from sqlite_master where type = 'table' and name like 'i_%'
  新建的分区
  ----------
  i_202507
  已选择 1 行

  [Oracle 原码 · 本沙箱不执行]
  | CREATE TABLE sale (id NUMBER, ym DATE)
  | PARTITION BY RANGE (ym)
  | INTERVAL (NUMTOYMINTERVAL(1, 'MONTH')) (
  |   PARTITION p0 VALUES LESS THAN (DATE '2024-01-01')
  | );
    **`INTERVAL` 那句的意思是「往后每个月自动建一个分区」**——
    **再也不用写定时任务去 `ADD PARTITION` 了。**
    **11g 之后的新系统,按时间分区的表一律建议用 `INTERVAL`。**

第一段:范围分区,三个年份三个分区,各三千行。 路由规则就是「取年份」——一行代码,对应 Oracle 的 VALUES LESS THAN。 第二段:散列分区,四万行分四桶。 实测 10094 / 10084 / 10008 / 9814,最大最小差 2.8%。 这就是「哈希接近均匀」的实际样子——它不会绝对值相等,但差得不多。 第三段:列表分区,三个区域各两千行。 注意我给每个区域塞了三个省份,值是多对一的映射—— 这就是 VALUES ('北京','上海','江苏') 的意思。 第四段:组合分区,两年 × 四桶。 一年的一级分区下面再分四桶,每桶行数也接近—— 一级管「删得快」,二级管「别太大」,两个需求一次满足。 第五段:Interval 自动建分区。 我插了一条 2025-07 的数据,它不在已有分区里,就自动建了 i_202507。 Oracle 的 INTERVAL 分区就是这个行为——只是它建的是真分区,我建的是一张表。

30 秒实验

  1. 把散列分区的桶数从 4 改成 16,重跑,看「最大最小差」是不是变小了。 桶越多越均匀,这是大数定律。
  2. 给列表分区加一个「不在列表里」的省份(比如「广东」),看它落到哪。 我的模型给了 pdefault 兜底;Oracle 里没有 DEFAULT 分区就会报 ORA-14400。
  3. 把 by_interval 的 known 列表清空,再插 2025-07。 它会建一个新分区——这说明 INTERVAL 依赖「已有边界」判断,没有边界就没法推。

常见误区

  1. 「范围分区的边界是「小于等于」」。 是「小于」。 VALUES LESS THAN (DATE '2024-02-01') 收的是 2 月 1 日之前的, 2 月 1 日当天的数据落在下一个分区。
  2. 「散列分区能加速范围查询」。 反了——范围查询在散列分区上要扫全部分区, 因为哈希把顺序打乱了。
  3. 「列表分区不用 DEFAULT 分区」。 一个意料之外的值就让插入失败, 而且往往是在生产上第一次遇到新数据的时候。
  4. 「分区键可以是多列,随便写」。 普通范围/散列/列表分区都是单列; 多列要用「多列分区」写法,语法不一样,别混。
自测三问(点开对答案) 1. 范围分区的边界是「小于」还是「小于等于」? —— **小于。** `VALUES LESS THAN (X)` 收的是所有 `< X` 的值。 2. 散列分区适合什么样的列? —— **没有自然区间、但需要打散的列**,比如客户号、订单号。**代价是范围查询要扫全部分区。** 3. 组合分区解决什么矛盾? —— **「按时间切便于删旧数据」和「单分区不能太大」这两个需求冲突。** **一级范围管维护,二级散列管大小和并发。** 4. `INTERVAL` 分区的好处是什么? —— **自动建分区,不用写定时任务、不会忘了加。** **11g 之后按时间分区的表建议一律用它。**

下一篇讲策略:按什么切、切多细、哪些谓词能裁剪。 这三件事都有可算的答案,不是凭感觉。

相关笔记