Принцип использования
Включение истории pghist.hist_enable создает в схеме таблицы 3 объекта для работы с историей:
1. Представление [schema].[table]_hist преставляет собой лог изменений(аудит) и предназначено для анализа разработчиками проблем с данными
SQL-запрос и приложение позволяют определить местоположение в исходном коде, cсылка на общую транзацию - изменения в других таблицах и SQL-запросы.
Пример:
-- Create schema and table, enable history, change data drop schema if exists example cascade; create schema example; create table example.document( id int primary key, number varchar(10), amount numeric(10,2) ); call pghist.hist_enable('example', 'document'); insert into example.document values (11, '#10', 100); insert into example.document values (12, '#20', 200); update example.document set number='#20/2',amount=210 where id=12; update example.document set amount=220 where id=12; delete from example.document where id=11; -- Select full info select * from example.document_hist; -- Select partial info by id select hist_timestamp,hist_operation,hist_db_user,amount_old from example.document_hist where id=12 and (hist_operation!='UPDATE' or 'amount'=any(hist_update_columns)) order by hist_statement_id; -- Select partial info with current values select * from example.document_hist union all select null,null,null,'[CURRENT_VALUES]',null,null,null,null,null,null,* from example.document order by id,1 -- Create extended view and grant select on it to developer -- (alternatively to 'grant select on all tables in schema pghist to developer'); select * from pghist.hist_data$example_document h join pghist.hist_statement s on s.id = h.hist_statement_id join pghist.hist_query q on q.hash = s.query_hash join pghist.hist_transaction t on t.id = s.transaction_id order by h.hist_statement_id, h.id; create or replace view example.document_hist_ext as select h.id,t.timestamp_commit,t.db_client_addr,q.text query_text from pghist.hist_data$example_document h join pghist.hist_statement s on s.id = h.hist_statement_id join pghist.hist_query q on q.hash = s.query_hash join pghist.hist_transaction t on t.id = s.transaction_id order by h.hist_statement_id, h.id; select * from example.document_hist_ext where id=12; grant select on example.document_hist_ext to developer_1;
2. Функция [schema].[table]_changes возвращает набор данных для показа изменений пользователю
Информация выводится по столбцам в формате старое-новое значение c описанием.
Отображать пользователю идентификаторы и внешние ключи неинформативно, поэтому для каждой строки и значений полей выводится описание, которое можно переопределить через SQL-выражение. По умолчанию для строки возращается комментарий таблицы с первичным ключом, для столбца, являющегося внешним ключом, - первое текстовое поле связанной таблицы, дла остальных - значение. Таким образом, отображать пользователю можно не значения, а описания.
Часть столбцов (например, имя пользователя) могу быть переопределены через функции кастомизации.
Пример:
-- Create schema and tables drop schema if exists example cascade; create schema example; create table example.customer( id int primary key, name varchar(100) not null ); insert into example.customer values (1,'Horns'),(2,'Hooves'); create table example.invoice( id int primary key, number varchar(10), date date, customer_id int references example.customer(id), amount numeric(20,2) ); comment on table example.invoice is 'Invoice'; comment on column example.invoice.id is 'Identifier'; comment on column example.invoice.number is 'Number'; comment on column example.invoice.date is 'Date'; comment on column example.invoice.customer_id is 'Сustomer'; comment on column example.invoice.amount is 'Amount'; create table example.product( id int primary key, name varchar(100) not null, code varchar(10) not null ); create table example.invoice_product( id serial primary key, invoice_id int references example.invoice(id), product_id int references example.product(id), quantity int, color char(1) check (color in ('R','G','B')) ); comment on table example.invoice_product is 'Product of invoice'; comment on column example.invoice_product.id is 'Identifier'; comment on column example.invoice_product.invoice_id is 'Invoice'; comment on column example.invoice_product.product_id is 'Product'; comment on column example.invoice_product.quantity is 'Quantity'; comment on column example.invoice_product.color is 'Color'; create index on example.invoice_product(invoice_id); -- Enable history call pghist.hist_enable('example', 'invoice'); call pghist.hist_enable('example', 'invoice_product', 'example', 'invoice'); -- Change data insert into example.invoice values (12,'#20', current_date, 1, 120.00); update example.invoice set customer_id=2 where id=12; insert into example.product(id,name,code) values (101,'Pensil','030'),(102,'Notebook','040'); insert into example.invoice_product(id, invoice_id, product_id, quantity, color) values (1,12,101,1000,'R'),(2,12,101,10,'G'); do $$ begin update example.invoice_product set quantity=quantity+1,color='B' where id=1; delete from example.invoice_product where id=2; update example.invoice set amount=150 where id=12; end; $$; -- Select all changes, first three columns provide chronological select * from example.invoice_changes() order by 1,2,3; -- Select all changes with column id (immutable), insert detail by columns select * from example.invoice_changes(insert_detail=>true, columns_immutable=>true) order by 1,2,3; -- Select partial changes by id select * from example.invoice_changes('id=12') order by 1,2,3; -- Select changes in two related tables, fast execution provides index invoice_product(invoice_id) select * from example.invoice_changes('id=$1',12) union all select * from example.invoice_product_changes('invoice_id=$1', 12) order by 1,2,3; -- Set description expression for columns row_desc and value_desc call pghist.hist_expression_row_desc('example', 'invoice', '''Invoice'''); call pghist.hist_expression_row_desc('example', 'invoice_product', $$ 'Row #'||$1.id||' / '||(select name from example.product where id=$1.product_id) $$); call pghist.hist_expression_value_desc('example', 'invoice_product', 'color', $$ case when $1='R' then 'Red' when $1='B' then 'Blue' when $1='G' then 'Green' else $1 end $$); -- Replace function for column db_user_name create or replace function example.db_user_name(db_user name) returns varchar language plpgsql as $$ begin return '['||db_user||']'; end; $$; call pghist.hist_column_custom_function('db_user_name', 'example.db_user_name');
Для объединения истории по нескольким таблицам результаты функций можно объединять через оператор union all. Наиболее актуально это для detail-таблиц, поэтому при включении для них истории необходимо указать master-таблицу, что приведет к дополнительному хранению и индексированию полей внешнего ключа аналогично первичному.
Для схем данных со сложной структурой рекомендуется создавать функции [schema].[table]_changes_ui(id), которые будут возвращать необходимый набор данных. Для приложения или проекта можно создать один единый данных table_changes_ui и одну экранную форму (web-страницу) для отображения истории по любой таблице.
Пример:
-- Create function that returns all columns, including unnecessary create or replace function example.invoice_changes_ui_simple(id int) returns setof pghist.table_change language sql security definer as $$ select * from example.invoice_changes('id=$1',id,insert_detail=>true) union all select * from example.invoice_product_changes('invoice_id=$1', id) order by 1,2,3; $$; select * from example.invoice_changes_ui_simple(12); -- Create type with only necessary columns for the UI and cast function to it, -- type is created only once (as a rule, there is only one history view form per project) create type example.table_change_ui as ( timestamp timestamptz, operation_name varchar, db_user_name varchar, row_desc text, column_comment varchar, value_old_desc text, value_new_desc text ); create or replace function example.table_change_ui_cast(tc pghist.table_change) returns example.table_change_ui language plpgsql as $$ begin return (tc.timestamp,tc.operation_name,tc.db_user_name,tc.row_desc,tc.column_comment,tc.value_old_desc,tc.value_new_desc)::example.table_change_ui; end; $$; create cast(pghist.table_change as example.table_change_ui) with function example.table_change_ui_cast as assignment; -- Create function that returns only necessary columns create or replace function example.invoice_changes_ui(id int) returns setof example.table_change_ui language sql security definer as $$ select tc::pghist.table_change::example.table_change_ui from ( select * from example.invoice_changes('id=$1',id,insert_detail=>true) union all select * from example.invoice_product_changes('invoice_id=$1', id) order by 1,2,3 ) tc; $$; select * from example.invoice_changes_ui(12);
3. Функция [schema].[table]_at_timestamp создает копию таблицы (временную таблицу) на момент времени в прошлом и возвращает ee
Используется для восстановления данных и получения данных и результата SQL-запроса из нескольких таблиц на момент времени в прошлом, может быть соединена с представлением [schema].[table]_hist по первичому ключу.
Для оптимальной работы SQL-запросов созданная временная таблица имеет индексы аналогичные исходной, время восстановления можно задать один раз в параметре pghist.at_timestamp.
Пример:
-- Create schema and tables, enable history drop schema if exists example cascade; create schema example; create table example.customer( id int primary key, name varchar(100) not null ); insert into example.customer values (1,'Horns'),(2,'Hooves'); create table example.document( id int primary key, number varchar(10), date date ); create table example.invoice( primary key (id), customer_id int references example.customer(id), amount numeric(20,2) ) inherits (example.document); create index on example.invoice(customer_id); call pghist.hist_enable('example', 'customer'); call pghist.hist_enable('example', 'document'); call pghist.hist_enable('example', 'invoice'); -- Enter data into tables insert into example.document values (11,'#10', current_date); insert into example.invoice values (12,'#20', current_date, 1, 120.00); insert into example.invoice values (13,'#30', current_date-1, 2, 130.00); -- Select data in past select * from example.document_at_timestamp(now()-interval '10 second'); -- Erroneous update and recovery update example.invoice set customer_id=2,amount=300 where date=current_date; update example.invoice i set amount = h.amount from example.invoice_at_timestamp('2024-04-06 10:00:00') h where i.id = h.id and i.date=current_date; -- Сombination log and versioning select * from example.invoice_at_timestamp('2024-04-06 10:00:00') where id in ( select id from example.invoice_hist where hist_timestamp>'2024-04-06 10:00:00' and hist_db_user=current_user ); -- Сomplex query in past select * from example.invoice i join example.customer c on c.id=i.customer_id; do $$ begin perform set_config('pghist.at_timestamp', '2024-04-06 10:00:00', true); perform example.invoice_at_timestamp(); perform example.customer_at_timestamp(); end; $$; select * from example_invoice_at_timestamp i join example_customer_at_timestamp c on c.id=i.customer_id;
Безопасность
Таблица истории hist_data$[schema]_[table] и триггерные функции создаются в схеме pghist, объекты для работы с историей - в схеме таблицы. Владельцем всех создавемых объектов является суперпользователь, хранимые процедуры создаются с правами владельца (security definer). Владельцу основной таблицы выдаются права с опцией передачи (with grant option) на чтение представления [schema].[table]_hist и вызов функций [schema].[table]_changes, [schema].[table]_at_timestamp.
При необходимости доступ разработчику к общим таблицам суперпользователь выдает отдельно.
Доступ к данным показан на схеме.
Пример:
-- postgres grant privileges on pghist schema and pghist.hist_enable procedure to developer_1 -- all privileges in pghist_grants.sql file grant usage on schema pghist to developer_1; grant execute on procedure pghist.hist_enable(name) to developer_1; -- developer_1 create table, enable history, grant privileges on example table and example_changes function to user_1 create table example( id int primary key, name varchar(20), number numeric(10,2), date date ); call pghist.hist_enable('example'); grant select,insert,update on example to user_1; grant execute on function example_changes to user_1; -- user_1 change data and view changes insert into example values (1, 'Example', 10, current_date); update example set number=20, date=date-1; select * from example_changes() order by 1,2,3; -- When trying to select log, user_1 receives an error -- SQL Error [42501]: ERROR: permission denied for view example_hist select * from example_hist;
pghist.hist_enable(schema, table_name, master_table_schema, master_table_name, columns_excluded)
Процедура включает ведение истории изменений для указанной таблицы, создает таблицу истории, триггеры и функции получения данных
Параметры:
schema | name | - | схема таблицы (необязательный) | ||
table_name | name | - | имя таблицы | ||
master_table_schema | name | - | схема мастер-таблицы (необязательный) | ||
master_table_name | name | - | имя мастер-таблицы (необязательный) | ||
columns_excluded | name[] | - | исключенные столбцы (необязательный) |
Примеры использования:
call pghist.hist_enable('document'); call pghist.hist_enable('example', 'invoice'); call pghist.hist_enable('example', 'invoice_product', 'example', 'invoice'); call pghist.hist_enable('orm', 'myclass', columns_excluded => array['inserted_at','updated_at']);
[schema].[table]_hist
Представление содержит список изменений по строкам, создается при включении ведения истории.
Сортируется по первым двум столбцам, что обеспечивает хронологию по времени
Столбцы:
hist_statement_num | bigint | - | глобальный номер SQL-выражения | ||
hist_row_num | bigint | - | номер строки в наборе данных, который изменяет SQL-выражения | ||
hist_timestamp | timestamptz | - | дата-время начала выполнения SQL-выражения | ||
hist_operation | varchar | - | операция: INSERT,UPDATE,DELETE,TRUNCATE | ||
hist_update_columns | name[] | - | список обновленных столбцов для операции UPDATE | ||
hist_db_user | name | - | пользователь базы данных | ||
hist_app_user | varchar | - | пользователь приложения, определяется как current_setting('app.user') | ||
hist_application_name | varchar | - | имя приложения | ||
hist_query_text | text | - | текст SQL-запроса | ||
hist_statement_id | bigint | - | ID SQL-выражения, ссылается на таблицу pghist.hist_statement | ||
[primary key column(s)] | - | значение первичного ключа (можно быть несколько) | |||
[column(s) of fkey on master table] | - | значение внешного ключа на мастер-таблицу (необязательно, можно быть несколько) | |||
[column...]_old | - | старое значение для каждого столбца таблицы [schema].[table] |
Примеры использования:
select * from example.document_hist; select * from example.document_hist where id=2; select * from example.document_hist where hist_operation='DELETE';
[schema].[table]_changes(where_clause, where_param, columns_immutable, insert_detail, cascade)
Функция возвращает изменения в таблице (setof pghist.table_change), создается при включении ведения истории.
Сортировка по первым трем столбцам обеспечивает хронологию по времени, для получения связанных данных из нескольких таблиц рекомендуется использовать оператор union all
Параметры:
where_clause | text | - | условие на таблицу (необязательный) | ||
where_param | anyelement | - | значение параметра в where_clause (необязательный) | ||
columns_immutable | boolean | - | возвращать значения неизменямых столбцов (необязательный, по умолчанию false) | ||
insert_detail | boolean | - | детализировать операцию INSERT по столбцам (необязательный, по умолчанию false) | ||
cascade | boolean | - | учитывать унаследованные таблицы (необязательный, по умолчанию true) |
Столбцы возвращаемого набора данных:
statement_num | bigint | - | глобальный номер SQL-выражения | ||
row_num | int | - | номер строки в наборе данных, который изменяет SQL-выражения | ||
column_num | int | - | номер столбца в строке | ||
timestamp | timestamptz | - | дата-время операции | ||
operation | varchar | - | операция: INSERT,UPDATE,DELETE,TRUNCATE | ||
operation_name | varchar | - | наименование операции, задается переопределяемой функцией pghist.hist_custom_operation_name | ||
column_name | name | - | имя столбца | ||
column_comment | varchar | - | комментарий столбца | ||
value_old | text | - | старое значение | ||
value_old_desc | text | - | описание старого значения, можно переопредилить через процедуру pghist.hist_expression_value_desc | ||
value_new | text | - | новое значение | ||
value_new_desc | text | - | описание нового значение, можно переопредилить через процедуру pghist.hist_expression_value_desc | ||
row_desc | text | - | описание строки, можно переопредилить через процедуру pghist.hist_expression_row_desc | ||
db_user | name | - | пользователь базы данных | ||
db_user_name | varchar | - | имя пользователя базы данных, задается переопределяемой функцией pghist.hist_custom_db_user_name | ||
app_user | varchar | - | пользователь приложения, задается переопределяемой функцией pghist.hist_custom_app_user | ||
app_user_name | varchar | - | имя пользователя приложения, задается переопределяемой функцией pghist.hist_custom_app_user_name | ||
schema | name | - | схема | ||
table_name | text | - | имя таблицы | ||
table_comment | text | - | комментарий таблицы |
Примеры использования:
select * from example.invoice_changes() order by 1,2,3; select * from example.invoice_changes(insert_detail=>true, columns_immutable=>true) order by 1,2,3; select * from example.invoice_changes('id=12') order by 1,2,3; select * from example.invoice_changes('id=$1',12) union all select * from example.invoice_product_changes('invoice_id=$1', 12) order by 1,2,3;
[schema].[table]_at_timestamp(transaction_timestamp, cascade)
Функция создает временную таблицу [schema]_[table]_at_timestamp как копию исходной таблицы по состоянию на момент времени и возращает ее
Параметры:
schema | - | схема таблицы | |||
table_name | - | имя таблицы | |||
transaction_timestamp | timestamptz | - | дата-время завершения транзации (необязательный, по умолчанию current_setting('pghist.at_timestamp')) | ||
cascade | boolean | - | учитывать унаследованные таблицы (необязательный, по умолчанию true) |
Примеры использования:
select * from example.document_at_timestamp(now()-interval '1 hour'); do $$ begin perform set_config('pghist.at_timestamp', '2024-04-06 10:00:00', true); perform example.invoice_at_timestamp(); perform example.customer_at_timestamp(); end; $$; select * from example_invoice_at_timestamp i join example_customer_at_timestamp c on c.id=i.customer_id;
pghist.hist_disable(schema, table_name)
Процедура выключает ведение истории изменений для указанной таблицы, удаляет необходимые объекты
Параметры:
schema | name | - | схема таблицы (необязательный) | ||
table_name | name | - | имя таблицы |
Примеры использования:
call pghist.hist_disable('document'); call pghist.hist_disable('example', 'invoice');
SQL-выражения описаний
pghist.hist_expression_row_desc(schema, table_name, expression)
pghist.hist_expression_value_desc(schema, table_name, column_name, expression)
Процедуры устанавливают выражение для описания строки/значения поля, в выражении доступна переменная $1 c строкой/значением поля
pghist.hist_expression_row_desc_current(schema, table_name)
pghist.hist_expression_value_desc_current(schema, table_name, column_name)
Функции возвращают текущее выражение для описания строки/значения поля
pghist.hist_expression_row_desc_default(schema, table_name)
pghist.hist_expression_value_desc_default(schema, table_name, column_name)
Функции возвращают выражение по умолчанию для описания строки/значения поля. Для строки возращается комментарий таблицы с первичным ключом, для столбца, являющегося внешним ключом, - первое текстовое поля связанной таблицы
Параметры:
schema | name | - | схема таблицы | ||
table | name | - | имя таблицы | ||
column_name | name | - | имя столбца | ||
expression | varchar | - | выражение, null отменяет сохраненное описание |
Примеры использования:
call pghist.hist_expression_row_desc('example', 'invoice', '''Invoice'''); call pghist.hist_expression_row_desc('example', 'invoice_product', $$ 'Row #'||$1.id||' / '||(select name from example.product where id=$1.product_id) $$); call pghist.hist_expression_value_desc('example', 'invoice_product', 'color', $$ case when $1='R' then 'Red' when $1='B' then 'Blue' when $1='G' then 'Green' else $1 end $$); select pghist.hist_expression_row_desc_current('example', 'invoice_product'); select pghist.hist_expression_value_desc_default('example', 'invoice_product', 'color');
Функции кастомизации
Для кастомизации и локализации полей конкретного приложения можно определить собственные функции.
Рекомендуется в SQL-менеджере (например, pgAdmin или DBeaver) взять за основу функцию по умолчанию, переопределить ее и сохранить в схеме приложения под другим именем
Столбцы, доступные для переопределения:
operation_name | varchar | - | наименование операции | ||
db_user_name | varchar | - | имя пользователя базы данных | ||
app_user | varchar | - | пользователь приложения | ||
app_user_name | varchar | - | имя пользователя приложения | ||
app_client_addr | inet | - | IP адрес клиентского приложения | ||
app_client_hostname | varchar | - | имя хоста(компьютера) клиентского приложения |
pghist.hist_column_custom_function(column_name, custom_function)
Процедура устанавливает функцию для получения значения поля
Параметры:
column_name | name | - | имя столбца | ||
custom_function | name | - | имя функции без параметров |
pghist.hist_column_custom_function(column_name)
Функция возвращает текущую функцию для получения значения поля
Параметр:
column_name | name | - | имя столбца |
Пример:
select pghist.hist_column_custom_function_current('db_user_name'); create or replace function myapp.user_name(db_user name) returns varchar language plpgsql as $$ begin return (select name from myapp.users where login=db_user); end; $$; call pghist.hist_column_custom_function('db_user_name', 'myapp.user_name');
Таблица истории pghist.hist_data$[schema]_[table]
При включении истории дополнительно к основной таблице создается таблица истории, разработчику (владельцу основной таблицы) доступна через представление [schema].[table]_hist
Столбцы:
hist_statement_id | bigint | - | ID SQL-выражения, ссылается на таблицу pghist.hist_statement | ||
hist_row_num | int | - | номер строки в наборе данных, который изменяет SQL-выражения | ||
hist_update_columns | name[] | - | список обновленных столбцов для операции UPDATE | ||
[primary key column(s)] | - | значение первичного ключа (можно быть несколько) | |||
[column(s) of fkey on master table] | - | значение внешного ключа на мастер-таблицу (необязательно, можно быть несколько) | |||
[column...]_old | - | старое значение для каждого столбца таблицы [schema].[table] |
Пример использования:
select * from pghist.hist_data$example_invoice h join pghist.hist_statement s on s.id=h.hist_statement_id join pghist.hist_query q on q.hash=s.query_hash where q.text like 'do%begin%';
Общие таблицы
pghist.hist_transaction | - | транзакции |
pghist.hist_query | - | SQL-запросы |
pghist.hist_statement | - | SQL-выражения |
pghist.hist_table | - | таблицы с историей |
pghist.hist_table_column | - | столбцы таблиц с историей |
pghist.hist_column_custom_function | - | кастомизированные функции столбцов |
При установке создаются таблицы, которые хранят общую информацию по изменениям (дата-время транзакции и SQL-выражения, операция, SQL-запрос, пользователь, сессия и т.д.), и таблицы с настройками. Изменение общих таблиц осуществляется через хранимые процедуры, допускается только чтение. По умолчанию разработчикам (владельцам основных таблицы) общие таблицы недоступны, при необходимости доступ выдается вручную.
Таблица истории pghist.hist_data$[schema]_[table] по полю statement_id ссылается на pghist.hist_statement, которая в свою очередь ссылается на pghist.hist_transaction и pghist.hist_query.
Примеры использования:
select * from pghist.hist_statement s join pghist.hist_transaction t on t.id=s.transaction_id join pghist.hist_query q on q.hash=s.query_hash where t.timestamp_commit between '2024-04-06 10:00:00' and '2024-04-06 11:00:00'; select * from pghist.hist_table ht left join pghist.hist_table_column htc on htc.schema=ht.schema and htc.table_name=ht.name;
pghist.pghist_version
Функция возвращает версию инструмента
pghist.hist_sql_log
Все команды, выполняемые утилитой, записываются в лог-таблицу pghist.hist_sql_log
Примеры использования:
select * from pghist.hist_sql_log where schema='example' and table_name='document' order by id; select * from pghist.hist_sql_log l join pghist.hist_transaction t on t.id=l.transaction_id where schema='example' and table_name='document' order by 1 desc;