Многоуровневое разбиение PostgreSQL

Я использую секционирование с базой данных postgreSQL некоторое время. Моя база данных довольно сильно выросла и прекрасно справляется с секционированием. К сожалению, теперь я, кажется, столкнулся с еще одним барьером в скорости и пытаюсь найти способы еще больше ускорить работу базы данных.

Мои базовые установки таковы: у меня есть одна основная таблица с именем database_data, от которой наследуются все разделы. Я решил иметь один раздел в месяц и назвал их так: database_data_YYYY_MM, что прекрасно работает.

Анализируя использование данных, я заметил, что в основном я выполняю операции вставки в таблицу и только некоторые обновления. Однако обновления также происходят только в строке определенного типа: у меня есть столбец с именем channel_id (FK для другой таблицы). Обновляемые строки всегда имеют channel_id из набора, возможно, 50 идентификаторов, поэтому это был бы отличный способ отличить строки, которые никогда не обновляются, от тех, которые потенциально обновляются.

Я полагал, что моя установка ускорится еще больше, если я буду использовать разделение, чтобы иметь одну таблицу с данными только для вставки и одну с потенциально обновляемыми данными в месяц, поскольку мои обновления каждый раз нужно проверять меньше строк.

Конечно, я мог бы использовать «простое» разделение, которое я использую сейчас, и добавить еще одну таблицу для каждого месяца, называемую database_data_YYYY_MM_update, и добавить специальные ограничения к ней и таблице database_data_YYYY_MM, чтобы планировщик запросов различал таблицы.

Однако я думал, что иногда у меня есть операции, которые работают со всеми данными за данный месяц, независимо от того, подлежат ли они обновлению или нет. В таком случае я мог бы ПРИСОЕДИНИТЬСЯ к двум таблицам, но для таких запросов может быть более простой способ.

Итак, теперь к моему реальному вопросу:

Возможно ли «двухслойное» разбиение в PostgreSQL? Под этим я подразумеваю, что вместо двух таблиц на каждый месяц, наследуемых от главной таблицы, у меня будет только одна таблица в месяц, напрямую наследуемая от главной таблицы, например. database_data_YYYY_MM, а затем иметь еще две таблицы, наследующие от этой таблицы, одну для вставки только данных, например. database_data_YYYY_MM_insert и один для обновляемых данных, например. database_data_YYYY_MM_update.

Ускорит ли это планирование запросов? Я предполагаю, что было бы быстрее, если бы планировщик запросов мог удалить обе таблицы одновременно, если промежуточная таблица была удалена.

Очевидным преимуществом здесь было бы то, что я мог бы работать со всеми данными за один месяц, просто используя таблицу database_data_YYYY_MM, а для своих обновлений напрямую использовать дочернюю таблицу.

Есть ли недостатки, о которых я не думаю?

Спасибо за ваши мысли.


Редактировать 1:

Я не думаю, что схема строго необходима для ответа на мой вопрос, но если это поможет понять, я приведу образец схемы:

CREATE TABLE database_data (
    id bigint PRIMARY KEY,
    channel_id bigint,    -- This is a FK to another table
    timestamp TIMESTAMP WITH TIME ZONE,
    value DOUBLE PRECISION
)

У меня есть триггер в таблице database_data, который генерирует разделы по запросу:

CREATE OR REPLACE FUNCTION function_insert_database_data() RETURNS TRIGGER AS $BODY$
DECLARE
    thistablename TEXT;
    thisyear INTEGER;
    thismonth INTEGER;
    nextmonth INTEGER;
    nextyear INTEGER;
BEGIN
    -- determine year and month of timestamp
    thismonth = extract(month from NEW.timestamp AT TIME ZONE 'UTC');
    thisyear = extract(year from NEW.timestamp AT TIME ZONE 'UTC');

    -- determine next month for timespan in check constraint
    nextyear = thisyear;
    nextmonth = thismonth + 1;
    if (nextmonth >= 13) THEN
        nextmonth = nextmonth - 12;
        nextyear = nextyear +1;
    END IF;

    -- Assemble the tablename

    thistablename = 'database_datanew_' || thisyear || '_' || thismonth;

    -- We are looping until it's successfull to catch the case when another connection simultaneously creates the table
    -- if that would be the case, we can retry inserting the data
    LOOP
        -- try to insert into table
        BEGIN
            EXECUTE 'INSERT INTO ' || quote_ident(thistablename) || ' SELECT ($1).*' USING NEW;
            -- Return NEW inserts the data into the main table allowing insert statements to return the values like "INSERT INTO ... RETURNING *"
            -- This requires us to use another trigger to delete the data again afterwards
            RETURN NEW;
        -- If the table does not exist, create it
        EXCEPTION
            WHEN UNDEFINED_TABLE THEN
                BEGIN
                    -- Create table with check constraint on timestamp
                    EXECUTE 'CREATE TABLE ' || thistablename || ' (CHECK ( timestamp >= TIMESTAMP WITH TIME ZONE '''|| thisyear || '-'|| thismonth ||'-01 00:00:00+00''
                        AND timestamp < TIMESTAMP WITH TIME ZONE '''|| nextyear || '-'|| nextmonth ||'-01 00:00:00+00'' ), PRIMARY KEY (id)
                        ) INHERITS (database_data)';
                    -- Add any trigger and indices to the table you might need
                    -- Insert the new data into the new table
                    EXECUTE 'INSERT INTO ' || quote_ident(thistablename) || ' SELECT ($1).*' USING NEW;
                    RETURN NEW;
                EXCEPTION WHEN DUPLICATE_TABLE THEN
                    -- another thread seems to have created the table already. Simply loop again.
                END;
            -- Don't insert anything on other errors
            WHEN OTHERS THEN
                RETURN NULL;
        END;
    END LOOP;
END;
$BODY$
LANGUAGE plpgsql;

CREATE TRIGGER trigger_insert_database_data
BEFORE INSERT ON database_data
FOR EACH ROW EXECUTE PROCEDURE function_insert_database_data();

Что касается выборочных данных: предположим, что у нас есть только два канала: 1 и 2. 1 предназначен только для вставки данных, а 2 может обновляться.

Мой двухуровневый подход будет выглядеть примерно так:

Основная таблица:

CREATE TABLE database_data (
    id bigint PRIMARY KEY,
    channel_id bigint,    -- This is a FK to another table
    timestamp TIMESTAMP WITH TIME ZONE,
    value DOUBLE PRECISION
)

Промежуточная таблица:

CREATE TABLE database_data_2015_11 (
    (CHECK ( timestamp >= TIMESTAMP WITH TIME ZONE '2015-11-01 00:00:00+00' AND timestamp < TIMESTAMP WITH TIME ZONE '2015-12-01 00:00:00+00)),
    PRIMARY KEY (id)
) INHERITS(database_data);

Разделы:

CREATE TABLE database_data_2015_11_insert (
    (CHECK (channel_id = 1)),
    PRIMARY KEY (id)
) INHERITS(database_data_2015_11);

CREATE TABLE database_data_2015_11_update (
    (CHECK (channel_id = 2)),
    PRIMARY KEY (id)
) INHERITS(database_data_2015_11);

Конечно, тогда мне понадобится еще один триггер для промежуточной таблицы, чтобы создавать дочерние таблицы по требованию.


person Tim    schedule 19.11.2015    source источник
comment
Без кода, схемы и примеров данных это становится не по теме.   -  person Clodoaldo Neto    schedule 19.11.2015


Ответы (1)


Это умная идея, но, к сожалению, она не работает. Если у меня есть родительская таблица с 1000 непосредственными дочерними элементами, и я запускаю SELECT, который должен получать данные только из одного дочернего элемента, то explain analyze дает мне время планирования около 16 мс. С другой стороны, если у меня всего 10 прямых потомков, и у всех по 10 потомков, и у всех по 10 потомков, я получаю время планирования запроса около 29 мс. Я был удивлен --- я действительно думал, что это сработает!

Вот код ruby, который я использовал для создания своих таблиц:

0.upto(999) do |i|
  if i % 100 == 0
    min_group_id = i
    max_group_id = min_group_id + 100
    puts "CREATE TABLE datapoints_#{i}c (check (group_id > #{min_group_id} and group_id <= #{max_group_id})) inherits (datapoints);"
  end
  if i % 10 == 0
    min_group_id = i
    max_group_id = min_group_id + 10
    puts "CREATE TABLE datapoints_#{i}x (check (group_id > #{min_group_id} and group_id <= #{max_group_id})) inherits (datapoints_#{i / 100 * 100}c);"
  end
  puts "CREATE TABLE datapoints_#{i + 1} (check (group_id = #{i + 1})) inherits (datapoints_#{i / 10 * 10}x);"
end
person Paul A Jungwirth    schedule 04.05.2017