sql
Особенности использования индексов в PostgreSQL
Вчера под ночь боролся с «Постгресом» (это такая СУБД), в итоге поборол, но не так, как хотелось бы.
SELECT id, CASE WHEN RANDOM()>.5 THEN value END AS value
INTO test
FROM generate_series(1, 1000000) WITH ORDINALITY AS test(id, value)В тестовой таблице два поля — идентификатор (id) и значение (value), примерно половина значений в поле value — NULL. И теперь нам надо создать индекс для запросов, которые выглядит примерно так:
SELECT id FROM test WHERE value IS NULL ORDER BY id;
SELECT id FROM test WHERE value IS NOT NULL ORDER BY id;План для них выглядит одинаково печально, что логично — на таблице ни одного индекса:
Sort (cost=61739.93..62977.68 rows=495100 width=4) (actual time=47289.775..47761.235 rows=498778 loops=1)
Sort Key: id
Sort Method: external merge Disk: 6848kB
-> Seq Scan on test (cost=0.00..14910.00 rows=495100 width=4) (actual time=12.039..40854.731 rows=498778 loops=1)
Filter: (value IS NULL)
Rows Removed by Filter: 501222
Planning Time: 0.174 ms
Execution Time: 48134.553 msЧтобы сэкономить место, мне очень хотелось сделать индекс функциональным:
CREATE UNIQUE INDEX ON test((value IS NULL), id);Таким образом вместо целого поля будет использован логический тип всего с двумя значениями.
Конечно
Первый запрос ожидаемо улучшился:
Index Scan using test_expr_id_idx on test (cost=0.42..33882.43 rows=495100 width=4) (actual time=0.056..519.584 rows=498778 loops=1)
Index Cond: ((value IS NULL) = true)
Planning Time: 0.182 ms
Execution Time: 894.551 msА второй — нет:
Sort (cost=62738.26..64000.51 rows=504900 width=4) (actual time=1046.582..1471.180 rows=501222 loops=1)
Sort Key: id
Sort Method: external merge Disk: 6880kB
-> Seq Scan on test (cost=0.00..14910.00 rows=504900 width=4) (actual time=0.033..522.071 rows=501222 loops=1)
Filter: (value IS NOT NULL)
Rows Removed by Filter: 498778
Planning Time: 0.136 ms
Execution Time: 1841.282 msНесмотря на то, что в индексе содержится информация, достаточная для выполнения этого запроса, в данном случае «Постгрес» не понимает, что VALUE IS NOT NULL эквивалентно (VALUE IS NULL) = false.
Причём, если попробовать переписать запрос, чтобы условие выборки выглядело как (VALUE IS NULL) = false, то тут оптимизатор не оплошает — перепишет за нас условие в виде VALUE IS NOT NULL и опять не будет узнавать его в индексе.
Я вижу несколько путей решения этой проблемы, все с недостатками.
CREATE UNIQUE INDEX ON test((CASE WHEN value IS NULL THEN TRUE ELSE FALSE END), id);Недостатки очевидны: громоздкость конструкции, кроме того, легко забыть, что условия для этой таблицы надо писать именно так. Можно поместить эту конструкцию внутрь функции, но это устранит только первый недостаток.
CREATE UNIQUE INDEX ON test((value IS NULL), id);
CREATE UNIQUE INDEX ON test((value IS NOT NULL), id);Недостаток: у нас два индекса — при вставке или обновлении будем писать в два места, раздуваться (bloat) у нас будут тоже два индекса вместо одного.
CREATE UNIQUE INDEX ON test(value, id);Фатальный недостаток: индекс, по сути, бесполезен — он содержит всю таблицу целиком, «Постгрес» скорее всего выберет последовательное сканирование таблицы, а индекс использовать не будет.
И, наконец, вариант, который мне нравится в этой ситуации больше всего — частичные индексы:
CREATE UNIQUE INDEX ON test(id) WHERE value IS NULL;
CREATE UNIQUE INDEX ON test(id) WHERE value IS NOT NULL;Да, у нас опять два индекса, но они довольно компактны по размеру: в каждом примерно половина данных, кроме того, в нём вообще только одно поле — второго нет вовсе, его заменяет условие индекса.
Добавлено позднее: мне тут подсказывают, что нужно ещё учитывать селективность. Это верно, но мне совершенно не хотелось затрагивать ещё и эту тему. В реально задаче в индексе больше полей.
SELECT [DEFER] * FROM…
Я тут подумал, что было бы очень круто как помечать запросы флагом, который бы говорил СУБД «если такой же запрос уже выполняется, то меня устроят его результаты, новый запускать не надо». Было бы очень полезно для запросов, в которых не нужна оперативность и которые заказываются большим числом пользователей.
Конечно такое можно кешировать со стороны приложения (что у нас и делается в продукте), но встают вопросы инвалидации кеша, предотвращения одновременного выполнения одного и того же запроса несколько раз, если в кеше
Со стороны СУБД было бы интереснее такое увидеть.
POSTQUEL
POSTQUEL — это язык запросов, который
POSTQUEL расшифровывается как «POSTgres QUEry Language», «язык запросов Постгреса», он был разработам в 1985 году в Калифорнийском университете в Беркли под руководством Майкла Стоунбрейкера, POSTQUEL основан на языке запросов QUEL, который использовался на тот момент в БД «Ингрес».
Примеры того на что был похож POSTQUEL есть в польской Википедии:
Получить размер заработной платы сотрудника Ковальски (pracownicy — сотрудники, płace — зарплата):
retrieve (PRACOWNICY.placa) from PRACOWNICY where PRACOWNICY.nazwisko = "Kowalski"Все сотрудники старше 40 лет (wiek — возраст, nazwisko — имя):
retrieve (P.nazwisko) from P in PRACOWNICY where P.wiek > 40Найти все департаменты, целиком занимающие один этаж (pietro — этаж, dnazwa — сокращение от «название департамента»):
retrieve (DEPART.dnazwa)
where DEPART.pietro NOT-IN {D.pietro from D in DEPART where D.dnazwa != DEPART.dnazwa}«Постквел» был более формализованным языком, чем Эскюэль, все команды строились по одному образцу и не допускали вариаций. Других подробностей я не нашёл, к сожалению, детального описания языка мне обнаружить пока не удалось, было бы любопытно посмотреть на него и проанализировать сильные стороны.
Добавлено позднее. Я обнаружил документацию 1992 года на «Постгрес», где довольно подробно упоминается и «Постквел» (ссылка есть в комментариях). Интересный
Например, оперировал он объектами, поддерживал наследование (все примеры из упомянутой документации):
create STUD_EMP (location = point) inherits (EMP)Поддерживал массивы (индексы начинаются с единицы):
create SAL_EMP (name = char[], pay_by_quarter = int4[4])
append SAL_EMP (name = "bill", pay_by_quarter="{10000, 10000, 10000, 10000}")
retrieve (SAL_EMP.name) where SAL_EMP.pay_by_quarter[1] !=SAL_EMP.pay_by_quarter[2]Поддерживал историю, вот, например, срез данных с первого января 1970 по наши дни:
retrieve (E.salary)
from E in EMP["Jan 1 00:00:00 1970 GMT", "now"]
where E.name = "Sam"Была возможность создавать пользовательские типы (тут создаётся новый тип массива):
define type int_array
(element = int4, internallength = variable,
input = array_in, output = array_out)Так же как и пользовательские функции:
define function eq_area_circle
(language = "c", returntype = bool)
arg is (circle, circle)
as "/usr/postgres/tutorial/circle.o"
define function manager
(language = "postquel", returntype = EMP)
arg is (EMP)
as "retrieve (E.all) from E in EMP
where E.name = DEPT.manager
and DEPT.name = $1.dept"
retrieve (EMP.name)
where name(manager(EMP)) = "Joe"Была даже перегрузка операторов!
define operator =
(arg1 = circle, arg2 = circle,
procedure = eq_area_circle)И так далее.
Поиск пропусков в таблице
У нас тут сегодня на работе возникла гипотетическая потребность найти пропуски в конкретной таблице базы данных. Так как я кроме как с MySQL в последнее время ни с чем не имел, прикинул как бы я решил эту задачу там. Получилось
SELECT id FROM (
SELECT id, id-@prev AS diff, @prev:=id
FROM
(SELECT @prev:=NULL) ``,
sourceofdata
ORDER BY id
) `` WHERE diff>1Внутри есть напрасный запрос «SELECT @prev:=NULL», который там только затем, чтобы решение получилось в одну строку.
Может