【问题标题】:PostgreSQL query not using INDEX when RLS (Row Level Security) is enabled启用 RLS(行级安全性)时,PostgreSQL 查询不使用 INDEX
【发布时间】:2018-06-22 04:02:16
【问题描述】:

我正在使用 PostgreSQL 10.1,切入正题...

假设我有一个 TABLE

CREATE TABLE public.document (
    id uuid PRIMARY KEY,

    title   text,
    content text NOT NULL
);

加上GIN INDEX

CREATE INDEX document_idx ON public.document USING GIN(
    to_tsvector(
        'english',
        content || ' ' || COALESCE(title, '')
    )
);

还有一个基本的全文搜索查询:

SELECT * FROM public.document WHERE (
    to_tsvector(
        'english',
        content || ' ' || COALESCE(title, '')
    ) @@ plainto_tsquery('english', fulltext_search_documents.search_text)
)

不管 public.document 表的大小如何,查询(您已经知道)非常快!规划师使用 INDEX,一切顺​​利。

现在我通过RLS(Row Level Security)介绍一些基本的访问控制,首先我启用它:

ALTER TABLE public.document ENABLE ROW LEVEL SECURITY;

然后我添加策略:

CREATE POLICY document_policy ON public.document FOR SELECT
    USING (EXISTS (
        SELECT 1 FROM public.user WHERE (is_current_user) AND ('r' = ANY(privileges))
    ));

为了简单起见,is_current_user 是另一个可以准确检查的查询。

现在 全文搜索查询document_policy 查询 展平,这样规划器执行 Seq Scan 而不是 Index扫描会导致查询速度降低 300 倍!

我认为这个问题很明显,我该如何解决这个问题,以便全文搜索查询保持快速?

提前致谢!

【问题讨论】:

  • 我认为这是 RLS 的已知限制。

标签: sql database postgresql row-level-security


【解决方案1】:

我从发帖的时候就解决了这个问题...任何遇到这个问题的人,我就是这样做的:

我的解决方案是有一个 private SECURITY DEFINER "wrapper" 函数,其中包含 propper 查询和另一个调用 privatepublic 函数一和INNER JOINS需要访问控制的表。

所以在上面的特定情况下,它会是这样的:

CREATE FUNCTION private.filter_document() RETURNS SETOF public.document AS
$$
    SELECT * FROM public.document WHERE (
        to_tsvector(
            'english',
            content || ' ' || COALESCE(title, '')
        ) @@ plainto_tsquery('english', fulltext_search_documents.search_text)
    )
$$
LANGUAGE SQL STABLE SECURITY DEFINER;
----
CREATE FUNCTION public.filter_document() RETURNS SETOF public.document AS
$$
    SELECT filtered_d.* FROM private.filter_documents() AS filtered_d
        INNER JOIN public.document AS d ON (d.id = filtered_d.id)
$$
LANGUAGE SQL STABLE;

由于我使用的是Postgraphile(这是超级棒顺便说一句!),我能够省略 private 模式的自省,使“危险”功能人迹罕至!通过适当的安全实施,最终用户将只能看到最终的 GraphQL 架构,从而将 Postgres 从外部世界中移除。

这很好用! 直到最近 Postgres 10.3 发布并修复它,不再需要这种 hack。

另一方面,我的 RLS 策略非常复杂、嵌套且深入。它们运行的​​表也非常大(总共大约有 50,000 多个条目要运行 RLS)。即使使用超级复杂和嵌套的策略,我也设法将性能保持在合理的范围内。

使用 RLS 时,请记住以下几点:

  1. 创建正确的INDEXES
  2. 在任何地方都喜欢内联查询! (即使这意味着将相同的查询重写 N 次)
  3. 一定要避免策略中的函数!如果您绝对必须将它们放在里面,请确保它们是STABLE 并且具有较高的COST(就像@mkurtz 指出的那样);或者是IMMUTABLE
  4. 从策略中提取查询,直接使用EXPLAIN ANALYZE 运行它并尝试尽可能优化它

希望你们和我一样觉得这些信息很有帮助!

【讨论】:

    【解决方案2】:

    尝试以下操作: 与其将查询写入USING(...) 子句,不如将查询写入STABLE 函数,代价非常高。 通过这样做,现在不应该经常调用该函数 - 理想情况下每个查询生命周期只调用一次,因为调用该函数的成本现在对 Postgres 来说似乎非常高。将函数标记为STABLE 告诉 Postgres 该函数的结果在单个查询生命周期内不会改变。我认为这对您的查询是正确的,不是吗? 阅读更多关于这两个参数的信息here

    像这样:

    CREATE OR REPLACE FUNCTION check_permission () RETURNS BOOLEAN AS $$
        SELECT EXISTS (
            SELECT 1 FROM public.user WHERE (is_current_user) AND ('r' = ANY(privileges))
        )
    $$ LANGUAGE SQL STABLE COST 100000;
    

    现在的政策:

    CREATE POLICY document_policy ON public.document FOR SELECT
        USING (check_permission());
    

    希望这会给您带来更好的性能。但请注意,只有在可以将函数标记为STABLE 时才能正常工作。如果您的函数可以在单个查询生命周期内返回不同的结果,那么这将无法正常工作,您最终会得到奇怪的结果。

    【讨论】:

    • 感谢@mkurz 的回复!我知道 COST “hack”,当您需要一小部分策略时,这很好。但是(至少从 Postgres 10.3+ 开始)在上面的示例中,使用内联查询(关于这种特定情况)再次更有效。是的,我需要我的策略是超级动态的,所以单一的高成本函数是行不通的。
    • 但另一方面,我从发帖时就已经解决了这个问题。我的解决方案是有一个 private SECURITY DEFINER "wrapper" 函数包含查询和另一个 public 函数调用 private 和 @987654329 @需要访问控制的表。
    • @enisdenjo 有趣!您能否使用包括 sql sn-ps (或其他答案)在内的解决方案更新您的问题?我也对我们下一个应用程序的行级安全性感兴趣,并且发现您的方法很有趣。谢谢!
    • 嘿@mkurz,您可以在我的回答中找到解释和示例。希望对你有帮助!
    • @enisdenjo 太棒了!非常感谢 - 我很感激你花时间写下来:)
    猜你喜欢
    • 2021-05-19
    • 2020-07-21
    • 2020-12-16
    • 1970-01-01
    • 2020-10-25
    • 2013-02-03
    • 1970-01-01
    • 1970-01-01
    • 2021-09-01
    相关资源
    最近更新 更多