воскресенье, 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)