自我管理的 PostgreSQL 分区表

Posted

技术标签:

【中文标题】自我管理的 PostgreSQL 分区表【英文标题】:Self-managing PostgreSQL partition tables 【发布时间】:2011-10-25 15:00:19 【问题描述】:

我正在尝试使用 Postgres 进行自我管理的分区表设置。这一切都围绕着这个功能,但我似乎无法让 Postgres 接受我的表名。有什么自管理分区表触发函数的想法或例子?

我目前的功能:

DECLARE
day integer;
year integer;
tablename text;
startdate text;
enddate text;
BEGIN
day:=date_part('doy',to_timestamp(NEW.date));
year:=date_part('year',to_timestamp(NEW.date));
tablename:='pings_'||year||'_'||day||'_'||NEW.id;
-- RAISE EXCEPTION 'tablename=%',tablename;
PERFORM 'tablename' FROM pg_tables WHERE 'schemaname'=tablename;
-- RAISE EXCEPTION 'found=%',FOUND;
IF FOUND <> TRUE THEN
    startdate:=date_part('year',to_timestamp(NEW.date))||'-'||date_part('month',to_timestamp(NEW.date))||'-'||date_part('day',to_timestamp(NEW.date));
    enddate:=startdate::timestamp + INTERVAL '1 day';
    EXECUTE 'CREATE TABLE $1 (
        CHECK ( date >= DATE $2 AND date < DATE $3 )
    ) INHERITS (pings)' USING quote_ident(tablename),startdate,enddate;
END IF;
EXECUTE 'INSERT INTO $1 VALUES (NEW.*)' USING quote_ident(tablename);
RETURN NULL;
END;

我希望它自动创建一个名为 pings_YEAR_DOY_ID 的表,但它总是失败:

2011-10-24 13:39:04 CDT [15804]: [1-1] ERROR:  invalid input syntax for type double precision: "-" at character 45
2011-10-24 13:39:04 CDT [15804]: [2-1] QUERY:  SELECT date_part('year',to_timestamp( $1 ))+'-'+date_part('month',to_timestamp( $2 ))+'-'+date_part('day',to_timestamp( $3 ))
2011-10-24 13:39:04 CDT [15804]: [3-1] CONTEXT:  PL/pgSQL function "ping_partition" line 15 at assignment
2011-10-24 13:39:04 CDT [15804]: [4-1] STATEMENT:  INSERT INTO pings VALUES (0,0,5);

尝试 2

在应用更改并对其进行更多修改之后(日期是 unixtimestamp 列,我的想法是在选择时整数列比时间戳列快)。我收到以下错误,不确定我是否使用了正确的 USING NEW 语法?

更新功能:

CREATE FUNCTION ping_partition() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$DECLARE
day integer;
year integer;
tablename text;
startdate text;
enddate text;
BEGIN
day:=date_part('doy',to_timestamp(NEW.date));
year:=date_part('year',to_timestamp(NEW.date));
tablename:='pings_'||year||'_'||day||'_'||NEW.id;
-- RAISE EXCEPTION 'tablename=%',tablename;
PERFORM 'tablename' FROM pg_tables WHERE 'schemaname'=tablename;
-- RAISE EXCEPTION 'found=%',FOUND;
IF FOUND <> TRUE THEN
    startdate := to_char(to_timestamp(NEW.date), 'YYYY-MM-DD');
    enddate:=startdate::timestamp + INTERVAL '1 day';
    EXECUTE 'CREATE TABLE ' || quote_ident(tablename) || ' (
        CHECK ( date >= EXTRACT(EPOCH FROM DATE ' || quote_literal(startdate) || ')
            AND date < EXTRACT(EPOCH FROM DATE ' || quote_literal(enddate) || ') )
    ) INHERITS (pings)';
END IF;
EXECUTE 'INSERT INTO ' || quote_ident(tablename) || ' SELECT $1' USING NEW; 
RETURN NULL;
END;
$_$;

我的声明:

INSERT INTO pings VALUES (0,0,5);

SQL 错误:

ERROR:  column "date" is of type integer but expression is of type pings
LINE 1: INSERT INTO pings_1969_365_0 SELECT $1
                                            ^
HINT:  You will need to rewrite or cast the expression.
QUERY:  INSERT INTO pings_1969_365_0 SELECT $1
CONTEXT:  PL/pgSQL function "ping_partition" line 22 at EXECUTE statement

【问题讨论】:

为什么没有粘贴函数头?只张贴正文是没有意义的。可能对您的问题至关重要。 抱歉,我的帖子中没有看到 cmets。我在看你的,我的坏,现在更新了。 【参考方案1】:

您正在将date_part() 的double precision 输出与text '-' 混合。这对 PostgreSQL 来说没有意义。您需要对text 进行显式转换。但是有一种更简单的方法可以完成所有这些操作:

startdate:=date_part('year',to_timestamp(NEW.date))
||'-'||date_part('month',to_timestamp(NEW.date))
||'-'||date_part('day',to_timestamp(NEW.date));

改用:

startdate := to_char(NEW.date, 'YYYY-MM-DD');

这也没有意义:

EXECUTE 'CREATE TABLE $1 (
        CHECK (date >= DATE $2 AND date < DATE $3 )
    ) INHERITS (pings)' USING quote_ident(tablename),startdate,enddate;

您只能使用USING 子句提供值。 Read the manual here。试试吧:

EXECUTE 'CREATE TABLE ' || quote_ident(tablename) || ' (
            CHECK ("date" >= ''' || startdate || ''' AND
                   "date" <  ''' || enddate   || '''))
            INHERITS (ping)';

或者更好的是,使用format()。见下文。

另外,像@a_horse answered:您需要将文本值放在单引号中。

这里类似:

<strike>EXECUTE 'INSERT INTO $1 VALUES (NEW.*)' USING quote_ident(tablename);</strike>

改为:

EXECUTE 'INSERT INTO ' || quote_ident(tablename) || ' VALUES ($1.*)'
USING NEW;

相关答案:

How to dynamically use TG_TABLE_NAME in PostgreSQL 8.2?

另外:虽然 PostgreSQL 中的列名允许使用“日期”,但它是 reserved word in every SQL standard。不要将您的列命名为“日期”,这会导致令人困惑的语法错误。

完整的工作演示

CREATE TABLE ping (ping_id integer, the_date date);

CREATE OR REPLACE FUNCTION trg_ping_partition()
  RETURNS trigger AS
$func$
DECLARE
   _tbl text := to_char(NEW.the_date, '"ping_"YYYY_DDD_') || NEW.ping_id;
BEGIN
   IF NOT EXISTS (
      SELECT 1
      FROM   pg_catalog.pg_class c
      JOIN   pg_catalog.pg_namespace n ON n.oid = c.relnamespace
      WHERE  n.nspname = 'public'  -- your schema
      AND    c.relname = _tbl
      AND    c.relkind = 'r') THEN

      EXECUTE format('CREATE TABLE %I (CHECK (the_date >= %L AND
                                              the_date <  %L)) INHERITS (ping)'
              , _tbl
              , to_char(NEW.the_date,     'YYYY-MM-DD')
              , to_char(NEW.the_date + 1, 'YYYY-MM-DD')
              );
   END IF;

   EXECUTE 'INSERT INTO ' || quote_ident(_tbl) || ' VALUES ($1.*)'
   USING NEW; 

   RETURN NULL;
END
$func$ LANGUAGE plpgsql SET search_path = public;

CREATE TRIGGER insbef
BEFORE INSERT ON ping
FOR EACH ROW EXECUTE PROCEDURE trg_ping_partition();

更新: 更高版本的 Postgres 有更优雅的方式来检查表是否存在:

How to check if a table exists in a given schema

to_char() 可以将date 视为$1。这会自动转换为 timestamp。The manual on date / time functions。

(可选)SET the search_path 用于您的职能范围,以避免通过更改 search_path 设置来避免不当行为。

多项其他简化和改进。比较代码。

测试:

INSERT INTO ping VALUES (1, now()::date);
INSERT INTO ping VALUES (2, now()::date);
INSERT INTO ping VALUES (2, now()::date + 1);
INSERT INTO ping VALUES (2, now()::date + 1);

SQL Fiddle.

【讨论】:

感谢您的帮助(我更新了我的帖子)。我认为我对 USING NEW 的语法有一些问题;在执行中,但除了您指出的内容外,我在网上找不到任何好的示例。 @variable:好的,我用完整的解决方案修改了我的分析器。但请仔细阅读 Pavel 的回答。最好提前创建分区表! 我已将您的代码修改为我的最终功能,非常感谢您的帮助。我已经合并了自动删除、索引和 ID。此外,我将日期列作为 bigint 而不是日期,我猜这在 SELECTS 上会更快。谢谢!【参考方案2】:

PostgreSQL 中的动态分区只是一个坏主意。您的代码在多用户环境中不安全。为了安全起见,您必须使用锁,这会减慢执行速度。分区的最佳数量约为一百。您可以轻松地提前创建这么多,以显着简化分区所需的逻辑。

【讨论】:

您能否详细说明为什么它在多用户环境中不安全? (我假设您的意思是“多连接环境”。)这对我来说并不是很明显。 没有锁,就会有竞争条件。您可以有两个并行插入 - IF NOT EXISTS 对两个连接都是正确的,EXECUTE 将在两个连接中处理 - 一个应该失败,一个插入将丢失。可能对于一些高频率的数据而不是重要的数据,它不会是问题,但它仍然是脏代码。【参考方案3】:

您需要将日期文字放在单引号中。目前你正在执行这样的事情:

 CHECK ( date >= DATE 2011-10-25 AND date < DATE 2011-11-25 )

这是无效的。在这种情况下,2011-10-25 被解释为 2011 减 10 减 25

您的代码需要在日期文字周围使用单引号来创建 SQL:

CHECK ( date >= DATE '2011-10-25' AND date < DATE '2011-11-25' )

【讨论】:

【参考方案4】:

我发现了全部内容,效果很好,甚至在 30 天后自动删除。我希望这对未来寻找自动分区触发功能的人有所帮助。

CREATE FUNCTION ping_partition() RETURNS trigger
    LANGUAGE plpgsql
    AS $_$
DECLARE
_keepdate text;
_tablename text;
_startdate text;
_enddate text;
_result record;
BEGIN
_keepdate:=to_char(to_timestamp(NEW.date) - interval '30 days', 'YYYY-MM-DD');
_startdate := to_char(to_timestamp(NEW.date), 'YYYY-MM-DD');
_tablename:='pings_'||NEW.id||'_'||_startdate;
PERFORM 1
FROM   pg_catalog.pg_class c
JOIN   pg_catalog.pg_namespace n ON n.oid = c.relnamespace
WHERE  c.relkind = 'r'
AND    c.relname = _tablename
AND    n.nspname = 'pinglog';
IF NOT FOUND THEN
    _enddate:=_startdate::timestamp + INTERVAL '1 day';
    EXECUTE 'CREATE TABLE pinglog.' || quote_ident(_tablename) || ' (
        CHECK ( date >= EXTRACT(EPOCH FROM DATE ' || quote_literal(_startdate) || ')
            AND date < EXTRACT(EPOCH FROM DATE ' || quote_literal(_enddate) || ')
            AND id = ' || quote_literal(NEW.id) || '
        )
    ) INHERITS (pinglog.pings)';
    EXECUTE 'CREATE INDEX ' || quote_ident(_tablename||'_indx1') || ' ON pinglog.' || quote_ident(_tablename) || ' USING btree (microseconds) WHERE microseconds IS NULL';
    EXECUTE 'CREATE INDEX ' || quote_ident(_tablename||'_indx2') || ' ON pinglog.' || quote_ident(_tablename) || ' USING btree (date, id)';
    EXECUTE 'CREATE INDEX ' || quote_ident(_tablename||'_indx3') || ' ON pinglog.' || quote_ident(_tablename) || ' USING btree (date, id, microseconds) WHERE microseconds IS NULL';
END IF;
EXECUTE 'INSERT INTO ' || quote_ident(_tablename) || ' VALUES ($1.*)' USING NEW;
FOR _result IN SELECT * FROM pg_tables WHERE schemaname='pinglog' LOOP
    IF char_length(substring(_result.tablename from '[0-9-]*$')) <> 0 AND (to_timestamp(NEW.date) - interval '30 days') > to_timestamp(substring(_result.tablename from '[0-9-]*$'),'YYYY-MM-DD') THEN
        -- RAISE EXCEPTION 'timestamp=%,table=%,found=%',to_timestamp(substring(_result.tablename from '[0-9-]*$'),'YYYY-MM-DD'),_result.tablename,char_length(substring(_result.tablename from '[0-9-]*$'));
        -- could have it check for non-existant ids as well, or for archive bit and only delete if the archive bit is not set
        EXECUTE 'DROP TABLE ' || quote_ident(_result.tablename);
    END IF;
END LOOP;
RETURN NULL;
END;
$_$;

【讨论】:

以上是关于自我管理的 PostgreSQL 分区表的主要内容,如果未能解决你的问题,请参考以下文章

产品入门三——产品经理的自我管理能力

自我管理杂谈:用敏捷开发策略做自我管理

[自我管理]时间精力与习惯管理

PM的自我管理

java+jsp基于ssm学生自我个人管理

复盘二: 了解自我和管理自我,诚惶诚恐,保持敬畏-- 宁向东的清华管理学课总结