Принцип использования
Включение истории 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-выражение. По умолчанию для строки возращается комментарий таблицы с первичным ключом, для столбца, являющегося внешним ключом, - первое текстовое поле связанной таблицы, для остальных - значение. Таким образом, отображать пользователю можно не значения, а описания.
При включении истории для detail-таблиц необходимо указать master-таблицу, это приведет к дополнительному хранению и индексированию полей внешнего ключа аналогично первичному. По умолчанию при получении изменений в master-таблице будут возвращаются и изменения в detail-таблицах.
Часть столбцов (например, имя пользователя) могу быть переопределены через функции кастомизации.
Пример:
-- 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, without detail table example.invoice_product select * from example.invoice_changes(hist_columns_insert=>true, hist_columns_immutable=>true, hist_tables_detail=>false) order by 1,2,3; -- Select partial changes by id with detail table, autocreated indexes provide fast execution select * from example.invoice_changes(12) order by 1,2,3; -- Equivalent with using union all select * from example.invoice_changes(id=>12, hist_tables_detail=>false) union all select * from example.invoice_product_changes(invoice_id=>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');
Изменения по опредленному условию можно получить используя cross join со списком первичных ключей. Для объединения истории по нескольким таблицам результаты функций можно объединять через оператор union all. Для подобных запросов рекомендуется создавать функции-обертки.
Для схем данных со сложной структурой рекомендуется создавать функции [schema].[table]_changes_ui(id), которые будут возвращать необходимый набор данных. Для приложения или проекта можно создать один единый тип данных hist_table_changes_ui и одну экранную форму (web-страницу) для отображения истории по любым таблицам и условиям.
Пример:
-- Create function that returns changes by date create or replace function example.invoice_changes_ui_date(date_changes date) returns setof pghist.hist_table_change language sql security definer as $$ select c.* from (select distinct id from example.invoice_hist where hist_timestamp::date=date_changes) h cross join example.invoice_changes(h.id) c order by 1,2,3; $$; select * from example.invoice_changes_ui_date(current_date); -- Create function that returns changes to master and detail tables create or replace function example.invoice_changes_ui_simple(id int) returns setof pghist.hist_table_change language sql security definer as $$ select * from example.invoice_changes(id=>id,hist_tables_detail=>false,hist_columns_insert=>true) union all select * from example.invoice_product_changes(invoice_id=>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.hist_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.hist_table_change_ui_cast(c pghist.hist_table_change) returns example.hist_table_change_ui language plpgsql as $$ begin return (c.timestamp,c.operation_name,c.db_user_name,c.row_desc,c.column_comment,c.value_old_desc,c.value_new_desc)::example.hist_table_change_ui; end; $$; create cast(pghist.hist_table_change as example.hist_table_change_ui) with function example.hist_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.hist_table_change_ui language sql security definer as $$ select c::pghist.hist_table_change::example.hist_table_change_ui from ( select * from example.invoice_changes(id=>id,hist_tables_detail=>false,hist_columns_insert=>true) union all select * from example.invoice_product_changes(invoice_id=>id) order by 1,2,3 ) c; $$; 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(hist_columns_immutable, hist_columns_insert, hist_tables_detail, hist_tables_inherited)
Функция возвращает изменения в таблице (setof pghist.table_change), создается при включении ведения истории.
Сортировка по первым трем столбцам обеспечивает хронологию по времени, для получения связанных данных из нескольких таблиц рекомендуется использовать оператор union all
Параметры:
[primary key column(s)] | - | значение первичного ключа (необязательный, может быть несколько) | |||
[column(s) of fkey on master table] | - | значение внешнего ключа на мастер-таблицу (необязательный, может быть несколько) | |||
hist_columns_immutable | boolean | - | возвращать значения неизменямых столбцов (необязательный, по умолчанию false) | ||
hist_columns_insert | boolean | - | детализировать операцию INSERT по столбцам (необязательный, по умолчанию false) | ||
hist_tables_detail | boolean | - | возвращать изменения в detail-таблицах (необязательный, по умолчанию true) | ||
hist_tables_inherited | boolean | - | учитывать унаследованные таблицы (необязательный, по умолчанию true) |
Столбцы возвращаемого набора данных (тип pghist.hist_table_change):
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(hist_columns_immutable=>true) order by 1,2,3; select * from example.invoice_changes(12) order by 1,2,3; select * from example.invoice_changes(id=>12, hist_columns_insert=>true, hist_tables_detail=>false) union all select * from example.invoice_product_changes(invoice_id=>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;