Обсуждение: RLS creates inaccurate limit and offset results

Поиск
Список
Период
Сортировка

RLS creates inaccurate limit and offset results

От
mike@mikebrancato.com
Дата:
Hello,

I have noticed that using row level security can cause use of `LIMIT` and `OFFSET` to return inconsistent results
withoutthe use of an explicit `ORDER BY`. 

Version info:
testdb=> select version();
                                                         version

--------------------------------------------------------------------------------------------------------------------------
 PostgreSQL 17.6 (Debian 17.6-2.pgdg13+1) on aarch64-unknown-linux-gnu, compiled by gcc (Debian 14.2.0-19) 14.2.0,
64-bit
(1 row)

For the example below, I’ll pretend we have a role “user” that is authenticated and has the GRANT for SELECT on the
table.

Sample data:

CREATE TABLE IF NOT EXISTS "organization"
(
"id" UUID DEFAULT gen_random_uuid(),
"name" TEXT NOT NULL,
"created_at" TIMESTAMPTZ NOT NULL DEFAULT NOW(),
PRIMARY KEY ("id")
);

COPY public.organization (id, name, created_at) FROM stdin;
db8d12e7-faac-4b6a-a4f1-127c1da8b297    Test    2025-11-11 15:57:29.323547+00
11111111-1111-1111-1111-111111111111    Acme Corporation    2025-11-11 15:57:29.324394+00
22222222-2222-2222-2222-222222222222    Beta Industries    2025-11-11 15:57:29.325026+00
33333333-3333-3333-3333-333333333333    Gamma Labs    2025-11-11 15:57:29.325643+00
\.

If I have a RLS policy like this:
CREATE POLICY organization_isolation_policy
    ON public.organization
    FOR SELECT USING (
    (id = ANY
     (
         ARRAY [
             '11111111-1111-1111-1111-111111111111'::uuid,
             '22222222-2222-2222-2222-222222222222'::uuid,
             '33333333-3333-3333-3333-333333333333'::uuid,
             'db8d12e7-faac-4b6a-a4f1-127c1da8b297'::uuid]
         )
        )
    );

Then using a `LIMIT 1 OFFSET 0` and `LIMIT 1 OFFSET 1` there is no difference in the data returned, though the latter
shouldbe shifted by 1 row in the result set: 
testdb=> SELECT * FROM organization LIMIT 1;
                  id                  |       name       |          created_at
--------------------------------------+------------------+-------------------------------
 11111111-1111-1111-1111-111111111111 | Acme Corporation | 2025-11-11 15:57:29.324394+00
(1 row)

testdb=> SELECT * FROM organization LIMIT 1 OFFSET 0;
                  id                  |       name       |          created_at
--------------------------------------+------------------+-------------------------------
 11111111-1111-1111-1111-111111111111 | Acme Corporation | 2025-11-11 15:57:29.324394+00
(1 row)

testdb=> SELECT * FROM organization LIMIT 1 OFFSET 1;
                  id                  |       name       |          created_at
--------------------------------------+------------------+-------------------------------
 11111111-1111-1111-1111-111111111111 | Acme Corporation | 2025-11-11 15:57:29.324394+00
(1 row)

Using `LIMIT 2 OFFSET 0` as the RLS user:
testdb=> SELECT * FROM organization LIMIT 2 OFFSET 0;
                  id                  |       name       |          created_at
--------------------------------------+------------------+-------------------------------
 db8d12e7-faac-4b6a-a4f1-127c1da8b297 | Test             | 2025-11-11 15:57:29.323547+00
 11111111-1111-1111-1111-111111111111 | Acme Corporation | 2025-11-11 15:57:29.324394+00
(2 rows)

If I remove the “Test” org ID from the RLS policy, then things seem to be correct (this is a very small dataset,
obviously).If I include any `ORDER BY` clause, then the results seem to be correct regardless of RLS policy. 


Without RLS (e.g. superuser):
testdb=# SELECT * FROM organization LIMIT 1 OFFSET 0;
                  id                  |       name       |          created_at
--------------------------------------+------------------+-------------------------------
 db8d12e7-faac-4b6a-a4f1-127c1da8b297 | Test             | 2025-11-11 15:57:29.323547+00
(1 row)

testdb=# SELECT * FROM organization LIMIT 2 OFFSET 0;
                  id                  |       name       |          created_at
--------------------------------------+------------------+-------------------------------
 db8d12e7-faac-4b6a-a4f1-127c1da8b297 | Test             | 2025-11-11 15:57:29.323547+00
 11111111-1111-1111-1111-111111111111 | Acme Corporation | 2025-11-11 15:57:29.324394+00
(2 rows)


—
Mike Brancato




Re: RLS creates inaccurate limit and offset results

От
Christophe Pettus
Дата:

> On Nov 11, 2025, at 08:29, mike@mikebrancato.com wrote:
> I have noticed that using row level security can cause use of `LIMIT` and `OFFSET` to return inconsistent results
withoutthe use of an explicit `ORDER BY`. 

LIMIT and OFFSET without ORDER BY are not guaranteed to return consistent results even without RLS.  RLS may cause it
bemore obvious, however. 


Re: RLS creates inaccurate limit and offset results

От
mike@mikebrancato.com
Дата:
On Nov 11, 2025, at 5:10 PM, Christophe Pettus <xof@thebuild.com> wrote:

LIMIT and OFFSET without ORDER BY are not guaranteed to return consistent results even without RLS.  RLS may cause it be more obvious, however.

If I understand the documentation (https://www.postgresql.org/docs/current/queries-limit.html), inconsistent results for different LIMIT / OFFSET values is only a known issue when selecting different subsets of data. Here, I’m seeing inconsistent results on the same subset of data. e.g. OFFSET 0 and OFFSET 1 return the same row without changing the overall query / WHERE clause or LIMIT. This claim seems to hold true without RLS, but with RLS, it adds at least this inconsistent edge case. If this is acceptable, I’d suggest documenting this as a known limitation of RLS that doesn’t require different LIMIT/OFFSET values and different subsets to result in inconsistently ordered data - I’d suspect its due to the POLICY checks that occur with accessing that table.

I ran across this with some failing integration tests for a personal project. Adding RLS started to make some API calls return incorrect information for edge cases that worked previously without RLS.


Mike Brancato

Re: RLS creates inaccurate limit and offset results

От
Christophe Pettus
Дата:

> On Nov 11, 2025, at 14:46, mike@mikebrancato.com wrote:
> If I understand the documentation (https://www.postgresql.org/docs/current/queries-limit.html), inconsistent results
fordifferent LIMIT / OFFSET values is only a known issue when selecting different subsets of data. 

The documentation could perhaps be better worded, but PostgreSQL makes no guarantee of the order of a result set in
absenceof an ORDER BY clause over that particular result set.  Since the ordering can't be guaranteed, which particular
subsetyou get from LIMIT and OFFSET is not guaranteed either. 

Again, this has nothing to do with RLS in particular.  The more complex the query and the more steps a result set has
togo through from disk ordering to result set ordering, the more likely it is to see inconsistent results, but it's
neverguaranteed in any case. 


Re: RLS creates inaccurate limit and offset results

От
Tom Lane
Дата:
mike@mikebrancato.com writes:
> If I understand the documentation (https://www.postgresql.org/docs/current/queries-limit.html), inconsistent results
fordifferent LIMIT / OFFSET values is only a known issue when selecting different subsets of data. 

You are willfully ignoring the lede on that page:

    When using LIMIT, it is important to use an ORDER BY clause that
    constrains the result rows into a unique order. Otherwise you will
    get an unpredictable subset of the query's rows.

This has nothing particularly to do with RLS; RLS is merely one way in
which a query might gain complications sufficient to cause the planner
to change plans.  Without ORDER BY, the planner is free to switch to
a plan that delivers rows in some other order.  Even just modifying
OFFSET can cause that to happen (since it's effectively a change in
the number of rows required to be fetched).

I suspect if you check with EXPLAIN, you'll find that you get
different plans with the different OFFSET values.  But whatever the
details, your code was making unjustified assumptions.

            regards, tom lane