Самоуправляемые таблицы разделов PostgreSQL

Я пытаюсь настроить самоуправляемую таблицу разделов с помощью 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

person ehiller    schedule 25.10.2011    source источник
comment
почему вы не вставили заголовок функции? Нет смысла выкладывать только тело. Вероятно, имеет решающее значение для вашей проблемы.   -  person Erwin Brandstetter    schedule 25.10.2011
comment
Извините, не увидел комментариев к моему посту. Смотрел на твой, мой плохой, сейчас обновил.   -  person ehiller    schedule 25.10.2011


Ответы (4)


Вы смешиваете double precision вывод date_part() с 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. Читайте руководство здесь . Вместо этого попробуйте:

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

Или, что еще лучше, используйте format(). . Смотри ниже.

Кроме того, например, @a_horse ответил: вам нужно поместить текстовые значения в одинарные кавычки.

Аналогично здесь:

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

Вместо:

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

Связанный ответ:

В стороне: хотя «дата» разрешена для имени столбца в PostgreSQL, это зарезервированное слово в каждом стандарте SQL. Не называйте свой столбец «дата», это приводит к путанице синтаксических ошибок.

Полная рабочая демонстрация

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();

Тесты:

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.

person Erwin Brandstetter    schedule 25.10.2011
comment
Спасибо, это очень помогло (и я обновил свой пост). Я думаю, что у меня есть некоторые проблемы с синтаксисом USING NEW; в EXECUTE, но я не могу найти в Интернете хороших примеров, кроме тех, на которые вы мне указали. - person ehiller; 25.10.2011
comment
@variable: Хорошо, я добавил в свой ответ полное решение. Но внимательно прочитайте ответ Павла. Лучше создать таблицы разделов заранее! - person Erwin Brandstetter; 26.10.2011
comment
Я изменил ваш код на свою последнюю функцию, большое спасибо за помощь. У меня есть автоматическое удаление, индексация и идентификаторы. Также у меня есть столбец даты как bigint, а не как дата, я догадался, что это будет быстрее при SELECTS. Спасибо! - person ehiller; 26.10.2011

Динамическое секционирование в PostgreSQL — просто плохая идея. Ваш код небезопасен в многопользовательской среде. Чтобы это было безопасно, вам придется использовать блокировки, которые замедляют выполнение. Оптимальное количество разделов около ста. Вы можете легко создать это количество заблаговременно, чтобы значительно упростить логику, необходимую для разделения.

person Pavel Stehule    schedule 25.10.2011
comment
Не могли бы вы объяснить, почему это небезопасно в многопользовательской среде? (Я предполагаю, что вы имеете в виду среду с несколькими подключениями.) Это не сразу очевидно для меня. - person jpmc26; 20.05.2017
comment
Без блокировок есть условия гонки. Вы можете иметь две параллельные вставки - IF NOT EXISTS верно для обоих соединений, а EXECUTE будет обрабатываться в обоих соединениях - одно должно быть неудачным, а одно вставка будет потеряно. Возможно для каких-то высоко частых данных и не важных данных это не может быть выдано, но все же это грязный код. - person Pavel Stehule; 20.05.2017

Вам нужно поместить литералы даты в одинарные кавычки. В настоящее время вы выполняете что-то вроде этого:

 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' )
person a_horse_with_no_name    schedule 25.10.2011

Я разобрался со всем, и это прекрасно работает, даже есть автоматическое удаление через 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;
$_$;
person ehiller    schedule 25.10.2011