Кажется, что postgres с включенной безопасностью на уровне строк в таблице не может должным образом оптимизировать запросы. Следующий пример sql демонстрирует проблему:
begin transaction;
-- create demo table
create table entries (id int primary key);
-- setup rls
alter table entries enable row level security;
create policy entries_policy on entries for all using (
current_setting('app.can_read_entries')::boolean = true
);
-- setup a role which cannot bypass rls
create role authenticated;
grant usage on schema public to authenticated;
grant select on all tables in schema public to authenticated;
-- configure setting (just as an example for a query condition)
set local app.can_read_entries = true;
rollback;
Если не используется RLS:
explain
select * from entries
where (
-- Simulate RLS query as superuser which bypasses RLS by adding the RLS condition manually
current_setting('app.can_read_entries')::boolean = true
);
-- Result (cost=0.01..35.51 rows=2550 width=4)
-- One-Time Filter: (current_setting('app.can_read_entries'::text))::boolean
-- -> Seq Scan on entries (cost=0.01..35.51 rows=2550 width=4)
тогда как с безопасностью на уровне строк это будет
-- switch to 'authenticated' role to use rls in queries
set local role 'authenticated';
-- run the query with rls inlining its conditions
explain
select * from entries;
-- Seq Scan on entries (cost=0.00..54.63 rows=1275 width=4)
-- Filter: (current_setting('app.can_read_entries'::text))::boolean
В первом запросе без rls оптимизатор правильно распознает, что условие является статическим для запроса, и извлекает его как проверку однократного фильтра. Но второй запрос, в котором условие предоставляется rls, не оптимизируется полностью, и postgres будет оценивать условие для каждой строки, даже если оно является постоянным для всего запроса.
В этом примере данных немного, а запрос простой, однако он довольно легко демонстрирует проблему, и такое же поведение можно наблюдать в таблицах с большим количеством данных, индексов и т. д. Более подробный пример описан здесь: https: //pastebin.com/iQu6L5Sj
Я уже читал об атрибуте герметичности функций и пробовал установить атрибут герметичности current_setting
и даже пошел и попробовал update pg_proc set proleakproof = true;
.
Однако, что бы я ни пытался, postgres не будет должным образом оптимизировать запрос с безопасностью на уровне строк.
Обновлено: Еще один пример, в котором не используется такая функция, как current_setting
:
-- additionally create a users table
create table users (id int primary key);
-- and then use this policy
create policy entries_policy on entries for all using (
exists (select 1 from users where id = 1)
-- current_setting('app.can_read_entries')::boolean = true
);
Без RLS он будет работать как
Result (cost=8.17..43.67 rows=2550 width=4)
One-Time Filter: $0
InitPlan 1 (returns $0)
-> Index Only Scan using users_pkey on users (cost=0.15..8.17 rows=1 width=0)
Index Cond: (id = 1)
-> Seq Scan on entries (cost=8.17..43.67 rows=2550 width=4)
а с помощью RLS он будет напрямую выполнять сканирование последовательности вместо использования раннего возврата:
Seq Scan on entries (cost=8.17..43.67 rows=1275 width=4)
Filter: $0
InitPlan 1 (returns $0)
-> Index Only Scan using users_pkey on users (cost=0.15..8.17 rows=1 width=0)
Index Cond: (id = 1)
1) Используйте EXPLAIN ANALYZE
. 2) Запросы отличаются тем, что в первом есть WHERE
, а во втором нет. 3) Оба запроса выполняют последовательное сканирование.
Снова используйте EXPLAIN ANALYZE
. Также включите запрос, который вы использовали для создания вывода EXPLAIN
. Кроме того, количество строк в таблице невелико, поэтому оптимизация действительно не требуется.
@AdrianKlaver Используемый запрос объяснения приведен в примере sql. внизу. И да, таблица в данном случае пустая, но даже пустой таблицы достаточно, чтобы уже показать проблему. Добавление к нему данных и индексов или использование более сложных запросов по-прежнему приведет к тому же поведению оптимизатора, который не извлекает статические условия из запросов RLS.
1) Вы изменили условие выполнения RLS, и показанные вами примеры запросов относятся к предыдущему условию. Если у вас есть надежда получить ответ, вам необходимо предоставить отдельный пример, показывающий последовательную последовательность шагов. 2) Цель планировщика — привести план в соответствие с содержимым таблицы. Работа с практически пустой таблицей — нереалистичный случай.
@AdrianKlaver Я думаю, это могло быть неясно, но первый запрос выполняется от имени суперадминистратора и, следовательно, игнорирует RLS. Вот почему я вручную вставил условие, которое запрос RLS будет использовать. Затем второй запрос выполняется с ролью «аутентифицированный», которая использует RLS, поэтому я пропустил запрос.
Какие строки в вашей таблице entries
должны быть доступны для роли authenticated
и почему? А строк вообще нет, поэтому последовательное сканирование — лучший план выполнения запроса.
@FrankHeikens Я обновил описание, чтобы объяснить его немного лучше. Конечно, последовательное сканирование — самое быстрое решение. Но проблема скорее в том, что rls НЕОБХОДИМО выполнить последовательное сканирование, тогда как без rls он полностью пропустит последовательное сканирование из-за оптимизации запросов. Код — это всего лишь минимальный пример воспроизведения и, конечно, не реальный случай.
Да, запросы, использующие безопасность на уровне строк, не будут оптимизированы так же, как запросы без нее. PostgreSQL будет выполнять только те оптимизации, которые, как он знает, безопасны. Поскольку возможности оптимизатора по рассуждению ограничены, иногда это означает, что он выбирает план, который не будет идеальным.
В вашем конкретном случае запросы будут выполняться почти одинаково, если пользователю разрешено видеть данные. Но в «запрещенном» случае есть отличие: запрос RLS все равно будет сканировать таблицу. Ваша проблема в том, что приведение от text
к boolean
, которое выполняется с помощью функции ввода типа boolin()
, не является герметичным.
Да, они БУДУТ делать то же самое, но в определенных сценариях это окажет огромное влияние на производительность. Например, даже простой запрос count(*) может быть выполнен по индексу практически мгновенно без rls, но потребует последовательного сканирования с помощью rls. Имхо, это огромное влияние. Я предполагаю, что это означает, что rls вообще непригоден для нашего случая. К вашему сведению: я разрабатываю приложение на супабазе, и оно по сути использует только RLS для управления всеми разрешениями доступа. Это означает, что мне придется отказаться от всего этого стека и переключиться на полный уровень API ¯_(ツ)_/¯
Вы хотите сказать, что планы на самом деле выполняются по-другому? Не могли бы вы отредактировать вопрос и добавить вывод EXPLAIN (ANALYZE)
для таких запросов?
@олее. Вы впервые упомянули, что используете супабазу, и вам действительно нужно добавить это к своему вопросу.
Что вы определяете для
optimize
? Ваша таблица пуста, и вы не поделились результатамиexplain(analyze, verbose, buffers, settings)
, поэтому мы все равно не видим никаких проблем.