关于如何处理PostgreSQL 9中的分区的简要分析

PostgreSQL 10于2017年10月初发布,大约一年前。

最有趣的新“特性”之一是无条件的声明式分区。但是如果你不急于升级到10呢?例如,亚马逊不着急,并引入了对PostgreSQL10仅在2018年2月的最后几天。

然后,通过继承来拯救好的奥尔的划分。我是出租车公司财务部的软件架构师,所以所有的例子都会以某种方式与旅行有关(钱的问题将留待下次使用)。

自从我们在2015年开始重写我们的金融体系以来,就没有建立宣示性政党的问题了。因此,我们成功地使用了下面描述的技术。

撰写本文的最初原因是,我在PostgreSQL中遇到的大多数分区示例都是非常基本的。这是表,这是我们正在查看的一列,甚至可能预先知道它包含哪些值。似乎一切都很简单。但现实生活会做出自己的调整。

在我们的示例中,我们将表划分为两列,其中一列包含旅行日期。我们会考虑这个问题。

让我们从表的样子开始:

 
create table rides (
 
 id bigserial not null primary key,
 
 tenant_id varchar(20) not null,
 
 ride_id varchar(36) not null,
 
 created_at timestamp with time zone not null,
 
 metadata jsonb
 
 -- Probably more columns and indexes coming here
 
);
 

对于每个租户,该表每月包含数百万行。幸运的是,租户之间的数据从不相交,最困难的请求是在一两个月结束时提出的。

  • 对于那些没有深入研究如何在PostgreSQL中运行分区的人(来自Oracle的幸运儿,你好!)我将简要介绍这一过程。

PostgreSQL依赖于它的三个“特性”:

  • 继承表的能力,
  • 表继承,
  • 检查条件。

让我们从继承开始。使用Inherits关键字,我们指定创建的表继承表的所有字段。这还创建了两个表之间的关系:通过从父表查询,我们还从子表获取所有数据。

检查条件是对图像的补充,因为它们保证了数据的不交集。因此,PostgreSQL优化器可以根据查询中的数据切断部分子表。

  • 这种方法的第一个水下岩石看起来很明显:任何请求都必须包含Tenant_id。但是,如果您不经常提醒自己,那么迟早您会编写自定义SQL本身,在它中,您会忘记指示这个Tenant_id。因此,扫描所有分区和一个不起作用的数据库.

但回到我们想要达到的目标。在申请方面,我希望有透明度。我们总是在同一个表中写入,并且数据库已经选择了将这些数据放在哪里。

为此,我们使用以下存储过程:

 
CREATE OR REPLACE FUNCTION insert_row()
 
 RETURNS TRIGGER AS
 
$BODY$
 
DECLARE
 
 partition_env TEXT;
 
 partition_date TIMESTAMP;
 
 partition_name TEXT;
 
 sql TEXT;
 
BEGIN
 
 -- construct partition name
 
 partition_env := lower(NEW.tenant_id);
 
 partition_date := date_trunc('month', NEW.created_at AT TIME ZONE 'UTC');
 
 partition_name := format('%s_%s_%s', TG_TABLE_NAME, partition_env, to_char(partition_date, 'YYYY_MM'));
 
 
 -- create partition, if necessary
 
 IF NOT EXISTS(SELECT relname FROM pg_class WHERE relname = partition_name) THEN
 
   PERFORM create_new_partition(TG_TABLE_NAME, NEW.tenant_id, partition_date, partition_name);
 
 END IF;
 
 
 select format('INSERT INTO %s values ($1.*)', partition_name) into sql;
 
 -- Propagate insert
 
 EXECUTE sql USING NEW;
 
 RETURN NEW; -- RETURN NULL; if no ORM
 
END;
 
$BODY$
 
 
LANGUAGE plpgsql;
 

您应该注意的第一件事是使用TG_TABLE_NAME。因为这是一个触发器,PostgreSQL为我们填充了很多变量,我们可以处理这些变量。可以找到完整的列表。这里.

在本例中,我们希望获得触发器的表的父表的名称。在我们的情况下,这将是骑马。我们在几个微服务中使用了类似的方法,并且这个部分几乎可以不变地传输。

  • 表演如果我们想要调用一个不返回任何内容的函数,则非常有用。通常,在示例中,所有的逻辑都试图放入一个函数中,但我们要小心。
  • 使用新指示在此查询中,我们使用尝试添加的字符串中的值。
  • $ 1. *将展开新行的所有值。事实上,这可以翻译成新的。翻译成NEW.ID,NEW.TENANT_ID,.。

下面的过程,我们称之为执行,创建一个新的分区,如果它还不存在。每个租户每隔一段时间就会发生一次这样的情况。

 
CREATE OR REPLACE FUNCTION create_new_partition(parent_table_name text,
 
                                           env text,
 
                                           partition_date timestamp,
 
                                           partition_name text) RETURNS VOID AS
 
$BODY$
 
DECLARE
 
 sql text;
 
BEGIN
 
 -- Notifying
 
 RAISE NOTICE 'A new % partition will be created: %', parent_table_name, partition_name;
 
 
 select format('CREATE TABLE IF NOT EXISTS %s (CHECK (
 
         tenant_id = ''%s'' AND
 
         created_at AT TIME ZONE ''UTC'' > ''%s'' AND
 
         created_at AT TIME ZONE ''UTC'' <= ''%s''))
 
         INHERITS (%I)', partition_name, env, partition_date,
 
               partition_date + interval '1 month', parent_table_name) into sql;
 
 -- New table, inherited from a master one
 
 EXECUTE sql;
 
 PERFORM index_partition(partition_name);
 
END;
 
$BODY$
 
LANGUAGE plpgsql;
 

如前所述,我们使用继承创建类似于父级和查帐来确定应该有哪些数据。

  • 提出通知只需在控制台中打印一个字符串。如果现在从PLSQL运行INSERT,我们可以看到是否创建了分区。

我们有新问题了。继承不继承索引。为了做到这一点,我们有两个解决方案:

使用继承创建索引:

使用创建表,如,然后ALTERTABLE继承

或按程序创建索引:

 
CREATE OR REPLACE FUNCTION index_partition(partition_name text) RETURNS VOID AS
 
$BODY$
 
BEGIN
 
 -- Ensure we have all the necessary indices in this partition;
 
 EXECUTE 'CREATE INDEX IF NOT EXISTS ' || partition_name || '_tenant_timezone_idx ON ' || partition_name || ' (tenant_id, timezone(''UTC''::text, created_at))';
 
 -- More indexes here...
 
END;
 
$BODY$
 
LANGUAGE plpgsql;
 

不要忘记子表索引是非常重要的,因为即使在对每个表进行分区之后,也会有数百万行。在我们的情况下,不需要父类的索引,因为父类始终是空的。

最后,我们创建一个触发器,在创建新行时调用该触发器:

 
CREATE TRIGGER before_insert_row_trigger
 
BEFORE INSERT ON rides
 
FOR EACH ROW EXECUTE PROCEDURE insert_row();
 

这里还有另一个微妙之处,很少强调注意。分区最好由数据不变的列来完成。在我们的例子中,这是可行的:Trip从不更改Tenant_id并创建_at。如果不是这样,就会出现问题-PostgreSQL不会将一些数据返回给我们。然后我们向他保证查帐所有的数据都是有效的。

有几种解决方案(除了显而易见的解决方案-不要更改分区上的数据):

  • 而不是在应用程序级别进行更新,我们总是这样做。删除+插入

我们在更新,它将数据传输到正确的分区。

  • 另一个值得考虑的细微差别是如何正确地索引包含日期的列。如果我们用时区在查询中,不要忘记它实际上是一个函数调用。因此,我们的索引也应该是基于函数的。我忘了。因此,再一次,由于负载而死亡的基地。

最后一个需要考虑的方面是分区如何与不同的ORM框架,无论是Ruby中的ActiveRecord还是戈姆在……里面.

PostgreSQL中的分区依赖于父表始终为空的事实。如果你不使用奥姆,您可以安全地返回到第一个存储过程,并更改回归新;上返回空然后,根本不添加父表中的行,这实际上是我们想要的。

但事实是大多数Orms使用插入“eRETURNING”条款。如果我们从触发器中返回NULL,ORM会感到恐慌,因为我们认为字符串没有被添加。它是添加的,但不是ORM看上去的地方。

有几种方法可以解决这个问题:

  • 不要将ORM用于插入
  • 修补ORM(有时建议使用ActiveRecord)
  • 添加另一个触发器,它将从父触发器中删除字符串。

最后一个选项是不可取的,因为对于每个操作,我们将执行三个操作。不过,我们会分别考虑:

 
CREATE OR REPLACE FUNCTION delete_parent_row()
 
 RETURNS TRIGGER AS
 
$BODY$
 
DECLARE
 
BEGIN
 
 delete from only rides where id = NEW.ID;
 
 RETURN null;
 
END;
 
$BODY$
 
LANGUAGE plpgsql;
 
 
CREATE TRIGGER after_insert_row_trigger
 
AFTER INSERT ON rides
 
FOR EACH ROW EXECUTE PROCEDURE delete_parent_row();
 

剩下的最后一件事就是检验我们的决定。为此,我们生成一定数量的行:

 
DO
 
$script$
 
DECLARE
 
 year_start_epoch bigint := extract(epoch from '20170101'::timestamptz at time zone 'UTC');
 
 delta bigint := extract(epoch from '20171231 23:59:59'::timestamptz at time zone 'UTC') - year_start_epoch;
 
 tenant varchar;
 
 tenants varchar[] := array['tenant_a', 'tenant_b', 'tenant_c', 'tenant_d'];
 
BEGIN
 
 FOREACH tenant IN ARRAY tenants LOOP
 
   FOR i IN 1..100000 LOOP
 
     insert into rides (tenant_id, created_at, ride_id)
 
     values (tenant, to_timestamp(random() * delta + year_start_epoch) at time zone 'UTC', i);
 
   END LOOP;
 
 END LOOP;
 
END
 
$script$;
 

让我们看看数据库的行为:

 
explain select *
 
from rides
 
where tenant_id = 'tenant_a'
 
and created_at AT TIME ZONE 'UTC' > '20171102'
 
and created_at AT TIME ZONE 'UTC' <= '20171103';
 

如果一切顺利,我们应该看到以下结果:

 
 Append  (cost=0.00..4803.76 rows=4 width=196)
 
   ->  Seq Scan on rides  (cost=0.00..4795.46 rows=3 width=196)
 
         Filter: (((created_at)::timestamp without time zone > '2017-11-02 00:00:00'::timestamp without time zone) AND ((created_at)::timestamp without time zone <= '2017-11-03 00:00:00'::timestamp without time zone) AND ((tenant_id)::text = 'tenant_a'::text))
 
   ->  Index Scan using rides_tenant_a_2017_11_tenant_timezone_idx on rides_tenant_a_2017_11  (cost=0.28..8.30 rows=1 width=196)
 
         Index Cond: (((tenant_id)::text = 'tenant_a'::text) AND ((created_at)::timestamp without time zone > '2017-11-02 00:00:00'::timestamp without time zone) AND ((created_at)::timestamp without time zone <= '2017-11-03 00:00:00'::timestamp without time zone))
 
(5 rows)
 

尽管每个租户都有10万行,但我们只从所需的数据片段中进行选择。成功!

我希望这篇文章对那些不熟悉分区是什么以及如何在PostgreSQL中实现的人来说是有趣的。在评论部分让我知道你的想法。

猜你喜欢

转载自www.cnblogs.com/ybyqi/p/9836724.html