Храните только последние 5 строк на объект в таблице аудита - PullRequest
0 голосов
/ 23 февраля 2019

У меня есть веб-приложение, поддерживаемое базой данных Postgres (v11) и главной таблицей, где каждая строка в таблице может рассматриваться как объект, а каждый столбец - это поле объекта.

Итак, мыhave:

| id | name | field1 | field2| .... | field 100|
-----------------------------------------------
| 1  | foo  | 12.2   | blue  | .... | 13.7     |
| 2  | bar  | 22.1   | green | .... | 78.0     |

Таблица была создана с использованием:

CREATE TABLE records(
  id VARCHAR(50) PRIMARY KEY,
  name VARCHAR(50),
  field1 NUMERIC,
  field2 VARCHAR(355),
  field100 NUMERIC);

Теперь у меня есть таблица аудита, в которой хранятся обновления для каждого поля каждого объекта.Таблица аудита определяется следующим образом:

| timestamp | objid | fieldname | oldval | newval | 
-----------------------------------------------
| 1234      | 1     | field2    | white  | blue   |
| 1367      | 1     | field1    | "11.5" | "12.2" |
| 1372      | 2     | field1    | "11.9" | "22.1" |
| 1387      | 1     | name      | baz    | foo    |

Таблица была создана с использованием:

CREATE TABLE audit_log(
  timestamp TIMESTAMP,
  objid VARCHAR (50) REFERENCES records(id),
  fieldname VARCHAR (50) NOT NULL,
  oldval VARCHAR(355),
  newval  VARCHAR(355));

oldval / newval сохраняются как varchar, поскольку они предназначены исключительно для аудитацель, поэтому фактический тип данных на самом деле не имеет значения.

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

Я понимаю, что вы можете получить это, используя GROUP BY и LIMIT, но проблема в том, что у меня более миллиона объектов, причем некоторые из них были обновлены более тысячи раз, в то время как другие имеют лишь несколько обновлений за несколько раз.года.И журнал аудита очень тяжел для чтения / записи (как и следовало ожидать).

Каков наилучший способ удаления всех записей, которые старше 5-го последнего обновления для каждого объекта (конечно, в идеале я перенесу это в какое-нибудь дополнительное хранилище)?

Ответы [ 3 ]

0 голосов
/ 23 февраля 2019

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

Сначала создайте новую таблицу на лету, выбрав записи, которыеВы хотите сохранить, используя синтаксис CREATE TABLE AS .Аналитические функции упрощают выбор записей.

CREATE TABLE audit_log_backup AS
SELECT mycol1, mycol2, ... 
FROM (
    SELECT a.*, ROW_NUMBER() OVER(PARTITION BY objid ORDER BY timestamp DESC) rn
    FROM audit_log a
) x WHERE rn <= 5

Затем просто TRUNCATE исходная таблица и повторная вставка сохраненных данных:

TRUNCATE audit_log;
INSERT INTO audit_log SELECT * FROM audit_log_backup;
--- and eventually...
DROP TABLE audit_log_backup;

Как объяснено в документации, усечение большой таблицы намного эффективнее, чем удаление из нее:

TRUNCATE быстро удаляет все строки из набора таблиц.Он имеет тот же эффект, что и неквалифицированный DELETE для каждой таблицы, но поскольку он на самом деле не сканирует таблицы, он работает быстрее.Кроме того, он немедленно восстанавливает дисковое пространство, а не требует последующей операции VACUUM.Это наиболее полезно для больших таблиц.

Одна вещь, о которой следует быть осторожным, как прокомментировал Erwin Brandsetter , заключается в том, что этот метод создает состояние гонки, при котором записи добавляются (или обновляются).) после запуска копирования не будет учитываться.Одним из решений было бы выполнение всех операций в одной транзакции, в то время как блокировка таблицы :

BEGIN WORK;
LOCK TABLE audit_log IN SHARE ROW EXCLUSIVE MODE;
CREATE TABLE audit_log_backup AS ...;
TRUNCATE audit_log;
INSERT INTO audit_log SELECT * FROM audit_log_backup;
COMMIT WORK;

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


Отказ от ответственности: что бы вы ни делали, убедитесь, что правильно сделали резервную копию всей таблицы, прежде чем начинать ее чистку!

0 голосов
/ 23 февраля 2019

Вы можете использовать простой row_number(), аналогичный , который @Willis предложил , улучшенный с ORDER BY:

WITH cte AS (
    SELECT ctid
         , row_number() OVER (PARTITION BY objid ORDER BY timestamp DESC) AS rn
    FROM   audit_log
   )
DELETE FROM audit_log
USING  cte
WHERE  cte.ctid = tbl.ctid
AND    cte.row_number > 5;

Это займет длинный время для вашего большого стола.Это может быть быстрее с многоколоночным индексом на audit_log(objid, timestamp DESC) и запросом:

WITH del AS (
   SELECT x.ctid
   FROM   records r
   CROSS LATERAL (
      SELECT a.ctid
      FROM   audit_log a
      WHERE  a.objid = r.id
      ORDER  BY a.timestamp DESC
      OFFSET 5  -- excluding the first 5 per object
      ) x
   )
DELETE FROM audit_log
USING  del
WHERE  del.ctid = tbl.ctid;

Или:

DELETE FROM audit_log
WHERE  ctid NOT IN (
   SELECT x.ctid
   FROM   records r
   CROSS  JOIN LATERAL (
      SELECT a.ctid
      FROM   audit_log a
      WHERE  a.objid = r.id
      ORDER  BY a.timestamp DESC
      LIMIT  5  -- the inverse selection here
      ) x
   );

Последний может быть быстрее с поддерживающим индексом.

Похожие:

Запись новой таблицы, содержащей только первые 5 для каждого объекта, будет намного быстрее.Вы можете использовать подзапрос из последнего запроса для этого.(И см. ответ GMB .) Он создает первозданную таблицу без раздувания.Но я исключил это из-за того, что таблица very read/write heavy.Если вы не можете позволить себе необходимую эксклюзивную блокировку в течение некоторого времени, это не разрешено.

Ваш столбец timestamp не определен NOT NULL.Вам может понадобиться NULLS LAST.См .:

0 голосов
/ 23 февраля 2019

В решении есть несколько компонентов:

  • Функция PostgreSQL row_number.К сожалению, это «оконная функция», и ее нельзя использовать в предложении where.
  • Общее табличное выражение (CTE): "с T as (... некоторый SQL ...) ...сделать что-нибудь с T ... "
  • Поле PostgreSQL ctid, которое однозначно идентифицирует строку в таблице.

Вы используете CTE для создания логической таблицы, которая включает в себяоба ctid и row_number.Затем вы ссылаетесь на это из заявления об удалении.Примерно так:

with t as (
    select ctid, row_number() over (partition by objid)
    from the_audit_table
)
delete from the_audit_table
where ctid in (select ctid from t where row_number > 5)

Если вас беспокоит эффект от одновременного выполнения всего этого, то просто запустите множество небольших транзакций в некотором подмножестве пространства objid.Или (если вы собираетесь удалить 99% строк), создайте новую таблицу, замените row_number > 5 на row_number <= 5 и вставьте ее в новую таблицу, а затем замените старую таблицу новой..

Сначала проверьте в QA!: -)

...