创建与管理分布式表

更新时间:
复制 MD 格式

随着业务发展,单张表的性能和容量会成为瓶颈。传统解决方案是垂直扩展(Scale Up),即提升单个服务器的硬件配置,但这很快会遇到性价比和物理极限。水平扩展(Scale Out)通过将数据分散到多个服务器,提供了近乎无限的扩展能力。PolarDB PostgreSQL分布式版集群支持水平扩展,它引入了两种特殊的表类型来管理数据分布:分布表(用于存储海量数据)和复制表(用于存储需要被频繁关联的小数据)。本文将指导您如何创建和管理这两种表。

创建分布表

创建分布表是实现数据水平拆分的核心步骤,适用于存储海量数据的业务表,如用户表、订单详情表等。其过程分为两步:首先创建一张普通表,然后通过create_distributed_table函数将其转换为分布表。

1. 选择分布列

分布列(Distribution Column)是决定数据如何分布到不同数据节点(DN)的关键。系统会根据指定列的哈希值来路由数据行。

  • 选择原则:通常选择表的主键或唯一标识作为分布列,这样可以确保数据均匀分布。

  • 重要限制:如果表上存在主键或唯一约束,分布列必须是构成这些约束的列之一。

示例:将普通表t转换为分布表,并以id列作为分布列。

  1. 创建一张普通表t。

    CREATE TABLE t (id int primary key, data text);
  2. 转换为分布表,并以id列作为分布列。

    SELECT create_distributed_table('t', 'id');

    返回结果如下:

     create_distributed_table 
    --------------------------
     
    (1 row)

2.(可选)指定分片数量

分片(Shard)是分布表的物理存储单元。默认情况下,每张分布表会被创建为32个分片。您也可以在创建时显式指定分片数量,或通过polar_cluster.shard_count参数进行全局设置。

示例:创建一张有4个分片的分布表。

  1. 创建一张普通表t1。

    CREATE TABLE t1 (id int primary key, data text);
  2. 显式指定分片数量。

    通过shard_count参数

    1. 在转换时通过shard_count参数指定。

      SELECT create_distributed_table('t1', 'id', shard_count := 4);

      返回结果如下:

       create_distributed_table 
      --------------------------
       
      (1 row)
    2. 查询分片数量。

      SELECT * FROM pg_dist_shard WHERE logicalrelid = 't1'::regclass;

      返回结果如下:

      logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue 
      --------------+---------+--------------+---------------+---------------
       t1           |  102072 | t            | -2147483648   | -1073741825
       t1           |  102073 | t            | -1073741824   | -1
       t1           |  102074 | t            | 0             | 1073741823
       t1           |  102075 | t            | 1073741824    | 2147483647
      (4 rows)

    通过polar_cluster.shard_count参数

    1. 通过polar_cluster.shard_count参数进行全局设置。

       SET polar_cluster.shard_count TO 4;
    2. 转换为分布表。

      SELECT create_distributed_table('t1', 'id');

      返回结果如下:

       create_distributed_table 
      --------------------------
       
      (1 row)
    3. 查询分片数量。

      SELECT * FROM pg_dist_shard WHERE logicalrelid = 't1'::regclass;

      返回结果如下:

      logicalrelid | shardid | shardstorage | shardminvalue | shardmaxvalue 
      --------------+---------+--------------+---------------+---------------
       t1           |  102072 | t            | -2147483648   | -1073741825
       t1           |  102073 | t            | -1073741824   | -1
       t1           |  102074 | t            | 0             | 1073741823
       t1           |  102075 | t            | 1073741824    | 2147483647
      (4 rows)

3. (可选)使用亲和组优化JOIN性能

在业务模型中,一个实体的相关信息可能分布在多张表内,通常需要进行关联查询JOIN才可以获得完整的信息。例如,user_info表存放所有的用户数据,user_order存放所有的用户订单,两者需要通过user_id关联。

在分布式数据库中,如果这两张表的数据随机分布在不同节点,关联查询就会触发跨节点的数据传输,开销极大。为解决此问题,PolarDB PostgreSQL分布式版入了亲和组(Colocation Group)的概念。

  • 作用:确保多张表中具有相同分布键值的数据行(例如user_id为1001的所有相关记录)始终物理地存放在同一个数据节点上。这样,基于分布键的JOIN操作就可以在节点内部高效完成,性能等同于本地查询。

  • 如何使用:PolarDB PostgreSQL分布式版提供了两种管理亲和组的方式:隐式(默认行为)和显式(推荐)。

    • 默认亲和(隐式行为):当您创建分布表时,如果未指定colocate_with参数,系统会根据分布列类型和分片数量这两个特征,自动将表放入一个默认的亲和组。这意味着所有分布列类型和分片数都相同的表,会被默认视为亲和。

      说明

      默认亲和(隐式行为)即使具有相同的分布列数据类型和分片数量,两张分布表中的数据也并不一定是互相关联的。

    • 显式控制(推荐方式):默认行为可能导致业务上无关联的表被错误地分到同一组。为精确控制表的亲和关系,建议采用显式声明:

      1. 创建亲和组中的第一张表时,在create_distributed_table函数中设置colocate_with := 'none',这将为它创建一个全新的、独立的亲和组。

      2. 创建后续需要亲和的表时,设置colocate_with := '第一张表的表名',将其准确地加入已创建的亲和组。

示例:将用户表和订单表放入同一个亲和组,而动物相关的表放入另一个组。

  1. 创建用户与订单相关表,并放入同一个亲和组。

    -- 创建一个新亲和组
    CREATE TABLE user_info (user_id int, user_data text);
    SELECT create_distributed_table('user_info', 'user_id', colocate_with := 'none');
    -- 加入已创建的亲和组中
    CREATE TABLE user_order (user_id int, order_id int, order_data text);
    SELECT create_distributed_table('user_order', 'user_id', colocate_with := 'user_info');
  2. 动物相关的表,并放入另一个亲和组。

    -- 创建一个新亲和组
    CREATE TABLE animal (animal_id int, animal_data text);
    SELECT create_distributed_table('animal', 'animal_id', colocate_with := 'none');
    -- 加入已创建的亲和组中
    CREATE TABLE animal_class (animal_id int, class_id int, class_data text);
    SELECT create_distributed_table('animal_class', 'animal_id', colocate_with := 'animal');
  3. 验证两组分布表有着不同的亲和组id,意味着各自被加入到了一个亲和组中。

    SELECT table_name, colocation_id, polar_cluster_table_type, distribution_column, shard_count
    FROM polar_cluster_tables
    WHERE table_name IN (
      'user_info'::regclass, 'user_order'::regclass,
      'animal'::regclass, 'animal_class'::regclass)
    ORDER BY colocation_id;

    返回结果如下:

      table_name  | colocation_id | polar_cluster_table_type | distribution_column | shard_count 
    --------------+---------------+--------------------------+---------------------+-------------
     user_info    |             3 | distributed              | user_id             |           4
     user_order   |             3 | distributed              | user_id             |           4
     animal       |             4 | distributed              | animal_id           |           4
     animal_class |             4 | distributed              | animal_id           |           4
    (4 rows)

创建复制表

复制表(Reference Table)的数据在所有数据节点上都存有一份完整的副本。它适用于存储数据量不大但需要与分布表频繁关联的公共数据或维度表,例如国家代码、商品分类等。

  • 优势:避免跨节点查询,加速关联操作。

  • 代价:写入操作会同步到所有节点,开销较大,因此不适合频繁变更的数据。

示例:创建一张复制表。

  1. 创建一张普通表t_reference。

    CREATE TABLE t_reference (id int primary key, data text);
  2. 转换为复制表,仅需要指定表名即可。

    SELECT create_reference_table('t_reference');

    返回结果如下:

     create_reference_table 
    ------------------------
     
    (1 row)
  3. 查询复制表的信息,可以看到复制表在所有节点上都有一个同名的分片:

    SELECT table_name, polar_cluster_table_type, distribution_column, shard_count
    FROM polar_cluster_tables
    WHERE table_name = 't_reference'::regclass;

    返回结果如下:

     table_name  | polar_cluster_table_type | distribution_column | shard_count 
    -------------+--------------------------+---------------------+-------------
     t_reference | reference                | <none>              |           1
    (1 row)
    SELECT table_name, shardid, nodename, nodeport
    FROM polar_cluster_shards
    WHERE table_name = 't_reference'::regclass;

    返回结果如下:

     table_name  | shardid |   nodename     | nodeport 
    -------------+---------+----------------+----------
     t_reference |  102096 | 10.xxx.xxx.xxx |     3007
     t_reference |  102096 | 10.xxx.xxx.xxx |     3020
     t_reference |  102096 | 10.xxx.xxx.xxx |     3006
     t_reference |  102096 | 10.xxx.xxx.xxx |     3003
    (4 rows)

将本地表加入元数据管理

通过polar_cluster_add_local_table_to_metadata函数,可以将已有本地表纳入分布式元数据管理,让其他CN也能通过原表名读写该表。数据仍保存在主CN,不会拆分或复制到各DN,也不能通过分片迁移或Rebalance转移到DN。

说明

这不是常见用法,通常应优先选择分布表或复制表。当业务需要保留本地表,同时让其他CN访问时,可以在以下特定场景考虑使用:

  • 保留复杂的跨表触发器逻辑:分布表和复制表可以使用触发器,但触发器中跨表读写若需要访问其他节点,会受到执行限制。若已有复杂触发器难以满足这些条件,可考虑将相关表保留在主CN并一起纳入元数据管理,让触发器中的读写在主CN内完成。

  • 让复制表通过外键引用本地表:被引用的表需要保留在主CN时,可以先将其纳入元数据管理。此类外键的ON DELETE和ON UPDATE仅支持NO ACTION或RESTRICT;复制表不能通过外键引用分布表。

该函数由主CN执行,调用账号需要拥有目标表。以下将已有的public.legacy_table纳入元数据管理:

SELECT polar_cluster_add_local_table_to_metadata('public.legacy_table');

如果表通过外键引用了其他表,或被其他表引用,需要添加cascade_via_foreign_keys := true,连同通过外键直接或间接关联的本地表一起处理;只有自引用外键时不需要。操作会锁定相关表并阻塞其他会话的读写,建议在业务低峰执行。

说明

纳入元数据管理的本地表,自增列同样强烈建议使用BIGSERIAL。即使数据保存在主CN,使用16/32位序列仍可能导致从CN写入失败,详见序列类型差异。

管理分布式表

对于已创建的分布表或复制表,您可以执行以下管理操作。

将分布式表转换回普通表

如果不再需要分布式特性,可以使用undistribute_table函数将分布表或复制表还原为单机表。数据会自动从各分片汇集到主CN节点。

示例:将分布表t转换为普通表。

SELECT undistribute_table('t');

返回结果如下:

NOTICE:  creating a new table for public.t
NOTICE:  moving the data of public.t
NOTICE:  dropping the old public.t
NOTICE:  renaming the new table to public.t
 undistribute_table
--------------------

(1 row)

其他DDL操作

对于已创建的分布表,您可以像操作普通PostgreSQL表一样执行其他DDL操作。这些操作会自动下推到所有物理分片上,以保持逻辑表与物理表结构的一致性。

  • 删除表:DROP TABLE table_name;

  • 创建索引:CREATE INDEX index_name ON table_name (column_name);