分区表

概述

当单表数据量随着时间变得越来越大时,会给数据的管理和查询带来不便。我们可以考虑对表进行分区,利用分区表特性将数据分成小块存储,可以大幅提升查询性能、管理便捷性及数据的可用性。

分区表是将表在物理存储层面分成多个小的片段,这些片段即称为分区,每个分区保存表的一部分数据。表的分区对上层应用是完全透明的,从应用的角度来看,表在逻辑上依然是一个整体。

每个分区都有自己的名字并可以拥有不同的存储特性,例如可以将分区保存在不同的表空间。

向分区表插入数据时,为了判断每条数据应该被分配至哪个分区,我们通常需要选择定义一个分区键(Partition Key)。根据每条数据分区键的值或者对其运算的结果来决定数据的分区归属,分区键可以由1个或多个列组成。

适用场景

  1. 数据管理手段:分区表不是常规的查询性能优化手段,而是一种数据管理手段。大部分的查询语句,如果分区表设计得不好,不能很好地做分区裁剪,对于查询性能会有负面影响。

  2. 大表拆分:本质是将一个大表在逻辑上拆分成较小的物理部分。当查询或更新访问单个分区的很大一部分时,可以通过利用该分区的顺序扫描来提高性能,而不是使用分散在整个表中的索引和随机访问读取。

  3. 历史数据处理:表中有大量的历史数据,数据存在明显的时间顺序,经常需要对历史的数据作清理或归档备份。

  4. 分层存储:表的存储需要分层存储,不同的数据部分需要存到不同的存储介质中,可以考虑使用分区表进行数据的分层存储。

  5. 分区规划:分区数一定要做好提前规划,预估好需要支持的最大分区数,采用合理的分区策略。

核心优势

查询性能优化

对于SQL查询,当 WHERE 条件涉及分区键时,可以快速定位需要扫描的分区,这样可以将数据的扫描范围限制在很小的范围,极大地提升查询性能。这个特性叫做分区裁剪(Partition Pruning)。

另外,在多表连接 JOIN 时,如果在每个表在连接的键上都进行了分区,那么引擎可以将两个大表之间的连接转换成更小的分区级连接,极大提升连接速度,这个特性叫做分区连接(Partition-wise Join)。

管理便利性

使用分区表之后,原来表级别的管理操作也被分散至"分区级",各个分区上独立地进行运维任务。原先一个大表上的运维任务,现在可以拆开成一系列小任务分散在不同的时间窗口执行。例如,平时备份表的操作,现在可以备份单个分区。

数据可用性

表分区后,每个分区都具有独立性。在你操作某个分区时,不会影响其他分区数据的使用,即使某个分区因为故障不可用,也完全不会影响其他分区上运行的事务。同时分区可以存储在不同的表空间,分散I/O压力。

分区策略

根据不同的应用场景,可以为表选择不同的分区策略。ProtonBase 提供的基础分区策略有:

  • 范围分区(Range Partition)
  • 哈希分区(Hash Partition)
  • 列表分区(List Partition)

在基础分区策略的基础上,可以有进一步的复合分区策略。

范围分区

范围分区根据预先定义的范围来划分分区,范围分区最适合管理类似且有明显顺序的数据,根据数据的顺序可以很容易划定分区范围。范围分区最典型的应用场景就是按时间对数据进行分区,所以其经常使用时间类型的分区键。分区的范围必须连续,不能有间隙或重叠。分区的边界值必须与主表的分区键类型匹配。

-- 创建RANGE分区表
CREATE TABLE table_name (
    column_definitions... )
    PARTITION BY RANGE (partition_key);
 
-- 创建RANGE分区
CREATE TABLE partition_name PARTITION OF parent_table_name
    FOR VALUES FROM (start_value) TO (end_value);

参数说明

  • start_value:分区的起始值,使用FOR VALUES FROM子句指定。该值是包含的。
  • end_value:分区的结束值,使用FOR VALUES TO。对于FOR VALUES TO,该值是不包含的,所有大于等于指定值的数据被分配至下一个分区,除了第一个分区,每个分区的下限即前一个分区的上限。
  • MIN_VALUE表示RANGE分区中的最小可能值,它是一个特殊的常量,用于指定分区的下界。使用MIN_VALUE作为分区的起始值时,表示该分区包含了分区键的最小值。FOR VALUES FROM (MIN_VALUE) TO (value)表示分区的范围从最小值开始,一直到value(不包含value)。
  • MAX_VALUE表示RANGE分区中的最大可能值,它是一个特殊的常量,用于指定分区的上界。使用MAX_VALUE作为分区的结束值时,表示该分区包含了分区键的最大值。
  • MIN_VALUEMAX_VALUE只能用于RANGE分区,不适用于LIST分区或HASH分区。
-- 如下使用时间字段 info_time 作为分区键,使用范围分区方法进行分区
CREATE TABLE range_pt (
    uid integer NOT NULL,
    info_time timestamp NOT NULL,
    money decimal(5,2) NOT NULL,
    PRIMARY KEY (uid,info_time)
) PARTITION BY RANGE (info_time);
 
-- 如下添加 3 个子分区
CREATE TABLE range_pt_2020_1 PARTITION OF range_pt FOR VALUES FROM ('2020-1-01') TO ('2020-1-01'::timestamp + interval '1 month');
CREATE TABLE range_pt_2020_2 PARTITION OF range_pt FOR VALUES FROM ('2020-2-01') TO ('2020-2-01'::timestamp + interval '1 month');
CREATE TABLE range_pt_2020_3 PARTITION OF range_pt FOR VALUES FROM ('2020-3-01') TO ('2020-3-01'::timestamp + interval '1 month');
 
-- 也可以加两个分区,把(minvalue到2020-1-01)和 (2020-4-01到maxvalue独立出来)
CREATE TABLE range_pt_before_2020_1 PARTITION OF range_pt FOR VALUES FROM (MINVALUE) TO ('2020-1-01');
CREATE TABLE range_pt_after_2020_4 PARTITION OF range_pt FOR VALUES FROM ('2020-4-01') TO (MAXVALUE);

这时可以查看表结构:

postgres=> \d+ range_pt
                                              Partitioned table "public.range_pt"
  Column   |            Type             | Collation | Nullable | Default | Storage | Compression | Stats target | Description
-----------+-----------------------------+-----------+----------+---------+---------+-------------+--------------+-------------
 uid       | integer                     |           | not null |         | ???     |             |              |
 info_time | timestamp without time zone |           | not null |         | ???     |             |              |
 money     | numeric(5,2)                |           | not null |         | ???     |             |              |
Partition key: RANGE (info_time)
Indexes:
    "range_pt_pkey" PRIMARY KEY, btree (uid ASC, info_time ASC)
Partitions: range_pt_2020_1 FOR VALUES FROM ('2020-1-01') TO ('2020-1-01'::timestamp without time zone + '1 mon'),
            range_pt_2020_2 FOR VALUES FROM ('2020-2-01') TO ('2020-2-01'::timestamp without time zone + '1 mon'),
            range_pt_2020_3 FOR VALUES FROM ('2020-3-01') TO ('2020-3-01'::timestamp without time zone + '1 mon'),
            range_pt_after_2020_4 FOR VALUES FROM ('2020-4-01') TO (MAXVALUE),
            range_pt_before_2020_1 FOR VALUES FROM (MINVALUE) TO ('2020-1-01')
Access method: row_v1

范围分区支持创建默认分区,如果数据没有分配到已有的分区中,则会被写入到默认分区。

CREATE TABLE range_default PARTITION OF range_pt DEFAULT;

范围分区也支持表达式分区:

-- 创建主表,添加了`PARTITION BY RANGE (DATE_TRUNC('day', created_at))`子句,定义了分区键。
-- `DATE_TRUNC('day', created_at)`表示按照`created_at`字段的日期部分进行分区。
CREATE TABLE sales
(
    id         SERIAL,
    product_id INT            NOT NULL,
    quantity   INT            NOT NULL,
    price      DECIMAL(10, 2) NOT NULL,
    created_at TIMESTAMP      NOT NULL
) PARTITION BY RANGE (DATE_TRUNC('day', created_at));
 
-- 创建分区表
CREATE TABLE sales_y2023m01 PARTITION OF sales
    FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
 
CREATE TABLE sales_y2023m02 PARTITION OF sales
    FOR VALUES FROM ('2023-02-01') TO ('2023-03-01');
 
-- 添加数据
INSERT INTO sales (product_id, quantity, price, created_at)
VALUES (1, 10, 100.00, '2023-01-15 10:30:00'),
       (2, 5, 50.00, '2023-01-20 15:45:00'),
       (1, 8, 80.00, '2023-02-03 09:15:00'),
       (3, 3, 30.00, '2023-02-18 14:00:00');

哈希分区

哈希分区是对指定的分区键(Partition Key)运行哈希算法来决定数据存储在哪个分区。哈希分区会随机地将数据分配到各个分区中,并尽量平均,保证各个分区的大小差不多一致。

由于数据是随机分布,所以哈希分区并不适合管理有明显时间顺序的历史数据。它更适合需要将数据平均分布到各个不同存储设备上的场景。同时在选用哈希分区时建议满足下列条件:

  1. 选取分区键时尽量选取唯一列(Unique)或列中有大量唯一值(Almost Unique)的列。
  2. 创建哈希分区时,分区的数量尽量是2的幂,例如2, 4, 8, 16等。

哈希分区表是通过 CREATE TABLE 语句的 PARTITION BY HASH 子句来创建的,创建时你可以显式地指定每个分区名称,所属表空间。

-- 创建HASH分区表
CREATE TABLE table_name (
    column_definitions... )
    PARTITION BY HASH (partition_key);
 
-- 创建HASH分区
CREATE TABLE child_table_name PARTITION OF parent_table_name
    FOR VALUES WITH (MODULUS M, REMAINDER R);

哈希分区有两个关键参数:

  • MODULUS(模数):指定哈希分区的总数,必须是正整数。
  • REMAINDER(余数):指定分区的编号,取值范围为 0 到模数 -1。
CREATE TABLE hash_pt (
    userid int4,
    username character varying(64),
    ctime timestamp(6) without time zone,
    PRIMARY KEY(userid))
PARTITION BY HASH(userid);
 
-- 如下添加 16 个子分区,即创建 16 个子分区表:
CREATE TABLE hash_pt_0 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 0);
CREATE TABLE hash_pt_1 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 1);
CREATE TABLE hash_pt_2 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 2);
CREATE TABLE hash_pt_3 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 3);
CREATE TABLE hash_pt_4 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 4);
CREATE TABLE hash_pt_5 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 5);
CREATE TABLE hash_pt_6 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 6);
CREATE TABLE hash_pt_7 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 7);
CREATE TABLE hash_pt_8 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 8);
CREATE TABLE hash_pt_9 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 9);
CREATE TABLE hash_pt_10 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 10);
CREATE TABLE hash_pt_11 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 11);
CREATE TABLE hash_pt_12 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 12);
CREATE TABLE hash_pt_13 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 13);
CREATE TABLE hash_pt_14 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 14);
CREATE TABLE hash_pt_15 PARTITION OF hash_pt FOR VALUES WITH (MODULUS 16, REMAINDER 15);

查看表结构:

postgres=> \d+ hash_pt
                                               Partitioned table "public.hash_pt"
  Column  |              Type              | Collation | Nullable | Default | Storage | Compression | Stats target | Description
----------+--------------------------------+-----------+----------+---------+---------+-------------+--------------+-------------
 userid   | integer                        |           | not null |         | ???     |             |              |
 username | character varying(64)          |           |          |         | ???     |             |              |
 ctime    | timestamp(6) without time zone |           |          |         | ???     |             |              |
Partition key: HASH (userid)
Indexes:
    "hash_pt_pkey" PRIMARY KEY, btree (userid ASC)
Partitions: hash_pt_0 FOR VALUES WITH (MODULUS 16, REMAINDER 0),
            hash_pt_1 FOR VALUES WITH (MODULUS 16, REMAINDER 1),
            hash_pt_10 FOR VALUES WITH (MODULUS 16, REMAINDER 10),
            hash_pt_11 FOR VALUES WITH (MODULUS 16, REMAINDER 11),
            hash_pt_12 FOR VALUES WITH (MODULUS 16, REMAINDER 12),
            hash_pt_13 FOR VALUES WITH (MODULUS 16, REMAINDER 13),
            hash_pt_14 FOR VALUES WITH (MODULUS 16, REMAINDER 14),
            hash_pt_15 FOR VALUES WITH (MODULUS 16, REMAINDER 15),
            hash_pt_2 FOR VALUES WITH (MODULUS 16, REMAINDER 2),
            hash_pt_3 FOR VALUES WITH (MODULUS 16, REMAINDER 3),
            hash_pt_4 FOR VALUES WITH (MODULUS 16, REMAINDER 4),
            hash_pt_5 FOR VALUES WITH (MODULUS 16, REMAINDER 5),
            hash_pt_6 FOR VALUES WITH (MODULUS 16, REMAINDER 6),
            hash_pt_7 FOR VALUES WITH (MODULUS 16, REMAINDER 7),
            hash_pt_8 FOR VALUES WITH (MODULUS 16, REMAINDER 8),
            hash_pt_9 FOR VALUES WITH (MODULUS 16, REMAINDER 9)
Access method: row_v1

列表分区

列表分区是由你为每个分区指定一系列的离散值(列表),当分区键等于特定的离散值时,数据会被放到相应的分区。列表分区可以让你自定义数据的组织方式,例如按照地域来分类数据。

列表分区表是通过 CREATE TABLE 语句的 PARTITION BY LIST 子句来创建的,创建时你需要为每个分区指定一个列表(离散值)。分区键的值必须是离散的。

-- 创建LIST分区表
CREATE TABLE table_name (
    column_definitions... )
    PARTITION BY LIST (partition_key);
 
-- 创建LIST分区
CREATE TABLE partition_name PARTITION OF parent_table_name
    FOR VALUES IN (value1, value2, ...);

列表分区支持为每个分区指定一个或多个离散值,分区值不能重复出现在多个分区中。

-- 对country字段建List分区
CREATE TABLE list_pt (
    userid int4,
    username character varying(64),
    country character varying(2),
    ctime timestamp(6) without time zone,
    PRIMARY KEY(userid, country))
PARTITION BY LIST(country);
 
CREATE TABLE list_pt_1 PARTITION OF list_pt FOR VALUES IN ('AT', 'DE', 'IT', 'FR', 'ES');
CREATE TABLE list_pt_2 PARTITION OF list_pt FOR VALUES IN ('US');
CREATE TABLE list_pt_3 PARTITION OF list_pt FOR VALUES IN ('CN');

查看表结构:

postgres=> \d+ list_pt
                                               Partitioned table "public.list_pt"
  Column  |              Type              | Collation | Nullable | Default | Storage | Compression | Stats target | Description
----------+--------------------------------+-----------+----------+---------+---------+-------------+--------------+-------------
 userid   | integer                        |           | not null |         | ???     |             |              |
 username | character varying(64)          |           |          |         | ???     |             |              |
 country  | character varying(2)           |           | not null |         | ???     |             |              |
 ctime    | timestamp(6) without time zone |           |          |         | ???     |             |              |
Partition key: LIST (country)
Indexes:
    "list_pt_pkey" PRIMARY KEY, btree (userid ASC, country ASC)
Partitions: list_pt_1 FOR VALUES IN ('AT', 'DE', 'IT', 'FR', 'ES'),
            list_pt_2 FOR VALUES IN ('US'),
            list_pt_3 FOR VALUES IN ('CN')
Access method: row_v1

列表分区同样支持创建默认分区:

CREATE TABLE list_pt_default PARTITION OF list_pt DEFAULT;

复合分区

复合分区,顾名思义,就是将多种分区策略结合起来使用,在基础分区的策略上,对每个分区再一次应用分区策略。例如,在基础的范围分区基础上,还可以对每个分区再次应用范围分区,即每个分区又被划分为若干个子分区。类似于中国可以划分为很多省(分区),每个省又可以划分为很多市(子分区)。

在使用复合分区时,3种基础分区策略可以随意组合,例如,使用范围分区作为基础分区,其子分区可以使用范围、哈希、列表分区策略。

二级分区示例

-- 使用二级分区的示例,第一级按省份列表分区,第二级按城市列表分区
-- 创建了一个名为`places`的母表,它按省份和城市两级进行列表分区。
-- 数据插入时会自动路由到对应的省份和城市分区表中。
-- 这种分区方式适合有明确已知列表值范围的场景。
CREATE TABLE places
(
    province   TEXT,
    city       TEXT,
    address    TEXT NOT NULL,
    created_at TIMESTAMP DEFAULT NOW(),
    PRIMARY KEY (province, city)
) PARTITION BY LIST (province);
 
CREATE TABLE places_anhui PARTITION OF places FOR VALUES IN ('安徽省') PARTITION BY LIST (city);
CREATE TABLE places_zhejiang PARTITION OF places FOR VALUES IN ('浙江省') PARTITION BY LIST (city);
 
-- 为其他省份创建分区表
CREATE TABLE places_anhui_hefei PARTITION OF places_anhui FOR VALUES IN ('合肥市');
CREATE TABLE places_anhui_wuhu PARTITION OF places_anhui FOR VALUES IN ('芜湖市');
 
-- 为安徽省的其他城市创建分区表
CREATE TABLE places_zhejiang_hangzhou PARTITION OF places_zhejiang FOR VALUES IN ('杭州市');
CREATE TABLE places_zhejiang_ningbo PARTITION OF places_zhejiang FOR VALUES IN ('宁波市');
-- 创建二级分区表的示例,第一级使用LIST分区按租户ID划分,第二级使用RANGE分区按订单时间划分。
-- 查询时可以根据tenant_id和order_date快速定位到相应的分区,提高查询效率。
-- 创建主表orders,指定第一级LIST分区键为tenant_id
CREATE TABLE orders
(
    order_id   SERIAL,
    tenant_id  INT,
    order_date DATE,
    amount     NUMERIC(10, 2)
) PARTITION BY LIST (tenant_id);
 
-- 创建每个租户的分区表,指定第二级RANGE分区键order_date
CREATE TABLE orders_tenant1 PARTITION OF orders
    FOR VALUES IN (1)
    PARTITION BY RANGE (order_date);
 
CREATE TABLE orders_tenant2 PARTITION OF orders
    FOR VALUES IN (2)
    PARTITION BY RANGE (order_date);
 
-- 为每个租户的分区表创建按月的RANGE分区
CREATE TABLE orders_tenant1_202301 PARTITION OF orders_tenant1
    FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
 
CREATE TABLE orders_tenant1_202302 PARTITION OF orders_tenant1
    FOR VALUES FROM ('2023-02-01') TO ('2023-03-01');
 
CREATE TABLE orders_tenant2_202301 PARTITION OF orders_tenant2
    FOR VALUES FROM ('2023-01-01') TO ('2023-02-01');
 
CREATE TABLE orders_tenant2_202302 PARTITION OF orders_tenant2
    FOR VALUES FROM ('2023-02-01') TO ('2023-03-01');
-- 先创建orders表,使用LIST分区,包含一个DEFAULT分区
CREATE TABLE orders
(
    order_id    INT,
    order_date  DATE,
    customer_id INT,
    amount      DECIMAL(10, 2),
    region      VARCHAR(20)
)
    PARTITION BY LIST (region);
 
-- 对于'North', 'East'分区orders_pRegionA,使用RANGE二级分区
CREATE TABLE orders_pRegionA
    PARTITION OF orders
        FOR VALUES IN ('North', 'East')
    PARTITION BY RANGE (order_date);
 
CREATE TABLE orders_pRegionA_2022
    PARTITION OF orders_pRegionA
        FOR VALUES FROM (MINVALUE) TO ('2023-01-01');
 
CREATE TABLE orders_pRegionA_2023
    PARTITION OF orders_pRegionA
        FOR VALUES FROM ('2023-01-01') TO (MAXVALUE);
 
-- 对于'South', 'West'分区orders_pRegionB,也使用RANGE二级分区
CREATE TABLE orders_pRegionB
    PARTITION OF orders
        FOR VALUES IN ('South', 'West')
    PARTITION BY RANGE (order_date);
 
CREATE TABLE orders_pRegionB_2022
    PARTITION OF orders_pRegionB
        FOR VALUES FROM (MINVALUE) TO ('2023-01-01');
 
CREATE TABLE orders_pRegionB_2023
    PARTITION OF orders_pRegionB
        FOR VALUES FROM ('2023-01-01') TO (MAXVALUE);
 
-- 对于DEFAULT分区orders_pDefault,使用HASH分区,MODULUS为5,创建5个HASH分区
CREATE TABLE orders_pDefault
    PARTITION OF orders
        DEFAULT
    PARTITION BY HASH (customer_id);
 
CREATE TABLE orders_pDefault_1 PARTITION OF orders_pDefault FOR VALUES WITH (MODULUS 5, REMAINDER 0);
CREATE TABLE orders_pDefault_2 PARTITION OF orders_pDefault FOR VALUES WITH (MODULUS 5, REMAINDER 1);
CREATE TABLE orders_pDefault_3 PARTITION OF orders_pDefault FOR VALUES WITH (MODULUS 5, REMAINDER 2);
CREATE TABLE orders_pDefault_4 PARTITION OF orders_pDefault FOR VALUES WITH (MODULUS 5, REMAINDER 3);
CREATE TABLE orders_pDefault_5 PARTITION OF orders_pDefault FOR VALUES WITH (MODULUS 5, REMAINDER 4);

运维操作

新增分区

新增分区有两种主要方式:

  1. 通过 CREATE TABLE PARTITION OF 语句来新建表
  2. 先把表建好,再通过对父表的 ALTER TABLE ... ATTACH PARTITION ... 来对父表新增分区

CREATE TABLE PARTITION OF 方式

CREATE [ [ GLOBAL | LOCAL ] { TEMPORARY | TEMP } | UNLOGGED ] TABLE [ IF NOT EXISTS ] table_name
    PARTITION OF parent_table [ (
  { column_name [ WITH OPTIONS ] [ column_constraint [ ... ] ]
    | table_constraint }
    [, ... ]
) ] { FOR VALUES partition_bound_spec | DEFAULT }
[ PARTITION BY { RANGE | LIST | HASH } ( { column_name | ( expression ) } [ COLLATE collation ] [ opclass ] [, ... ] ) ]
[ USING method ]
[ WITH ( storage_parameter [= value] [, ... ] )]
[ TABLESPACE tablespace_name ]

常见分区方式示例:

-- 按日期或时间戳分区:
PARTITION BY RANGE (created_at)
 
-- 按整数ID分区:
PARTITION BY RANGE (user_id)
 
-- 按哈希值分区:
PARTITION BY HASH (id)
 
-- 按列表分区:
PARTITION BY LIST (country)
 
-- 按组合键分区:
PARTITION BY RANGE (created_at, user_id)
 
-- 使用表达式或函数:
PARTITION BY RANGE (DATE_TRUNC('day', created_at))
PARTITION BY RANGE (EXTRACT(YEAR FROM created_at))

ALTER TABLE ATTACH PARTITION 方式

这种方式要求分区和父表的 Schema 完全一致,包括数据类型、浮点精度、NOT NULL、PRIMARY KEY 等属性的设置也需要一致。

-- attach range partition
ALTER TABLE table_name ATTACH PARTITION partition_name FOR VALUES FROM ('value1') TO ('value2');
 
-- attach list partition
ALTER TABLE table_name ATTACH PARTITION partition_name FOR VALUES IN ('value1', 'value2');
 
-- attach hash partition
ALTER TABLE table_name ATTACH PARTITION partition_name FOR VALUES WITH (MODULUS value1, REMAINDER value2);

也支持把物化视图添加到父表中,使用语法与普通分区添加一致。注意,物化视图的结构需与主表完全一致。

注意事项:

  • 建议在创建基表时就定义分区方式,如果使用 ATTACH 方式,新分区表的表结构必须与主表完全一致,且新分区的范围不能与已有分区重叠。
  • 对分区表的查询可以像普通表一样进行。
  • 当插入新数据时,可以使用普通的 INSERT 语句插入数据,分区表会自动根据分区键将数据路由到对应的分区。
  • 查询分区表与查询普通表相同,使用 SELECT 语句。查询分区表,ProtonBase 会自动识别只扫描相关分区,优化查询可在 WHERE 子句中指定分区键条件,缩小查询范围。
  • 如果表中有主键,分区键必须是主键的一部分或全部,主键约束只能在分区表层面定义,不能在每个分区上单独定义。

删除分区

  1. 如果要将分区表直接删除,通过 DROP TABLE partition_table 来完成。
  2. 如果要保留分区表数据,通过对父表的 ALTER TABLE parent_table DETACH PARTITION partition_name 来完成。

替换分区

  1. 对原有的分区表进行 DETACH 操作。
  2. 对新的分区表 ATTACH 到父表上。

合并分区

  1. 对要合并的分区进行 DETACH 操作。
  2. 将两个分区的数据写到同一个表中。
  3. 将新表 ATTACH 到父表中。
  4. 清理无用的数据。

分裂分区

  1. 将要分裂的分区进行 DETACH 操作。
  2. 通过SQL,将要分裂的分区分成几个表。
  3. 分别将新的分区 ATTACH 到父表中。
  4. 清理无用的数据。

最佳实践

选择合适的分区键

  • 分区键应该是查询条件中经常使用的列,以便利用分区裁剪(Partition Pruning)提高查询效率。
  • 分区键的值应该有良好的分布性,避免数据倾斜导致的性能问题。

注意分区边界

  • 分区边界应该连续且不重叠。
  • 定义分区边界时,考虑数据的增长趋势,预留足够的空间应对未来数据增长。

管理分区数量

  • 分区数量不宜过多,否则可能增加管理复杂度和系统开销。
  • 定期评估分区数量,根据数据量和访问模式适当调整。
  • 对于范围分区,及时添加新分区以接收新数据,避免数据插入失败。
  • 对于不再需要的历史分区,及时进行归档或删除,以节省存储空间。

谨慎执行 DDL 操作

  • 对分区表执行 DDL 操作(如添加列)时,会影响所有分区,耗时可能较长。
  • 在业务低峰期执行 DDL 操作,或使用 CREATE TABLE ... AS 技术先创建新表再替换。

注意分区表的限制

  • 分区表不支持某些特性,如外键、ON CONFLICT 子句等。
  • 查询分区表时,某些操作(如 DISTINCTGROUP BY)可能需要跨分区执行,影响性能。