Row security policies let you control, on a per-user basis, which rows a user can query or modify—independently of the SQL privilege system. This feature is also known as Row-Level Security (RLS).
By default, tables have no row security policies, so any user with the appropriate SQL privileges can read or write all rows equally. Once you enable RLS on a table, every SELECT, INSERT, UPDATE, and DELETE must pass a matching policy. If no policy exists, a default-deny rule applies: no rows are visible or modifiable.
Table-wide operations such asTRUNCATEandREFERENCESare not subject to row security.
How it works
When a user runs a query against a table with RLS enabled:
The database checks which policies apply to the current user and command type.
Each applicable policy expression is evaluated for every candidate row, before any conditions from the user's own query are applied.
Rows where the expression does not return
trueare filtered out silently.
Policy expressions run with the privileges of the user executing the query. Use security-definer functions when a policy needs to access data that the calling user cannot directly read.
leakproof functions are an exception: the optimizer may evaluate them before the row-security check, because they are guaranteed not to leak information.Key concepts
Policy scope
A policy can target a specific command (SELECT, INSERT, UPDATE, DELETE) or apply to ALL commands. Multiple roles can be assigned to one policy; standard role membership and inheritance rules apply. If no role is specified, or if PUBLIC is used, the policy applies to all users.
USING and WITH CHECK clauses
| Clause | Controls |
|---|---|
USING | Which existing rows are visible or eligible for UPDATE/DELETE |
WITH CHECK | Which rows may be written by INSERT or UPDATE |
If a policy specifies only USING, PolarDB for Oracle implicitly applies the same expression as the WITH CHECK clause.
Permissive vs. restrictive policies
| Policy type | Combined with | Effect |
|---|---|---|
| Permissive (default) | OR | A row passes if it satisfies any permissive policy |
| Restrictive | AND | A row must pass all restrictive policies |
Use permissive policies to grant access; use restrictive policies to add mandatory constraints that always apply regardless of other permissions.
Who bypasses row security
| Principal | Behavior |
|---|---|
| Superusers | Always bypass RLS |
Roles with BYPASSRLS attribute | Always bypass RLS |
| Table owner | Bypasses RLS by default; opt in with ALTER TABLE ... FORCE ROW LEVEL SECURITY |
Policy management commands and required privileges
Only the table owner can enable or disable row security and manage policies on a table.
| Operation | Command | Required privilege |
|---|---|---|
| Enable RLS | ALTER TABLE ... ENABLE ROW LEVEL SECURITY | Table owner |
| Disable RLS | ALTER TABLE ... DISABLE ROW LEVEL SECURITY | Table owner |
| Force owner subject to RLS | ALTER TABLE ... FORCE ROW LEVEL SECURITY | Table owner |
| Create a policy | CREATE POLICY | Table owner |
| Modify a policy | ALTER POLICY | Table owner |
| Remove a policy | DROP POLICY | Table owner |
Disabling RLS does not remove existing policies—they are preserved but ignored until RLS is re-enabled.
Each policy name must be unique within a table. Different tables may share the same policy name.
Use case: restrict managers to their own accounts
Allow only members of the managers role to access rows in the accounts table, and only rows where the manager column matches their own username.
CREATE TABLE accounts (manager text, company text, contact_email text);
ALTER TABLE accounts ENABLE ROW LEVEL SECURITY;
CREATE POLICY account_managers ON accounts TO managers
USING (manager = current_user);Because no separate WITH CHECK is specified, PolarDB for Oracle applies the same USING expression as the write constraint. A manager cannot SELECT, UPDATE, or DELETE rows belonging to another manager, and cannot INSERT or UPDATE rows to set a different manager's name.
Use case: let all users read, but only modify their own row
To allow all users to access only their own row in a users table, a simple policy can be used:
CREATE POLICY user_policy ON users
USING (user_name = current_user);To use a different policy for rows that are being added to the table compared to those rows that are visible, multiple policies can be combined. This pair of policies allows all users to view all rows in the users table, but only modify their own:
-- All users can read all rows
CREATE POLICY user_sel_policy ON users
FOR SELECT
USING (true);
-- Each user can only modify their own row
CREATE POLICY user_mod_policy ON users
USING (user_name = current_user);For SELECT, the two policies combine with OR: all rows are visible. For INSERT, UPDATE, and DELETE, only user_mod_policy applies, so users are limited to their own rows.
Use case: production example with multiple roles and policies
This example emulates a Unix-style password file with separate administrator and user permissions.
-- Schema
CREATE TABLE passwd (
user_name text UNIQUE NOT NULL,
pwhash text,
uid int PRIMARY KEY,
gid int NOT NULL,
real_name text NOT NULL,
home_phone text,
extra_info text,
home_dir text NOT NULL,
shell text NOT NULL
);
CREATE ROLE admin; -- Administrator
CREATE ROLE bob; -- Normal user
CREATE ROLE alice; -- Normal user
-- Seed data
INSERT INTO passwd VALUES
('admin', 'xxx', 0, 0, 'Admin', '111-222-3333', null, '/root', '/bin/dash');
INSERT INTO passwd VALUES
('bob', 'xxx', 1, 1, 'Bob', '123-456-7890', null, '/home/bob', '/bin/zsh');
INSERT INTO passwd VALUES
('alice', 'xxx', 2, 1, 'Alice', '098-765-4321', null, '/home/alice', '/bin/zsh');
ALTER TABLE passwd ENABLE ROW LEVEL SECURITY;
-- Admin can read and write all rows
CREATE POLICY admin_all ON passwd TO admin USING (true) WITH CHECK (true);
-- All users can view all rows
CREATE POLICY all_view ON passwd FOR SELECT USING (true);
-- Users can update only their own record, and only to an allowed shell
CREATE POLICY user_mod ON passwd FOR UPDATE
USING (current_user = user_name)
WITH CHECK (
current_user = user_name AND
shell IN ('/bin/bash', '/bin/sh', '/bin/dash', '/bin/zsh', '/bin/tcsh')
);
-- Grant admin full access
GRANT SELECT, INSERT, UPDATE, DELETE ON passwd TO admin;
-- Public can read non-sensitive columns
GRANT SELECT
(user_name, uid, gid, real_name, home_phone, extra_info, home_dir, shell)
ON passwd TO public;
-- Users can update their own profile fields
GRANT UPDATE
(pwhash, real_name, home_phone, extra_info, shell)
ON passwd TO public;Verify the policies work
-- admin sees all rows and all columns
SET ROLE admin;
TABLE passwd;
-- Returns all 3 rows with all columns including pwhash
-- alice cannot access the table directly (no SELECT on all columns granted)
SET ROLE alice;
TABLE passwd;
-- ERROR: permission denied for table passwd
-- alice can only read permitted columns
SELECT user_name, real_name, home_phone, extra_info, home_dir, shell FROM passwd;
-- Returns all 3 rows (allowed by all_view + column-level GRANT)
-- alice cannot update the user_name column (no permission)
UPDATE passwd SET user_name = 'joe';
-- ERROR: permission denied for table passwd
-- alice can update her own real_name
UPDATE passwd SET real_name = 'Alice Doe';
-- UPDATE 1
-- alice cannot update a different user's record (RLS silently filters)
UPDATE passwd SET real_name = 'John Doe' WHERE user_name = 'admin';
-- UPDATE 0
-- alice cannot set an unlisted shell
UPDATE passwd SET shell = '/bin/xx';
-- ERROR: new row violates WITH CHECK OPTION for "passwd"
-- alice cannot delete rows (no DELETE privilege)
DELETE FROM passwd;
-- ERROR: permission denied for table passwd
-- alice cannot insert rows (no INSERT privilege)
INSERT INTO passwd (user_name) VALUES ('xxx');
-- ERROR: permission denied for table passwd
-- alice can change her own password; other rows are silently excluded
UPDATE passwd SET pwhash = 'abc';
-- UPDATE 1Use case: combine permissive and restrictive policies
Building on the passwd example, add a restrictive policy so the admin role can only access records when connected over a local Unix socket (not over a network connection).
CREATE POLICY admin_local_only ON passwd AS RESTRICTIVE TO admin
USING (pg_catalog.inet_client_addr() IS NULL);With this policy in place, an admin connecting from a remote IP sees no rows:
-- Connected remotely (inet_client_addr() returns an IP address, not NULL)
SELECT current_user;
-- current_user
-- --------------
-- admin
SELECT inet_client_addr();
-- inet_client_addr
-- ------------------
-- 127.0.0.1
TABLE passwd;
-- user_name | pwhash | uid | gid | real_name | home_phone | extra_info | home_dir | shell
-- -----------+--------+-----+-----+-----------+------------+------------+----------+-------
-- (0 rows)
UPDATE passwd SET pwhash = NULL;
-- UPDATE 0Performance considerations
Policy expressions that reference only columns of the current row have the lowest overhead and are the most straightforward to reason about. Design policies this way when possible.
If a policy must consult other rows or tables—using subqueries or functions containing SELECT statements—be aware of the following:
Race conditions: A subquery in a policy expression reads a snapshot of the referenced table. If another transaction modifies that table concurrently, the policy may evaluate against stale data, potentially allowing a user to see rows they should not. The following schema illustrates this risk. A policy grants access to rows in
informationbased on the user'sgroup_idin theuserstable:-- Privilege groups CREATE TABLE groups (group_id int PRIMARY KEY, group_name text NOT NULL); INSERT INTO groups VALUES (1, 'low'), (2, 'medium'), (5, 'high'); GRANT ALL ON groups TO alice; GRANT SELECT ON groups TO public; -- User privilege levels CREATE TABLE users (user_name text PRIMARY KEY, group_id int NOT NULL REFERENCES groups); INSERT INTO users VALUES ('alice', 5), ('bob', 2), ('mallory', 2); GRANT ALL ON users TO alice; GRANT SELECT ON users TO public; -- Protected data CREATE TABLE information (info text, group_id int NOT NULL REFERENCES groups); INSERT INTO information VALUES ('barely secret', 1), ('slightly secret', 2), ('very secret', 5); ALTER TABLE information ENABLE ROW LEVEL SECURITY; -- A row is visible to users whose group_id >= the row's group_id CREATE POLICY fp_s ON information FOR SELECT USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user)); CREATE POLICY fp_u ON information FOR UPDATE USING (group_id <= (SELECT group_id FROM users WHERE user_name = current_user)); GRANT ALL ON information TO public;If alice runs the following transaction to revoke mallory's access and update a sensitive row:
BEGIN; UPDATE users SET group_id = 1 WHERE user_name = 'mallory'; UPDATE information SET info = 'secret from mallory' WHERE group_id = 2; COMMIT;And mallory concurrently runs:
SELECT * FROM information WHERE group_id = 2 FOR UPDATE;In
READ COMMITTEDmode, mallory's transaction may read alice's updated row ininformationwhile still using the pre-update snapshot ofusers. This happens because mallory's transaction blocks waiting for alice's to commit, then fetches the updatedinformationrow due to theFOR UPDATEclause—but theuserssub-SELECT does not haveFOR UPDATE, so it reads mallory's old (higher) privilege level and grants access to the newly written value. To mitigate this:Use
SELECT ... FOR SHAREin the subquery to take a shared lock on referenced rows. This requires grantingUPDATEprivilege on the referenced table, or encapsulating the subquery in a security-definer function. Note that heavy concurrent use of row share locks on the referenced table can pose a performance problem, especially if updates are frequent.Take an
ACCESS EXCLUSIVElock on the referenced table before updating it, preventing concurrent reads of stale values.Wait for all concurrent transactions to finish before making changes that rely on the updated security configuration.
Referential integrity bypass: Unique constraints, primary key constraints, and foreign key checks always bypass row security to preserve data integrity. Design your schema carefully to avoid "covert channel" leaks through these checks.
Usage notes
Set the
row_securityconfiguration parameter tooffto detect queries whose results would be silently filtered by a policy. Instead of filtering rows, the database raises an error, making it easier to identify and investigate unexpected RLS behavior. This does not bypass row security—it makes silent filtering visible. When taking a database backup, it could be disastrous if row security silently caused some rows to be omitted; settingrow_security = offensures such omissions surface as errors rather than going undetected.Policies are table-specific. Enabling or disabling RLS on a table does not affect policies on other tables.
Troubleshooting
| Behavior | Likely cause | Action |
|---|---|---|
| Query returns 0 rows unexpectedly | RLS is enabled and no policy matches the current user | Check which policies exist with SELECT * FROM pg_policies WHERE tablename = 'your_table'. Verify the current role with SELECT current_user. |
UPDATE or DELETE affects 0 rows, no error | RLS is filtering out rows silently | Check the USING clause of applicable policies. Set row_security = off temporarily to confirm filtering is the cause (this raises an error rather than silently filtering). |
INSERT or UPDATE raises ERROR: new row violates WITH CHECK OPTION | The new row fails the WITH CHECK expression | Review the WITH CHECK clause of the applicable policy and ensure the inserted or updated values satisfy the constraint. |
| Table owner cannot see their own rows | FORCE ROW LEVEL SECURITY is set | Run ALTER TABLE ... NO FORCE ROW LEVEL SECURITY to restore default owner bypass behavior. |
| Superuser is subject to RLS | Not possible—superusers always bypass RLS | Verify the role actually has superuser status: SELECT rolsuper FROM pg_roles WHERE rolname = current_user. |
| Admin sees no rows when connecting remotely | A restrictive policy (e.g., inet_client_addr() IS NULL) is blocking non-local connections | Review restrictive policies on the table and connect over a local Unix socket, or adjust the policy. |