Надежная стратегия с низким уровнем конкуренции для увеличения столбца счетчика при вставке и обновлении - PullRequest
0 голосов
/ 01 ноября 2019

У меня есть исправительный вопрос относительно последовательностей. Я немного их использовал и ознакомился с документами, и надеюсь, что это легкий вопрос для группы. Сейчас мы на Postgres 11.4 и перейдем к PG 12, когда это будет доступно в RDS.

Цель состоит в том, чтобы набор чисел увеличивался при каждом добавлении или обновлении строки. Мы используем имя поля "con_id" (идентификатор параллелизма) для этого типа счетчика. Таким образом, первый раз, когда строка вставляется в пустую таблицу, значение равно 1, вторая строка получает 2 и т. Д. Звучит как SEQUENCE. У меня была стандартная последовательность в этой роли, затем я переключился на AS IDENTITY ... но теперь понимаю, что это, вероятно, ошибка.

При обновлении счетчик должен продолжать работать. Таким образом, если обновляется первая строка, con_id изменяется с 1 на 3, current max()+1. К сведению, во всех наших обновлениях используется ON CONFLICT(id) SET, а не просто прямая UPDATE.

Смысл числового ряда состоит в том, чтобы определить границы начала-остановки для различных операций:

operation     last_number_processed
sync_to_domo  124556
rollup_day    123516
rollup_week   103456

Затем, когда пришло время выполнить одну из этих операций, все, что вам нужно сделать, чтобы найти правильный блок записей, это выбрать con_id от last_number_processed + 1 до max (con_id). Вы должны обновить трекер операций с этим максимумом (con_id) после завершения операции.

select max(con_id) from source_data; -- Get the current highest number.

В этом случае диапазон будет примерно таким же, как 124557-128923 для "sync_to_domo".

Uniqueness isn 'Т требуется здесь, хотя это желательно. Пробелы вообще не имеют значения. Хранение чисел в последовательности имеет важное значение.

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

И, да, для этой цели можно использовать временную метку,это просто другой вид числовой линии. Причиной целого числа является совпадение с тем, как мы кодируем другие системы. Проще всего объяснить и рассуждать об этом, когда тип данных остается неизменным на разных платформах. Или, похоже, в данном случае.

Тестовый код

Я добавляю некоторый тестовый код и результаты к своему первоначальному вопросу. Это работает, но я сомневаюсь, что это супер эффективно. Тест, приведенный ниже, является минимальной версией и не полезен ... Я просто пытаюсь определить, могу ли я получить постоянно увеличивающуюся число строк вставок и ревизий. Эта попытка выглядит хорошо при быстрой проверке, но я так сильно не усвоил подход Postgres к спорам, что я включаю его главным образом, чтобы люди могли сказать мне, почему это ужасно. Поэтому, пожалуйста, сделайте; -)

Настройка должна иметь последовательность, которая назначается автоматически на INSERT и с помощью каждого ROW триггера на UPDATE. Звучит меньше, чем идея, есть ли лучший способ? Он работает в тесте с одним соединением, но увеличивает счетчики в два раза на UPDATE. Для меня это не проблема, но я не понимаю, почему это происходит.

Вот автономный тестовый код:

DROP TABLE IF EXISTS data.test_con_ids;
DROP SEQUENCE IF EXISTS test_con_ids_sequence;
DROP FUNCTION IF EXISTS data.test_con_ids_update;

BEGIN;
CREATE SEQUENCE data.test_con_ids_sequence 
   AS bigint;

CREATE TABLE data.test_con_ids (
    id integer NOT NULL DEFAULT NULL PRIMARY KEY,
    con_id bigint NOT NULL DEFAULT NEXTVAL ('test_con_ids_sequence'),
    dts timestamptz default NOW()
);


CREATE OR REPLACE FUNCTION data.test_con_ids_update()
 RETURNS trigger
 LANGUAGE plpgsql
AS $function$
BEGIN
    NEW.con_id := NEXTVAL ('test_con_ids_sequence');
 RETURN NEW;
END
$function$;

    -- It's late here, not sure if I could use a FOR EACH STATEMENT trigger here, which should be faster. If it would work.
    CREATE TRIGGER test_con_ids_update_trigger  BEFORE UPDATE ON data.test_con_ids
        FOR EACH ROW EXECUTE PROCEDURE test_con_ids_update();

-- Add ten records, IDs 1-10, con_ids 1-10.     
    INSERT INTO data.test_con_ids (id) 
          VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10);

-- Update rows 1-5 to their current values. The trigger should increment the con_id.   
    INSERT INTO data.test_con_ids (id) 
          VALUES (1),(2),(3),(4),(5)
          ON CONFLICT(id) DO UPDATE set id = EXCLUDED.id; -- Completely pointless, obviously...just a test. We always UPSERT with ON CONFLICT DO UPDATE.

    COMMIT;

И вот результаты:

id  con_id  dts
1   12  2019-11-02 21:52:34.333926+11
2   14  2019-11-02 21:52:34.333926+11
3   16  2019-11-02 21:52:34.333926+11
4   18  2019-11-02 21:52:34.333926+11
5   20  2019-11-02 21:52:34.333926+11
6   6   2019-11-02 21:52:34.333926+11
7   7   2019-11-02 21:52:34.333926+11
8   8   2019-11-02 21:52:34.333926+11
9   9   2019-11-02 21:52:34.333926+11
10  10  2019-11-02 21:52:34.333926+11 

Это работает, 1-10 созданы, 1-5 обновлены и получают приращение значений счетчика con_id. На 2 почему-то (?), Но, по крайней мере, они находятся в полезном порядке, и это то, что нам нужно.

Может кто-нибудь предложить предложения о том, как добиться такого поведения более эффективно? Цель - постоянно увеличивающаяся числовая строка для записей, отражающих последние действия INSERT и UPDATE. И поскольку мы используем целые числа для этого везде, мы пытаемся придерживаться целых чисел вместо временных меток. Но, честно говоря, во многих отношениях это косметика. Другая причина, по которой я смотрю на SEQUENCE, заключается в том, что, если я не понял неправильно, это не связано с транзакцией. Это идеально подходит для этого ... нам не нужен ряд без пробелов, только последовательный.

Тест Postgres 12

Следуя предложению Белайера, я создал экспериментальную базу данных PG 12. Я пошел с настройками по умолчанию, так что все в public. (В реальном мире я убираю public.) Да, сгенерированный столбец, кажется, работает, пока у вас есть неизменяемая функция. Я прочитал о IMMUTABLE в Postgres несколько раз .... и я не понимаю. Поэтому я не могу сказать, что эта функция безопасна. Похоже, так и должно быть. Я следовал шаблонам, используемым в этой достойной статье:

https://www.2ndquadrant.com/en/blog/generated-columns-in-postgresql-12/

CREATE OR REPLACE FUNCTION public.generate_concurrency_id() RETURNS bigint
AS $$
    SELECT EXTRACT(EPOCH FROM clock_timestamp())::bigint; 
$$
LANGUAGE sql IMMUTABLE;

COMMENT ON FUNCTION public.generate_concurrency_id() IS 'Generate a bigint to act as a progressive change counter for a table.';

DROP TABLE IF EXISTS public.test_con_ids;
CREATE TABLE test_con_ids (
    id integer NOT NULL DEFAULT NULL PRIMARY KEY,
    con_id bigint GENERATED ALWAYS AS (generate_concurrency_id()) STORED,
    dts timestamptz default NOW()
);

-- Add ten records, IDs 1-10, con_ids 1-10.     
    INSERT INTO public.test_con_ids (id) 
          VALUES (1),(2),(3),(4),(5),(6),(7),(8),(9),(10);

-- Unless you wait here, the con_ids all look the same...this operation is way too short to tick over to another second, unless you get lucky.
-- Update rows 1-5 to their current values. The trigger should increment the con_id.   
    INSERT INTO public.test_con_ids (id) 
          VALUES (1),(2),(3),(4),(5)
          ON CONFLICT(id) DO UPDATE set id = EXCLUDED.id; -- Completely pointless, obviously...just a test. We always UPSERT with ON CONFLICT DO UPDATE.

Пример выше работает с сгенерированным столбцом, но я не знаю о характеристиках производительности PG12 вычисленных столбцов против триггеров.

На более грустной ноте, я думаю, что это может не сработать для меня вообще. Мне действительно нужна метка времени фиксации, которая доступна только через логическое декодирование. Вот почему, на примере.

  • Я суммирую данные и хочу получить необработанные обновленные или вставленные строки.
  • Последнее число, которое я получил от записи, добавленной в 01:00:00Теперь я хочу получить все старшие числа.
  • Хорошо ... Я делаю это.
  • Ой, подождите, незавершенная транзакция приходит и фиксирует временные метки / производные числа от более ранних.

A SEQUENCE здесь тоже не работает, потому что, опять же, строка / кортеж не видна моему процессу сборщика, пока транзакция не зафиксирована.

Так что я думаю, что это логическое декодирование, сводная таблица обновлений или перебор.

1 Ответ

0 голосов
/ 05 ноября 2019

Грустная и ужасная правда

Я понял, что не знаю, как совершать транзакции, и что ни одна из этих схем не может работать. Это не только не работает для UPDATE, но и для INSERT, что немного удивительно. Если у меня здесь что-то не так, скажите, пожалуйста, где.

В качестве мысленного эксперимента представьте три одновременных незафиксированных транзакции в таблице, в которой число concurrency_id увеличивается на INSERT или UPDATE. Вот простая картина.

enter image description here

Теперь, что такое max(concurrency_id) в таблице? Это бессмысленно, поскольку ни одна из транзакций еще не совершена. Если вы используете sequence, вы можете взять nextval, а затем узнать, что все, что с меньшим числом, «раньше». Хорошо, представьте, что я использую max в производстве, что я получу? и зависит от того, когда я спрашиваю, и в каком порядке / состоянии находятся транзакции. Вот небольшая таблица истинности с результатами для max() и nextval() для различных комбинаций:

Scenario    T1            T2            T3            Nextval()   Max()
1           Committed     Committed     Committed     16             15
2           Committed     Committed     Open          16             10
3           Committed     Open          Committed     16             15
4           Committed     Open          Open          16              5
5           Open          Committed     Committed     16             15
6           Open          Committed     Open          16             10
7           Open          Open          Committed     16             15
8           Open          Open          Open          16              0

Вся цель здесь - установить маркеры на числовой линии (или временной шкале), чтобы выделить уже обработанные строки. Обработанный статус не является свойством строк, так как одни и те же строки могут использоваться во многих операциях. Например, множественные сведения, синхронизация с различными источниками, архивирование и т. Д.

Если я хочу знать, что обрабатывать, мне нужно последнее обработанное значение, 0 / NULL в начале и максимальное значение для обработки. Если я запускаю проверку в сценариях 1, 3, 5 или 7, я получаю одинаковое максимальное значение 15. Однако зафиксированные записи в этой точке меняются

Scenario    T1            T2            T3            Nextval()   Max()    Values
1           Committed     Committed     Committed     16             15    1-15
3           Committed     Open          Committed     16             15    1-5 and 11-15
5           Open          Committed     Committed     16             15    6-15
7           Open          Open          Committed     16             15    11-15

В каждом случае окно обработкиТрекер хранит максимальное значение, 15 в каждом случае. Пока вреда нет. Но что происходит, когда я в следующий раз запускаю проверку и хочу найти все необработанные строки в следующий раз? Они смотрят, глядя на> 16. Чистый результат в том, что работает только сценарий 1, все остальные заканчиваются записями с более ранними числами / временными метками, фиксирующими после Я выполнил свою проверку, и поэтому эти числа никогда не идутбыть захваченным. Насколько я могу судить, использование последовательности не может помочь. Хотя последовательность не привязана к транзакции, строка , которую она представляет, привязана к транзакции. И временные метки не помогают, так как нет доступной функции timestamp-in-фактически-commit-order.

Я думаю, что единственное решение (как мне кажется, другие уже говорили мне в другом месте) - это логическое декодирование или репликация. Разница в том, что поток репликации гарантированно находится в режиме воспроизведения, в порядке фиксации.

Примечания выше представляют собой мысленный эксперимент (иш). Если Св. Дейкстра дал нам что-нибудь, это то, что мы можем и должны использовать мысленные эксперименты, чтобы рассуждать о проблемах параллелизма. Если я ошибся, что-то упустил и т. Д., Пожалуйста, ответьте или прокомментируйте.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...