среда, 20 ноября 2019 г.

Условное выполнение команд в psql


Для выполнения команд по условию в psql имеется конструкция \if. Например, нам нужен аналог CREATE .. IF NOT EXISTS для слотов репликации:

SELECT NOT EXISTS (SELECT NULL FROM pg_replication_slots WHERE slot_name = 'replica') AS is_not_exists
\gset

\if :is_not_exists
    SELECT pg_create_physical_replication_slot('replica');
\endif


То же самое можно сделать c \gexec. И  без промежуточной переменной:

SELECT 'SELECT pg_create_physical_replication_slot(''replica'')'
 WHERE NOT EXISTS (SELECT NULL FROM pg_replication_slots WHERE slot_name = 'replica')
\gexec

среда, 31 мая 2017 г.

Работа с системным каталогом

Основные таблицы системного каталога и команды psql для работы с ними.

вторник, 28 февраля 2017 г.

Отложенные ограничения целостности

Пример реализации сложных ограничений целостности для обеспечения согласованности данных.

Имеем дело с проводками в учетной системе. Каждая проводка имеет одну строку заголовка в таблице headers и несколько строк с дебетуемыми и кредитуемыми счетами в таблице lines.

Очевидное требование к данным, чтобы сумма всех дебетуемых и кредитуемых счетов у одной проводки была одинаковой. Также не должно быть строк в headers без соответствующих строк в lines.

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

Решить такую задачу на уровне базы данных можно следующим образом. 
Понадобится строчный триггер на lines, который автоматически поддерживает сумму всех дебетуемых и кредитуемых счетов в дополнительных столбцах в headers: amount_dr, amount_cr.
Триггер гарантирует, что после каждой команды insert/update/delete в lines, в таблице headers обновляются итоговые суммы по дебету и кредиту. 

Теперь напрашивается сделать отложенное (deferred) ограничение check в headers с условием amount_dr = amount_cr. 
И всё. Пока транзакция не завершена, это условие не будет проверяться, но commit зафиксирует только состояние равенства.

Однако PostgreSQL разрешает сделать отложенными только ограничения PRIMARY KEY, UNIQUE, FOREIGN KEY, EXCLUDE. А ограничения CHECK и NOT NULL нет. В этом есть своя логика, но сейчас она мешает. 

Поскольку нам нужно именно отложенное ограничение, то воспользуемся FOREIGN KEY вместо CHECK.
Как? Придется добавить в headers еще один фиктивный столбец, в который тот же триггер на lines будет записывать true или false, в зависимости от того, являются ли текущие итоги по дебету и кредиту сбалансированными. И создать внешний ключ на этот столбец, который ссылается на фиктивную таблицу с одним столбцом и одной строкой со значение true.
И этот внешний ключ будет отложенным.

Впрочем, вот рабочий протитип:

create table dummy (
   pk boolean primary key
);
insert into dummy values (true);

create table headers (
   header_id int primary key,
   accounting_date date default now()::date,
   amount_dr numeric(20,2) not null default 0,
   amount_cr numeric(20,2) not null default 0,
   is_balance boolean not null default false,
   foreign key (is_balance) references dummy (pk)
      deferrable initially deferred
);

create table lines (
   header_id int,
   line_id int,
   account text not null,
   amount_dr numeric(20,2) not null,
   amount_cr numeric(20,2) not null,
   primary key (header_id, line_id),
   foreign key (header_id) references headers (header_id) on delete cascade,
   /* счет или дебетуется, или кредитуется */
   check ((amount_dr <> 0 and amount_cr = 0)
      or (amount_dr = 0 and amount_cr <> 0))
);

create function check_balance() returns trigger
as $$
<<local>>
declare
   change_dr lines.amount_dr%type;
   change_cr lines.amount_cr%type;
   header_id  lines.header_id%type;
begin
   if TG_OP = 'DELETE' then
      change_dr := -1 * old.amount_dr;
      change_cr := -1 * old.amount_cr;
      local.header_id := old.header_id;
   elsif TG_OP = 'UPDATE' then
      change_dr := new.amount_dr - old.amount_dr;
      change_cr := new.amount_cr - old.amount_cr;
      local.header_id := new.header_id;
   elsif TG_OP = 'INSERT' then
      change_dr := new.amount_dr;
      change_cr := new.amount_cr;
      local.header_id := new.header_id;
   end if;

   update headers h
   set    amount_dr = amount_dr + change_dr,
          amount_cr = amount_cr + change_cr,
          is_balance = (amount_dr + change_dr = amount_cr + change_cr) and
                       (amount_dr + change_dr) <> 0
   where  h.header_id = local.header_id;

   return new;
end;
$$ language plpgsql;

create trigger line_change
   after insert or delete or update of amount_dr, amount_cr
   on lines
   for each row
   execute procedure check_balance();


Проверяем.
Сценарий 1. Нельзя зафиксировать заголовок проводки без строк:

demo=# begin;
BEGIN
demo=# insert into headers (header_id) values (1);
INSERT 0 1
demo=# select * from headers;
 header_id | accounting_date | amount_dr | amount_cr | is_balance
-----------+-----------------+-----------+-----------+------------
         1 | 2017-03-02      |      0.00 |      0.00 | f
(1 row)

demo=# commit;
ERROR:  insert or update on table "headers" violates foreign key constraint "headers_is_balance_fkey"
DETAIL:  Key (is_balance)=(f) is not present in table "dummy".

Сценарий 2. Если проводка не сбалансирована:

demo=# begin;
BEGIN
demo=# insert into headers (header_id) values (1);
INSERT 0 1
demo=# insert into lines values (1,1,'10', 1000, 0);
INSERT 0 1
demo=# insert into lines values (1,2,'60', 0, 1180);
INSERT 0 1

пока транзакция незавершена - всё ок:

demo=# select * from headers;
 header_id | accounting_date | amount_dr | amount_cr | is_balance
-----------+-----------------+-----------+-----------+------------
         1 | 2017-03-02      |   1000.00 |   1180.00 | f
(1 row) 


пробуем завершить - получаем ошибку: 

demo=# commit;
ERROR:  insert or update on table "headers" violates foreign key constraint "headers_is_balance_fkey"
DETAIL:  Key (is_balance)=(f) is not present in table "dummy".


Сценарий 3. Сбалансированная проводка

demo=# begin;
BEGIN
demo=# insert into headers (header_id) values (1);
INSERT 0 1
demo=# insert into lines values (1,1,'10', 1000, 0);
INSERT 0 1
demo=# insert into lines values (1,2,'60', 0, 1180);
INSERT 0 1
demo=# insert into lines values (1,3,'19', 180, 0);
INSERT 0 1
demo=# commit;
COMMIT 


проверим, что всё нормально: 

demo=# select * from headers;
 header_id | accounting_date | amount_dr | amount_cr | is_balance
-----------+-----------------+-----------+-----------+------------
         1 | 2017-03-02      |   1180.00 |   1180.00 | t
(1 row)

demo=# select * from lines;
 header_id | line_id | account | amount_dr | amount_cr
-----------+---------+---------+-----------+-----------
         1 |       1 | 10      |   1000.00 |      0.00
         1 |       2 | 60      |      0.00 |   1180.00
         1 |       3 | 19      |    180.00 |      0.00
(3 rows)

Конечно, такое решение не лишено недостатков в виде фиктивной однострочной таблицы, дополнительных столбцов в headers и необходимости их поддержки. Но с другой стороны, можно не беспокоиться, что прикладной код нарушит согласованность данных.

пятница, 24 февраля 2017 г.

PLDebugger - отладчик PL/pgSQL

Возникло желание посмотреть как в pgAdmin работает отладчик PL/pgSQL и можно ли его использовать самостоятельно, например в psql.

В итоге всё получилось. Но не всё прошло гладко, поэтому есть смысл остановиться подробнее на каждом шаге:
  1. Установить pgAdmin 4.
  2. Установить pldebugger.
  3. Проверить отладчик в pgAdmin и найти описание использования api.
  4. Сделать пример в psql.
Исходные данные:
  • ОС Ubuntu 14.04 
  • PostgreSQL 9.6.2 (установлен из пакета пользователем root)

1. Установка pgAdmin 4

Установка pgAdmin 4 v1.2 на Ubuntu осложняется тем, что на сайте и в документации нет раздела "Установка".
Для обладателей Windows и MacOS программы установки уже подготовлены в разделе Download. Пользователи Ubuntu могут воспользоваться инструкцией с askubuntu.com. Только вместо несуществующего пакета virtualenv нужно установить python-virtualenv.

2. Установка отладчика pldebugger

Отладчик PL/pgSQL устанавливается отдельно от pgAdmin. Он представляет собой расширение PostgreSQL, которое нужно собрать и установить, а также прописать в параметре shared_preload_libraries библиотеку plugin_debugger.

В файле README.pldebugger есть раздел про установку, ориентированный на сборку PostgreSQL из исходных кодов. Для PostgreSQL, установленного из пакета, можно сделать так:
  1. Понадобятся пакеты:
    apt-get install postgresql-server-dev-9.6 libssl-dev libkrb5-dev
  2. Расширение устанавливаем из под root, используя PGXS:
    sudo su root
    git clone git://git.postgresql.org/git/pldebugger.git
    cd pldebugger
    export USE_PGXS=1
    make
    make install
  3. Не забываем перезапустить PostgreSQL после установки параметра shared_preload_libraries.

3. Проверка отладчика в pgAdmin и описание работы api

Отладчик работает согласно документации. Здесь особо нечего добавить. Разве что для установки точки останова, нужно "тыкать" мышкой не в число с номером строки исходного кода, а чуть-чуть правее, на свободный участок, в котором должна появиться точка.

Что касается описания функций API для использования без pgAdmin. Отдельного описания найти не удалось, но можно воспользоваться следующим:
  1. Достаточно подробные комментарии в исходном коде. Например, в файле pldbgapi.c.
  2. В переписке разработчиков нашлось такое письмо за 2007 год от Korry Douglas.
  3. Никто не отменял самый "честный" способ. Включить журналирование всех команд (log_statement='all'), запустить отладку в pgAdmin и смотреть, что и в каком порядке пишется в журнал сервера.

4. Пример отладки в psql

Вооружившись необходимыми знаниями можно попробовать выполнить отладку в psql, используя расширение pldbgapi.

Понадобятся два сеанса psql: 1 - сеанс управления отладкой (proxy), 2 - сеанс, в котором запускается функция для отладки (target).

Установим отладчик:

 1| => show shared_preload_libraries;

 1|  shared_preload_libraries 
 1| ---------------------------
 1|  "$libdir/plugin_debugger"
 1| (1 row)
 1|

 1| => create extension pldbgapi;

 1| CREATE EXTENSION


Создадим функцию для подсчета количества строк в таблице, имя которой
передается параметром.

При помощи отладчика мы хотим убедиться, что текст команды SELECT формируется корректно:

 1| => CREATE FUNCTION get_count (tabname text) RETURNS bigint
 1| AS $$DECLARE
 1|     cmd text;
 1|     retval bigint;
 1| BEGIN
 1|     cmd := 'SELECT COUNT(*) FROM '
 1|            || quote_ident(tabname);
 1|
 1|     EXECUTE cmd INTO retval;
 1|     RETURN retval;
 1| END;
 1| $$ LANGUAGE plpgsql STABLE;

 1| CREATE FUNCTION


Получим идентификатор сеанса отладки и OID функции:

 1| => select pldbg_create_listener() as sessionid,
 1|          'get_count'::regproc::oid as funcoid \gset


 1| => \echo :sessionid :funcoid

 1| 1 115750


Установим точку останова в начале функции (3-й параметр: -1) и ждем вызова функции из любого (4-й параметр: NULL) сеанса:

 1| => select * from pldbg_set_global_breakpoint(:sessionid, :funcoid, -1, NULL);

 1|  pldbg_set_global_breakpoint
 1| -----------------------------
 1|  t
 1| (1 row)
 1|

 1| => select * from pldbg_wait_for_target(:sessionid);


Отладочный сеанс перешел в режим ожидания...

Откроем второй сеанс и вызываем функцию:

 2|          => select get_count('pg_class');

Выполнение доходит до точки останова и теперь второй сеанс переходит в режим ожидания команд отладчика.

Первый сеанс просыпается и переходит в режим управления отладкой:

 1|  pldbg_wait_for_target
 1| -----------------------
 1|                  21413
 1| (1 row)


В режиме управления можно:
  • проверить стек вызова (pldbg_get_stack),
  • перемещаться по функциям в стеке (pldbg_select_frame),
  • смотреть значения параметров и локальных переменных текущей функции в стеке (pldbg_get_variables),
  • изменить значения переменных (pldbg_deposit_value),
  • установить/удалить точки останова (pldbg_set_breakpoint/pldbg_drop_breakpoint),
  • продолжить выполнение до следующей точки (pldbg_continue),
  • выполнять пошаговую отладку с заходом внутрь функций (pldbg_step_into),
  • или выполняя строку целиком (pldbg_step_over),
  • в конечном итоге, прекратить сеанс отладки (pldbg_abort_target).
Посмотрим, например,  значения переменных:

 1| => select name, varClass, value, pg_catalog.format_type(dtype, NULL) as dtype
 1|       from   pldbg_get_variables(:sessionid)
 1|       order by varClass;

 1|   name   | varclass |  value   | dtype 
 1| ---------+----------+----------+--------
 1|  tabname | A        | pg_class | text
 1|  cmd     | L        | NULL     | text
 1|  retval  | L        | NULL     | bigint
 1| (3 rows)


Видим, что параметр функции tabname = 'pg_class', а значения переменных cmd и retval еще не установлены.

Добавим еще одну точку останова на команде EXECUTE (8 строка функции):

 1| => select * from pldbg_set_breakpoint(:sessionid, :funcoid, 8);

 1|  pldbg_set_breakpoint
 1| ----------------------
 1|  t
 1| (1 row)


Продолжим выполнение до новой точки останова:

 1| => select p.targetName,
 1|             (select string_agg(t.n::text||
 1|                        case when t.n=p.linenumber-1 then '*' else '' end,
 1|                        chr(10)) from generate_series(1, 11) as t(n)) as line,
 1|             pldbg_get_source(:sessionid, p.func) AS src
 1| from pldbg_continue(:sessionid) p;

 1|    targetname    | line |                 src                
 1| -----------------+------+-------------------------------------
 1|  get_count(text) | 1   +| DECLARE                            +
 1|                  | 2   +|     cmd text;                      +
 1|                  | 3   +|     retval bigint;                 +
 1|                  | 4   +| BEGIN                              +
 1|                  | 5   +|     cmd := 'SELECT COUNT(*) FROM ' +
 1|                  | 6   +|            || quote_ident(tabname);+
 1|                  | 7   +|                                    +
 1|                  | 8*  +|     EXECUTE cmd INTO retval;       +
 1|                  | 9   +|     RETURN retval;                 +
 1|                  | 10  +| END;                               +
 1|                  | 11   |
 1| (1 row)


Еще раз посмотрим значения локальных переменных:

 1| => select name, varClass, value, pg_catalog.format_type(dtype, NULL) as dtype
 1|       from   pldbg_get_variables(:sessionid)
 1|       order by varClass;

 1|   name   | varclass |             value             | dtype 
 1| ---------+----------+-------------------------------+--------
 1|  tabname | A        | pg_class                      | text
 1|  cmd     | L        | SELECT COUNT(*) FROM pg_class | text
 1|  retval  | L        | NULL                          | bigint
 1| (3 rows)


Видим, что переменной cmd присвоен текст команды SELECT.

Выполним следующую строку функции:

 1| => select s.targetname, s.linenumber-1 as line
 1|       from pldbg_step_over(:sessionid) s;

 1|    targetname    | line
 1| -----------------+------
 1|  get_count(text) |    9
 1| (1 row)


И  еще раз посмотрим значения локальных переменных:

 1| => select name, varClass, value, pg_catalog.format_type(dtype, NULL) as dtype
 1|       from   pldbg_get_variables(:sessionid)
 1|       order by varClass;

 1|   name   | varclass |             value             | dtype 
 1| ---------+----------+-------------------------------+--------
 1|  tabname | A        | pg_class                      | text
 1|  cmd     | L        | SELECT COUNT(*) FROM pg_class | text
 1|  retval  | L        | 321                           | bigint
 1| (3 rows)


Команда SELECT выполнена, в переменную retval записан результат.

Прекратим сеанс отладки:

 1| => select * from pldbg_abort_target(:sessionid);

 1|  pldbg_abort_target
 1| --------------------
 1|  t
 1| (1 row)


 2|          ERROR:  canceling statement due to user request
 2|          CONTEXT:  PL/pgSQL function get_count(text) line 9 at RETURN


Выполнение функции во втором сеансе завершается ошибкой.

Итоги

Отладчик PL/pgSQL работает, его API можно использовать без pgAdmin. 
Осталось дождаться, когда найдется умелец, который возьмет и сделает удобный графический интерфейс, лишенный недостатков, которыми столь "грешит" pgAdmin.

понедельник, 20 февраля 2017 г.

Сумма прописью на SQL

Беглый поиск решений для записи денежной суммы прописью (словами) находит лишь этот вариант.
Причем, он очень похож на более раннюю реализацию для Oracle PL/SQL.

Здесь представлена попытка внести разнообразие.
А чтобы было не так скучно, функция to_text сделана на SQL.

четверг, 7 июля 2016 г.

Ключевые слова и vim

В документации ключевые слова PostgresSQL написаны в верхнем регистре. Действительно, так удобно читать команды SQL.

Но писать код в разных регистрах не очень удобно. 
Для упрощения ввода, в vim можно настроить автоматическую замену слов. Например, после выполнения команды

:ab select SELECT

набранное в нижнем регистре select будет преобразовано в верхний.

Список ключевых слов выдает функция pg_get_keywords().
Остается сформировать файл с нужными командами и выполнять его при запуске редактора.

psql <<EOF
\pset format unaligned
\pset tuples_only on
\o ~/.vim/pg_keywords_abbr
select 'ab '||t.a||' '||upper(t.a) from pg_get_keywords() as t(a,b,c);
EOF


И строка в ~/.vim/vimrc для загрузки автозамен при редактировании файлов с расширением sql:

au BufNewFile,BufRead *.sql source ~/.vim/pg_keywords_abbr

пятница, 13 мая 2016 г.

Записки о локализации


Кодировка символов


  1. На сервере. Убедиться (psql: \l) что кодировка базы данных поддерживает кириллицу (одна из списка)
  2. На клиенте. Убедиться что установленная кодировка на клиенте поддерживает кириллицу.
    Зависит от ОС.
    Примечание. В Windows, чтобы для cmd.exe установить кодировку 1251:
    • c:\> 'chcp 1251' выбрать шрифты Lucida Console
    • выбрать шрифты Lucida Console
  3. На клиенте. Установить параметр client_encoding в соответствии с кодировкой клиента.
    Например, SET client_encoding TO 'WIN1251';
    Примечание. psql устанавливает client_encoding в кодировку клиента автоматически при подключении к БД (запуск или \connect). Проверить текущее значение и/или установить новое: команда \encoding

Язык сообщений сервера

  • Выбор языка сообщений возможен, если при сборке сервера PostgreSQL использовалась опция --enable_nls.
    Проверить: pg_config --configure
    В директории pg_config--localedir должны быть подкаталоги для каждого установленного языка
  • Кодировка символов для сообщений сервера определяется параметром client_encoding
  • Установить требуемый язык сообщений сервера в параметре lc_messages.
    Например, SET lc_messages TO 'ru_RU.UTF8';
    Примечание. Если в сессии, которая стартовала сервер (pg_ctl start), была установлена переменная окружения LANGUAGE, то именно это значение будет использоваться для языка сообщений сервера. Значение параметра LC_MESSAGES не будет использоваться.
    Примечание. При подключении к серверу могут возникать ошибки (например, указан неправильный пароль). В этот момент сервер еще не знает кодировку клиента (параметр client_encoding еще не установлен), поэтому будет отправлять сообщение об ошибке в кодировке сервера. Если клиент использует другую кодировку и установлен язык сообщений сервера отличный от en (например, ru), то клиент получит сообщение в неверной кодировке.
    Примечание. Сообщения сервера отправляются не только клиенту, но и в журнал сервера. При установке русского языка для сообщений сервера нужно убедиться, что утилиты для обработки журнала (если используются) понимают кириллицу.

Язык сообщений утилит PostgreSQL (psql, pg_dump, pg_ctl)

  • Выбор языка сообщений возможен, если при сборке утилит PostgreSQL использовалась опция --enable_nls.
    Проверить: pg_config --configure
    В директории pg_config--localedir должны быть подкаталоги для каждого установленного языка

    Примечание. В общем случае, утилиты PostgreSQL можно использовать для подключения к другому серверу PostgreSQL, например удаленному.
  • Язык сообщений утилит PostgreSQL определяется настройками ОС.
    Например, в linux/unix (и в Windows) это переменные окружения (в порядке предпочтения): LANGUAGE, LC_ALL, LC_MESSAGES, LANG
    Примечание. psql выводит как свои собственные сообщения (например, при запуске), так и сообщения сервера. Поскольку язык для этих двух видов сообщений настраивается в разных местах (параметр lc_messages для сообщений сервера и переменные ОС для сообщений psql), то и выводится они могут на разных языках.

Установка параметров в строке соединения


В строке соединения можно указать параметры конфигурации, которые нужно установить после подключения.

psql postgresql://postgres@localhost:5432/postgres?options="-c work_mem%3D32MB -c maintenance_work_mem%3D512MB"

Тоже самое можно сделать установив переменную окружения PGOPTIONS:

export PGOPTIONS='-c work_mem=32MB -c maintenance_work_mem=512MB'
psql -c "show maintenance_work_mem;"
 maintenance_work_mem
----------------------
 512MB


Может быть полезным, когда код приложения изменить нельзя и не подходит функциональность ALTER DATABASE SET или ALTER ROLE SET.


Ссылки:
Connection strings
Percent-encoding in a URI

воскресенье, 27 марта 2016 г.

pg_dump и таблицы расширений

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

Пример. Файлы расширения my_ext с таблицей.

$ cat `pg_config --sharedir`/extension/my_ext.control
default_version = '1.0'

$ cat `pg_config --sharedir`/extension/my_ext--1.0.sql
\echo Use CREATE EXTENSION my_ext to load this file. \quit
create table my_ext_table (
   id serial
  ,param_name text
  ,param_value text
);
create view my_ext_params as select param_name, param_value from my_ext_table;

Создаем расширение в отдельной БД:

postgres=# create database db;
CREATE DATABASE
postgres=# \c db
You are now connected to database "db" as user "postgres".
db=# create extension my_ext;
CREATE EXTENSION

Проверяем его состав:

db=# \dx+ my_ext
Objects in extension "my_ext"
      Object Description      
------------------------------
 sequence my_ext_table_id_seq
 table my_ext_table
 view my_ext_params
(3 rows)

Добавляем строку в таблицу:

db=# insert into my_ext_table (param_name, param_value) values ('Param1', 'Value1');
INSERT 0 1
db=# select * from my_ext_params; 
 param_name | param_value 
------------+-------------
 Param1     | Value1
(1 row)

Делаем dump и смотрим как выгружается расширение:

db=# \! pg_dump db|grep my_ext
-- Name: my_ext; Type: EXTENSION; Schema: -; Owner: 
CREATE EXTENSION IF NOT EXISTS my_ext WITH SCHEMA public;

В дампе только команда CREATE EXTENSION, которая запускает скрипт my_ext–1.0.sql. Добавленная в таблицу my_ext_table строка не выгружается и следовательно не будет восстановлена.

Как правильно написал Том Лэйн, это проблема разработчиков расширений. Внимательно читая документацию, находим функцию pg_extension_config_dump(regclass, text), специально предназначенную для регистрации таблиц, данные которых pg_dump должен выгружать. Таким образом, в скрипте на создание расширения my_ext—1.0.sql нужно было добавить вызов функции:
select pg_extension_config_dump('my_ext_table'::regclass, '');

Этого достаточно для того, чтобы в вывод pg_dump записывалась команда COPY для выгрузки табличных данных (CREATE TABLE не будет выгружаться. Создание таблицы, по-прежнему, выполняется в CREATE EXTENSION). Второй параметр функции позволяет задать предложение WHERE,  чтобы выгружать только строки удовлетворяющие условию. Это может понадобиться, чтобы избежать повторной вставки, если скрипт на создание расширения сам создает строки в таблице.
Функцию pg_extension_config_dump можно вызывать только в скрипте на создание расширения (в нашем случае my_ext–1.0.sql). Поэтому если разработчики расширения не предусмотрели выгрузку табличных данных, то просто вызвать эту функцию не получится, требуется обновить расширение.
Есть пара неожиданностей, связанных с использованием этой функции.
1. В таблице my_ext_table есть столбец id с типом serial, т.е. использующий последовательность для генерации значений. Совершенно не очевидно (но факт), что вызов функции pg_extension_config_dump('my_ext_table'::regclass, '') не обращает на эту последовательность никакого внимания. Поэтому после восстановления из дампа, выборка номеров последовательности my_ext_table_id_seq начнется с 1 и наверняка пересечется с уже существующими номерами в таблице.
2. Это к тому, что текущие значения последовательностей, входящих в состав расширения, pg_dump также должен выгружать. И совершенно неочевидно, что для этого опять нужна функция pg_extension_config_dump.
В нашем случае, если данные таблиц и последовательностей расширения должны выгружаться pg_dump, то скрипт на создание расширения должен выглядеть так:

cat `pg_config --sharedir`/extension/my_ext--1.0.sql
\echo Use CREATE EXTENSION my_ext to load this file. \quit
create table my_ext_table (
   id serial
  ,param_name text
  ,param_value text
);
create view my_ext_params as select param_name, param_value from my_ext_table;

select pg_extension_config_dump('my_ext_table'::regclass, '');
select pg_extension_config_dump('my_ext_table_id_seq'::regclass, '');
 
Расширение, созданное в таком виде, после вставки строк в my_ext_table, заставит pg_dump будет не просто пересоздавать расширение (CREATE EXTENSION), но и выгружать табличные данные (COPY), а также восстанавливать значение последовательности (SETVAL):

db=# drop extension my_ext; 
DROP EXTENSION
db=# create extension my_ext;
CREATE EXTENSION
db=# \! pg_dump db|grep my_ext
-- Name: my_ext; Type: EXTENSION; Schema: -; Owner: 
CREATE EXTENSION IF NOT EXISTS my_ext WITH SCHEMA public;
-- Data for Name: my_ext_table; Type: TABLE DATA; Schema: public; Owner: postgres
COPY my_ext_table (id, param_name, param_value) FROM stdin;
-- Name: my_ext_table_id_seq; Type: SEQUENCE SET; Schema: public; Owner: postgres
SELECT pg_catalog.setval('my_ext_table_id_seq', 1, true);
 
Информация о том, данные каких таблиц расширения должен выгружать pg_dump, сохраняется в системном каталоге pg_extension. В столбце extconfig - список OID таблиц, а в extcondition - список предложений WHERE (2-й параметр функции):
 
db=# select * from pg_extension where extname = 'my_ext';
 extname | extowner | extnamespace | extrelocatable | extversion |   extconfig   | extcondition
---------+----------+--------------+----------------+------------+---------------+--------------
 my_ext  |       10 |         2200 | f              | 1.0        | {25108,25106} | {"",""}

Уровни изоляции транзакций. Oracle vs PostgreSQL

До версии 9.1 в PostgreSQL, также как и в Oracle, можно было выставить два уровня изоляции: READ COMMITED и SERIALIZABLE.
И реализованы они были одинаково.

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

В версии 9.1 эту функциональность добавили в PostgreSQL.
Теперь уровень изоляции SERIALIZABLE обеспечивает полную изоляцию транзакций.
А старый уровень SERIALIZABLE переименовали в REPEATABLE READ.
Именно поэтому он получился строже чем по стандарту, а именно, не допускает фантомного чтения.

В результате получается, что сравнивать уровни изоляции между Oracle и PostgreSQL можно только так:
Oracle
PostgreSQL
READ COMMITEDREAD COMMITED
SERIALIZABLEREPEATABLE READ
N/ASERIALIZABLE

Полезные ссылки
Wiki PostgreSQL:
https://wiki.postgresql.org/wiki/Serializable
https://wiki.postgresql.org/wiki/SSI
Статья Тома Кайта (Oracle):
http://www.oracle.com/technetwork/issue-archive/2010/10-jan/o65asktom-082389.html

lc_messages и ubuntu

Проблема

Изменение параметра lc_messages не приводит к изменению сообщений сервера на Ubuntu.
Т.е. значение параметра меняется, а сообщения сервера выводятся на том языке, которому соответстовала локаль OS при запуске 'pg_ctl start'

Решение

1. Нужно убедиться, что при сборке postgresql была включена опция (./configure --enable-nls[='ru']).
Два способа проверить:
  1. $ pg_config --configure
  2. $ ls -lR `pg_config --localedir`
Должна быть директория 'ru'. Иначе нужно переустанавливать ту же мажорную версию postgresql (только софт, $PGDATA с базами не нужно трогать).
В этой директории находятся файлы с сообщениями на русском языке. Нет файлов на русском языке - ничего не увидим на русском языке.

2. На Ubuntu 14.04 по умолчанию не установлена локаль en_US.UTF-8
Проверить установленные локали:

$ locale -a

Чтобы добавить:

$ sudo locale-gen en_US en_US.UTF-8
$ sudo dpkg-reconfigure locales

Не лишнем будет убедиться, что установлена локаль ru_RU.UTF-8

3. Не нужно устанавливать LANGUAGE.
Переменная среды LANGUAGE задает список языков (в порядке предпочтения) для вывода сообщений программами. Если она установлена, то изменение параметра LC_MESSAGES не приведет к смене языка сообщений.

Итог

При соблюдении этих трех пунктов сообщения сервера будут на языке локали из параметра конфигурации lc_messages.

Создание базы данных с кодировкой koi8r на Ubuntu

1. Проверка установленных в Ubuntu локалей:
 
$ locale -a |grep ru
ru_RU.utf8
ru_UA.utf8

2. Установка локали для koi8r
 
sudo locale-gen ru_RU.KOI8-R

3. Проверяем, что появилась локаль для koi8r
 
$ locale -a |grep ru
ru_RU.koi8r
ru_RU.utf8
ru_UA.utf8

4. Перезапускаем postgres
 
pg_ctl stop ...
pg_ctl start ...

5. Теперь можно создать базу данных с новой кодировкой и обязательно из template0
 
postgres=# create database test_koi8r
             encoding = 'KOI8R'
             lc_collate = 'ru_RU.koi8r'
             lc_ctype = 'ru_RU.koi8r'
             template = template0;
CREATE DATABASE

6. Проверяем
 
postgres=# \l test*
                               List of databases
    Name    | Owner | Encoding |   Collate   |    Ctype    | Access privileges
------------+-------+----------+-------------+-------------+-------------------
 test_koi8r | pal   | KOI8R    | ru_RU.koi8r | ru_RU.koi8r |
(1 row)

CREATE OR REPLACE VIEW

Команда CREATE OR REPLACE VIEW не всегда может пересоздать представление.
Используя вариант CREATE OR REPLACE в представление можно добавлять новые столбцы или изменять алгоритм расчета существующих.
 
postgres=# create view v (col1)
postgres-#    as select 1 as col1;
CREATE VIEW
postgres=# create or replace view v (col1, col2)
postgres-#    as select 11 as col1, 22 as col2;
CREATE VIEW
 
Но попытка переименовать столбец (поставим на первое место col0, вместо col1):
 
postgres=# create or replace view v (col0, col1, col2)
postgres-#    as select 0 as col0, 1 as col1, 2 as col2;
ERROR:  cannot change name of view column "col1" to "col0"

... или изменить тип данных приводят к ошибке:
 
postgres=# create or replace view v (col1, col2)
postgres-#    as select '11'::text as col1, 22 as col2;
ERROR:  cannot change data type of view column "col1" from integer to text 

В таких случаях нужно удалять и заново создавать представление:
 
postgres=# drop view v;
DROP VIEW
postgres=# create or replace view v (col0, col1, col2)
postgres-#    as select 0 as col0, 1 as col1, 2 as col2;
CREATE VIEW

Такая логика работы понятна и документирована:
"The new query must generate the same columns that were generated by the existing view query (that is, the same column names in the same order and with the same data types), but it may add additional columns to the end of the list. The calculations giving rise to the output columns may be completely different."
Но отличается от Oracle, где у CREATE OR REPLACE VIEW таких ограничений нет.

Трассировка работающей сессии

В Oracle у DBA есть возможность включить трассировку для уже работающей сессии.
Например, так:

DBMS_SYSTEM.set_sql_trace_in_session(sid=>123, serial#=>1234, sql_trace=>TRUE);

В результате на сервере появится файл, который можно будет проанализировать.

В PostgreSQL для трассировки уже работающих сессий можно использовать следующий подход.
Для трассировки используются различные конфигурационные параметры, возьмем к примеру log_statement_stats.
Чтобы включить трассировку нужно знать <PID> серверного процесса, к которому нужно подключиться, и выполнить следующие действия:
1. ALTER SYSTEM SET log_statement_stats TO on;
2. Как известно ALTER SYSTEM лишь записывает параметр в postgresql.auto.conf,
но не применяет изменения. Для применения нужно перечитать конфигурацию.
Но перечитать нужно только в серверном процессе, который отлаживаем.
Поэтому вместо утилиты pg_ctl reload и функции pg_reload_conf() используем:
kill -s HUP <PID>
Статистика по всем командам только этого серверного процесса начинает записываться в журнал сервера.
3. Удалим log_statement_stats из postgresql.auto.conf
ALTER SYSTEM RESET log_statement_stats;

Наследование и права на дочерние таблицы

Неожиданный вопрос от слушателей нашего курса DBA1, прошедшего в Омске со 2-го по 4-е декабря 2015г.
Есть две таблицы: родительская и дочерняя. Пользователю выдали права на родительскую таблицу, но не выдали на дочернюю. Обращаясь к родительской таблице, сможет он увидеть строки дочерней?

Моделируем ситуацию

postgres=# create user u1 login;
CREATE ROLE
postgres=# create user u2 login;
CREATE ROLE
postgres=# \c - u1
You are now connected to database "postgres" as user "u1".
postgres=> create table tab (c text);
CREATE TABLE
postgres=> create table tab_child () inherits (tab);
CREATE TABLE
postgres=> insert into tab values ('parent table');
INSERT 0 1
postgres=> insert into tab_child values ('child table');
INSERT 0 1
postgres=> select * from tab;
      c      
--------------
 parent table
 child table
(2 rows)
postgres=> grant select, insert, update, delete on tab to u2;
GRANT
postgres=> \c - u2
You are now connected to database "postgres" as user "u2".
postgres=> select * from tab;
 
Каков будет результат запроса?
  • Только строки из родительской таблицы
  • Ошибка из-за отстутствия прав доступа к дочерней
  • Все записи из обеих таблиц
  • Другое

Проверяем

Итак результат:

postgres=> select * from tab;
      c      
--------------
 parent table
 child table
(2 rows)
 
Не смотря на то, что прав на дочернюю таблицу пользователь не имеет, запрос к родительской таблице вернет все записи, включая записи дочерней таблицы. По такому же принципу, права на update и delete для родительской таблицы, позволяют изменять и удалять записи в дочерней:

postgres=> update tab set c = c ||'!';
UPDATE 2
postgres=> select * from tab;
       c      
---------------
 parent table!
 child table!
(2 rows)
postgres=> delete from tab;
DELETE 2
postgres=> select * from tab;
 c
---
(0 rows)
 
При этом, прямой запрос к дочерней таблицы завершится ошибкой, из-за недостаточных прав:

postgres=> select * from tab_child;
ERROR:  permission denied for relation tab_child

Объяснение

Если результат оказался неожиданным, то не помешает внимательно [пере|про]читать документацию:
“Note how table access permissions are handled. Querying a parent table can automatically access data in child tables without further access privilege checking. This preserves the appearance that the data is (also) in the parent table. Accessing the child tables directly is, however, not automatically allowed and would require further privileges to be granted.”

ALTER ROLE ... SET ROLE ...

Команду SET ROLE можно использовать вместе с ALTER ROLE ... SET или ALTER DATABASE ... SET ...
В документации такой синтаксис не описан, но работает.
В результате, подключаемся под одной ролью и тут же переходим в другую. Если нужно запутаться, то это верная дорога.

postgres=# create role usr1 login;
CREATE ROLE
postgres=# create role usr2 login;
CREATE ROLE
postgres=# grant usr1 to usr2;
GRANT ROLE
postgres=# alter role usr2 set role usr1;
ALTER ROLE
postgres=# \c - usr2
You are now connected to database "postgres" as user "usr2".
postgres=> select session_user, current_user;
 session_user | current_user
--------------+--------------
 usr2         | usr1
(1 row)
postgres=> select * from pg_db_role_setting;
 setdatabase | setrole |  setconfig 
-------------+---------+-------------
           0 |   24803 | {role=usr1}
(1 row)

пятница, 18 марта 2016 г.

psql и ON_ERROR_ROLLBACK

Название этой psql-переменной подсказывает, что если ее установить, то ошибочная команда будет приводить к откату транзакции: при ошибке - откат.
Как-то так:

postgres=# begin;
BEGIN
postgres=# select1+1;
ERROR:  syntax error at or near "select1"
LINE 1: select1+1;
        ^
postgres=# commit;
ROLLBACK


Такое поведение psql сильно раздражает. Из-за любой случайной ошибки при наборе команды приходится терять всю транзакцию.
Только вот переменная ON_ERROR_ROLLBACK не виновница, а наоборот, призвана помочь в таких ситуациях.

postgres=# \set ON_ERROR_ROLLBACK ON
postgres=# begin;
BEGIN
postgres=# select1+1;
ERROR:  syntax error at or near "select1"
LINE 1: select1+1;
        ^
postgres=# commit;
COMMIT


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

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


Строка \set ON_ERROR_ROLLBACK INTERACTIVE определенно заслуживает место в .psqlrc

вторник, 15 марта 2016 г.

Тип данных box и точность ввода координат

Для работы с прямоуголниками в PostgreSQL используется тип данных box. Чтобы задать значение типа box нужно ввести две пары координат противоположных углов прямоугольника.

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

postgres=# select '(4,4),(1,1)'::box @> '(3,3),(2,2)'::box;
 ?column?
----------
 t


И наоборот, маленький не включает в себя большой:

postgres=# select '(4,4),(1,1)'::box <@ '(3,3),(2,2)'::box;
 ?column?
----------
 f


Однако нужно иметь в виду, что точность ввода координат, которую понимает PostgreSQL, составляет 5 знаков после запятой. Поэтому если их синхронно уменьшить в 1 000 000 раз, то маленький прямоугольник может включать в себя большой:

postgres=# select '(0.000004,0.000004),(0.000001,0.000001)'::box
               <@ '(0.000003,0.000003),(0.000002,0.000002)'::box;
 ?column?
----------
 t


Такое некорректное поведение встречается не на всех операциях с "коробками". Но совет здесь один - при работе с прямоугольниками можно полагаться только на точность в пять знаков после запятой.

понедельник, 14 марта 2016 г.

Секционирование и внешние таблицы

В 9.5 внешние таблицы можно включать в наследование. Это позволяет распределить секции по разным источникам, в частности по разным базам данных.

Пример.
postgres@student:~$ psql -a -f fdw.sql
/*
Добавим расширение postgres_fdw в template1,
чтобы оно автоматически устанавливалось в новые БД
*/
\c template1
You are now connected to database "template1" as user "postgres".
create extension if not exists postgres_fdw;
psql:fdw.sql:6: NOTICE:  extension "postgres_fdw" already exists, skipping
CREATE EXTENSION
/*
Создадим две базы: локальную и удаленную
Для целей примера, это просто две БД в одном кластере
*/
drop database if exists db_remote;
DROP DATABASE
drop database if exists db_local;
DROP DATABASE
create database db_remote;
CREATE DATABASE
create database db_local;
CREATE DATABASE
/*
В каждой из баз создадим таблицу t с одинаковой структурой
В локальной БД дополнительно создадим родительскую таблицу t_parent
Это будет наша распределенная таблица
*/
\c db_remote
You are now connected to database "db_remote" as user "postgres".
create table t (id int, dbname text, check (dbname = 'db_remote'));
CREATE TABLE
insert into t values (1, 'db_remote');
INSERT 0 1
\c db_local
You are now connected to database "db_local" as user "postgres".
create table t (id int, dbname text, check (dbname = 'db_local'));
CREATE TABLE
insert into t values (2, 'db_local');
INSERT 0 1
create table t_parent (id int, dbname text);
CREATE TABLE
/*
В локальной БД создадим внешнюю таблицу для таблицы t
в БД db_remote
*/
create server remote_server
   foreign data wrapper postgres_fdw
   options (dbname 'db_remote');
CREATE SERVER
create user mapping for postgres
   server remote_server
   options (user 'postgres');
CREATE USER MAPPING
create foreign table t_remote
   (id int, dbname text)
   server remote_server
   options (schema_name 'public', table_name 't');
CREATE FOREIGN TABLE
/*
Для внешней таблицы также требуется ограничение check для работы секционирования
*/
alter table t_remote add constraint check_db check (dbname = 'db_remote');
ALTER TABLE
/*
Включим наследование и проверим работу секционирования
*/
alter table t_remote inherit t_parent;
ALTER TABLE
alter table t inherit t_parent;
ALTER TABLE
/*
Проверим что видны данные из обеих таблиц:
*/
select tableoid::regclass, * from t_parent;
 tableoid | id |  dbname  
----------+----+-----------
 t        |  2 | db_local
 t_remote |  1 | db_remote
(2 rows)
/*
Проверим работу секционирования
*/
explain select * from t_parent where dbname = 'db_local';
                          QUERY PLAN                          
---------------------------------------------------------------
 Append  (cost=0.00..25.88 rows=7 width=36)
   ->  Seq Scan on t_parent  (cost=0.00..0.00 rows=1 width=36)
         Filter: (dbname = 'db_local'::text)
   ->  Seq Scan on t  (cost=0.00..25.88 rows=6 width=36)
         Filter: (dbname = 'db_local'::text)
(5 rows)
explain select * from t_parent where dbname = 'db_remote';
                              QUERY PLAN                              
-----------------------------------------------------------------------
 Append  (cost=0.00..127.20 rows=8 width=36)
   ->  Seq Scan on t_parent  (cost=0.00..0.00 rows=1 width=36)
         Filter: (dbname = 'db_remote'::text)
   ->  Foreign Scan on t_remote  (cost=100.00..127.20 rows=7 width=36)
(4 rows)