вторник, 2 февраля 2016 г.

5 полезных приёмов для программиста PostgreSQL





1. СТРОГОЕ  СРАВНЕНИЕ ЗНАЧЕНИЙ В ЗАПРОСАХ

Есть к примеру запрос, который обновляет некоторые поля данными из исходной таблицы в случае их отличия от данных в таблице назначения:


update dest
set
name=src.name
from src
where dest.id=src.id
and
dest.name<>src.name;


Проблема заключается в том, что одно из полей name в таблицах dest или srcможет быть NULL, а следовательно не может обрабатываться оператором сравнения <>. В таком случае поле в таблице назначения src обновлено не будет.Раньше для решения этой проблемы я пользовался функциeй проверки значения на NULL - coalesce. Мой код выглядел неоптимально:


update dest
set
name=src.name
from src
where dest.id=src.id
and
coalesce(dest.name,'')<>coalesce(src.name,'');

Если обновление происходит для нетекстового поля, то вместо приведения к пустой строке в случае NULL, нужно приводить к 0. Пример:

update dest
set
numcol=src.numcol
from src
where dest.id=src.id
and
coalesce(dest.numcol ,0)<>coalesce(src.numcol ,0);

Постоянно писать проверку на NULL для каждого обновляемого поля утомительно.
Поэтому я открыл для себя  оператор  IS DISTINCT FROM.
Данный оператор по своей функции схож с оператором <>, и возвращает true, если правая и левая части разные и false, если они одинаковые ( с учетом NULL в этих значениях).
Оператор IS NOT DISTINCT FROM похож на оператор =, но также учитывает с NULL.

Проиллюстрирую работу этих операторов на примерах:

select NULL IS DISTINCT FROM  NULL; --Результат false
select NULL IS NOT DISTINCT FROM  NULL; --Результат true
select NULL IS NOT DISTINCT FROM  10; --Результат false
select NULL IS NOT DISTINCT FROM  'abc'; --Результат false
select 10 IS NOT DISTINCT FROM  10; --Результат true
select 'abc' IS DISTINCT FROM  'abc'; --Результат false


Таким образом, оптимизируя код первого запрос, я написал следующее:

update dest
set
name=src.name
from src
where dest.id=src.id
and
dest.name IS DISTINCT FROM  src.name;


IS DISTINCT FROM или IS NOT DISTINCT FROM следует использовать вместо <> или = там, где в сравниваемых значениях возможны NULL.


2. ЗАМЕДЛЕНИЕ РАБОТЫ ЗАПРОСА


Иногда при работе некоторых функций возникает взаимоблокировка транзакций (deadlock).  
По логам видно, что 2 функции вызвали взаимоблокировку. При этом одна из них может достаточно быстро отрабатывать свою задачу и запустив их в разных окнах PgAdmin можно не получить deadlock.  В таких ситуация возникает потребность замедлить работу функции или запроса. 
Так я открыл для себя функцию pg_sleep(nsec). Функция замедляет выполнения запроса, на nsec секунд. Таким образом, чтобы замедлить мою исходную тестируемую функцию MyFunc на 10 секунд можно выполнить следующий код:

select *,pg_sleep(10) from MyFunc();



3. РЕАЛИЗАЦИЯ ЗАПРОСОВ В ФУНКЦИЯХ В ЗАВИСИМОСТИ ОТ ЗНАЧЕНИЙ ВХОДНЫХ ПАРАМЕТРОВ ФУНКЦИИ



Представим себе функцию CountOfGoodsByBrandID, которая на вход принимает параметр v_brand_id. Назначение функции - вычислять количество товаров в таблице goods, принадлежащих указанному бренду-параметру.
Однако если функция принимает в качестве v_brand_id=0, то необходимо выдать общее количество товаров.
Напишем код функции с учетом этого нюанса:

create or replace function public.CountOfGoodsByBrandID(v_brand_id int default 0)
RETURNS int
security definer
as $$
declare
result int;
BEGIN

if v_brand_id<>0 then
select count(1) into result from goods where brand_id=v_brand_id;
else
select count(1) into result from goods;
end if;

return result;
END;
$$ language plpgsql;

Для поддержки различной логики в зависимости от значения v_brand_id приходится реализовывать громоздкую 
конструкцию:

if v_brand_id<>0 then
select count(1) into result from goods where brand_id=v_brand_id;
else
select count(1) into result from goods;
end if;

помимо длинной реализации проблема заключается в потенциальной возможности усложнения кода за счет появления  еще одного   входного параметра, который как может иметь какое-либо значение, или же можеть быть NULL. Добавим в функцию входной параметр v_is_domestic (значение 1 -  товар отечественный, 0 - импортный, NULL - выборка всех товаров). Тогда реализация логики выбора запроса будет следующей:

if (v_brand_id<>0 and v_is_domestic is not null) then

select count(1) into result from goods
where brand_id=v_brand_id and is_domestic=v_is_domestic;

ELSIF (v_brand_id<>0 and v_is_domestic is  null) then

select count(1) into result from goods
where brand_id=v_brand_id;

ELSIF (v_brand_id=0 and v_is_domestic is not null) then

select count(1) into result from goods
where is_domestic=v_is_domestic;

ELSIF (v_brand_id=0 and v_is_domestic is  null) then

select count(1) into result from goods;

end if;

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

1)
select count(1) into result from goods
where
((brand_id=v_brand_id and v_brand_id<>0) or (v_brand_id=0))
and
((is_domestic=v_is_domestic and v_is_domestic is not null) or (v_is_domestic is null));
;


Количество кода существенно сократилось. Вся логика обработки параметров функции перешла в тело запроса.
В качестве альтернативы данному запросу можно написать запрос с использованием оператора CASE:

2) 
select count(1) into result from goods where
case
when (v_brand_id=0 and v_is_domestic is null) then 1=1
when (v_brand_id<>0 and v_is_domestic is null) then brand_id=v_brand_id
when (v_brand_id=0 and v_is_domestic is not null) then is_domestic=v_is_domestic
else
brand_id=v_brand_id and is_domestic=v_is_domestic
end;


Он длинее, чем предыдущий, и более понятен. Однако для профессионального программирования функции с поддержкой различных значений параметров я использую реализацию 1 (см. выше).
Таким образом, наша функция после оптимизации логики будет иметь следующий вид:
drop function if exists public.CountOfGoodsByBrandID(int,int);
create or replace function public.CountOfGoodsByBrandID(v_brand_id int default 0,v_is_domestic int default NULL)
RETURNS int
security definer
as $$
declare
result int;
BEGIN

select count(1) into result from goods where
((brand_id=v_brand_id and v_brand_id<>0) or (v_brand_id=0))
and
((is_domestic=v_is_domestic and v_is_domestic is not null) or (v_is_domestic is null));


return result;
END;
$$ language plpgsql;



4. КОНКАТЕНАЦИЯ ЗНАЧЕНИЙ ПАРАМЕТРОВ В ДИНАМИЧЕСКОМ SQL 


В своих функциях я часто использую динамически формируемые запросы. Значения в текст таких запросов добавляются с помощью оператора конкатенации  (||)

Пример:

EXECUTE 'SELECT id,name,brand_id from public.goods where brand_id=' || v_brand_id::text || ' and is_domestic=' || v_is_domestic::text || ';'

Код запроса здесь соседствует с параметрами и при увеличении количества параметров или текста кода запроса  возрастает сложность поддержки. Хотелось бы функцию, позволяющую отделить текста запроса от подставляемых в него значений, как например sprintf в PHP. И такая замечательная функция есть (format) , вот реализация формирования динамического sql c ее помощью:

EXECUTE format('SELECT id,name,brand_id from public.goods where brand_id=%s  and is_domestic=%s;',v_brand_id::text,v_is_domestic::text);

Благодаря format, поддержка и понимание кода упрощается.


5. ВЫПОЛНЕНИЕ ФУНКЦИЙ ПОД ПРАВАМИ СОЗДАТЕЛЯ


Иногда приходится модифицировать код sql или pl/pgsql -функций и выкладывать их на рабочем сервере, для того, чтобы пользователи сайта могли запускать данные функции. Конечно пользователи запускают функции не напрямую, а делая запросы к сайту, взаимодействуя с интерфейсом сайта.

Сайт подключен к БД под учеткой с ограниченными правами. Это делается для того, чтобы злоумышленники, найдя дыру в безопасности сайта, не смогли провести атаку типа sql-injection.
Функции же создаются и обновляются под учеткой с админскими правами на сервере Postgresql.
Когда учетка сайта обращается к данной функции - возникает ошибка доступа. Даже если владельцем функции назначена учетка сайта, то все равно возникает проблема, т.к. функция при выполнении может использовать таблицы и другие объекты, доступ к которым для учетки сайта закрыт.
Поэтому, чтобы избежать таких ошибок с правами учетки, я все функции создаю и обновляю с директивой SECURITY DEFINER.
Данная директива говорит о том, что права на доступ к объектам БД функция при работе берет не от запустившей ее учетки, а от создавшей ее учетки.

CREATE FUNCTION see_goods() RETURNS SETOF goods AS $$

SELECT * FROM goods

$$ LANGUAGE SQL SECURITY DEFINER;


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

Каталог блогов Blogolist