Postgres exclusion constraint on insert/update - postgresql

I have a table defined like so
Table "public.foo"
Column | Type | Collation | Nullable | Default
----------+---------+-----------+----------+-------------------------------------
foo_id | integer | | not null | nextval('foo_foo_id_seq'::regclass)
bar_id | integer | | |
approved | boolean | | |
Indexes:
"foo_pkey" PRIMARY KEY, btree (foo_id)
Foreign-key constraints:
"foo_bar_id_fkey" FOREIGN KEY (bar_id) REFERENCES bar(bar_id)
How would I define an exclusion constraint, such that only one row of foo with a specific bar_id would be able to set approved to true?
For example with the following data:
foo_id | bar_id | approved
--------+--------+----------
1 | 1 | t
2 | 1 |
3 | 2 |
(3 rows)
I would be able to set approved to row 3 to true, because no other row with foo_id 3 has true for approved.
However updating row 2 's approved to true would fail, because row 1 also has foo_id 1 and is already approved.

You don't need an exclusion constraint, a filtered unique index will do:
create unique index only_one_approved_bar
on foo (bar_id)
where approved;
I would also recommend to define approved as not null. Boolean columns that allow null values are typically a source of constant confusion.

try this
ALTER TABLE public.foo
ADD CONSTRAINT uniq_approved UNIQUE (bar_id, approved)
or you can create unique index
CREATE UNIQUE INDEX uniq_approved ON public.foo
USING btree (bar_id, approved)
WHERE approved

Related

Postgres does not pick partial index even when the clause matches

I have a table that looks like so:
Column | Type | Collation | Nullable | Default
-----------+---------+-----------+----------+---------
app_id | uuid | | not null |
entity_id | uuid | | not null |
attr_id | uuid | | not null |
value | text | | not null |
ea_index | boolean | | |
Indexes:
"triples_pkey" PRIMARY KEY, btree (app_id, entity_id, attr_id, value)
"ea_index" UNIQUE, btree (app_id, entity_id, attr_id) WHERE ea_index
"triples_app_id" btree (app_id)
"triples_attr_id" btree (attr_id)
Foreign-key constraints:
"triples_app_id_fkey" FOREIGN KEY (app_id) REFERENCES apps(id) ON DELETE CASCADE
"triples_attr_id_fkey" FOREIGN KEY (attr_id) REFERENCES attrs(id) ON DELETE CASCADE
I have a special partial index ea_index, enabled for all the rows that have this column.
Now, when I run:
EXPLAIN (
SELECT
*
FROM triples
WHERE
app_id = '6b1ca162-0175-4188-9265-849f671d56cc' AND
entity_id = '6b1ca162-0175-4188-9265-849f671d56cc' AND
ea_index
);
I get:
Index Scan using triples_app_id on triples (cost=0.28..4.30 rows=1 width=221)
Index Cond: (app_id = '6b1ca162-0175-4188-9265-849f671d56cc'::uuid)
Filter: (ea_index AND (entity_id = '6b1ca162-0175-4188-9265-849f671d56cc'::uuid))
(3 rows)
I am a bit confused: why is this not using an index scan on ea_index? How could I debug this further?
This was because of a costing decision. EXPLAIN showed that it expected only 1 row, so there was no difference which index it chose. Changing up the uuids, it did pick the correct index.

unique index violation during update

I have run into a unique index violation in a bigger db. The original problem occurs in a stored pl/pgsql function.
I have simplified everything to show my problem. I can reproduce it in a rather simple table:
CREATE TABLE public.test
(
id integer NOT NULL DEFAULT nextval('test_id_seq'::regclass),
pos integer,
text text,
CONSTRAINT text_pkey PRIMARY KEY (id)
)
WITH (
OIDS=FALSE
);
ALTER TABLE public.test
OWNER TO root;
GRANT ALL ON TABLE public.test TO root;
I define a unique index on 'pos':
CREATE UNIQUE INDEX test_idx_pos
ON public.test
USING btree
(pos);
Before the UPDATE the data in the table looks like this:
testdb=# SELECT * FROM test;
id | pos | text
----+-----+----------
2 | 1 | testpos1
3 | 2 | testpos2
1 | 5 | testpos4
4 | 4 | testpos3
(4 Zeilen)
tr: (4 rows)
Now I want to decrement all 'pos' values by 1 that are bigger than 2 and get an error (tr are my translations from German to English):
testdb=# UPDATE test SET pos = pos - 1 WHERE pos > 2;
FEHLER: doppelter Schlüsselwert verletzt Unique-Constraint »test_idx_pos«
tr: ERROR: duplicate key violates unique constraint »test_idx_pos«
DETAIL: Schlüssel »(pos)=(4)« existiert bereits.
tr: key »(pos)=(4) already exists.
If the UPDATE had run complete the table would look like this and be unique again:
testdb=# SELECT * FROM test;
id | pos | text
----+-----+----------
2 | 1 | testpos1
3 | 2 | testpos2
1 | 4 | testpos4
4 | 3 | testpos3
(4 Zeilen)
tr: (4 rows)
How can I avoid such situation? I learned that stored pl/pgsql functions are embedded into transactions, so this problem shouldn't appear?
Unique indexes are evaluated per row not per statement (which is e.g. different to Oracle's implementation)
The solution to this problem is to use a unique constraint which can be deferred and thus is evaluated at the end of the transaction.
So instead of the unique index, define a constraint:
alter table test add constraint test_idx_pos unique (pos)
deferrable initially deferred;

Query planner using a primary key index instead of a more targeted column index when adding order by primary key

Please excuse simplification of actual query. Just to make it readable. Currently having slowdowns in our queries when adding order by primary key.
select id, field1, field2
from table1
where field1 = 'value'
limit 1000;
So having an index for field1, this query uses that index, which makes the query faster. I can trace that the query planner uses that index via the explain command.
Adding an order by suddenly changes the index used to the primary key index though. Which makes the query a lot slower.
select id, field1, field2
from table1 where field1 = 'value'
order by id asc
limit 1000;
Is there a way to force the query planner to use the field1 index?
EDIT:
Actual table detail:
\d fax_message
Table "public.fax_message"
Column | Type | Modifiers
--------------------------+-----------------------------+-----------
id | bigint | not null
broadcast_ref | character varying(255) |
busy_retries | integer |
cli | character varying(255) |
dncr | boolean | not null
document_set_id | bigint | not null
fax_broadcast_attempt_id | bigint |
fps | boolean | not null
header_format | character varying(255) |
last_updated | timestamp without time zone | not null
max_fax_pages | integer |
message_ref | character varying(255) |
must_be_sent_before_date | timestamp without time zone |
request_id | bigint |
resolution | character varying(255) |
retries | integer |
send_from | character varying(255) |
send_ref | character varying(255) |
send_to | character varying(255) | not null
smartblock | boolean | not null
status | character varying(255) | not null
time_zone | character varying(255) |
total_pages | integer |
user_id | uuid | not null
delay_status_check_until | timestamp without time zone |
version | bigint | default 0
cost | numeric(40,10) | default 0
Indexes:
"fax_message_pkey" PRIMARY KEY, btree (id)
"fax_message_broadcast_ref_idx" btree (broadcast_ref)
"fax_message_delay_status_check_until" btree (delay_status_check_until)
"fax_message_document_set_idx" btree (document_set_id)
"fax_message_fax_broadcast_attempt_idx" btree (fax_broadcast_attempt_id)
"fax_message_fax_document_set_idx" btree (document_set_id)
"fax_message_message_ref_idx" btree (message_ref)
"fax_message_request_idx" btree (request_id)
"fax_message_send_ref_idx" btree (send_ref)
"fax_message_status_fax_broadcast_attempt_idx" btree (status, fax_broadcast_attempt_id)
"fax_message_user" btree (user_id)
Foreign-key constraints:
"fk2881c4e5106ed2de" FOREIGN KEY (request_id) REFERENCES core_api_send_fax_request(id)
"fk2881c4e5246f3088" FOREIGN KEY (document_set_id) REFERENCES fax_document_set(id)
"fk2881c4e555aad98b" FOREIGN KEY (user_id) REFERENCES users(id)
"fk2881c4e59920b254" FOREIGN KEY (fax_broadcast_attempt_id) REFERENCES fax_broadcast_attempt(id)
Referenced by:
TABLE "fax_message_status_modifier" CONSTRAINT "fk2dfbe52acb955ec1" FOREIGN KEY (fax_message_id) REFERENCES fax_message(id)
TABLE "fax_message_attempt" CONSTRAINT "fk82058973cb955ec1" FOREIGN KEY (fax_message_id) REFERENCES fax_message(id)
Actual index used:
\d fax_message_status_fax_broadcast_attempt_idx
Index "public.fax_message_status_fax_broadcast_attempt_idx"
Column | Type | Definition
--------------------------+------------------------+--------------------------
status | character varying(255) | status
fax_broadcast_attempt_id | bigint | fax_broadcast_attempt_id
btree, for table "public.fax_message"
Real queries:
With order by:
explain select this_.id as id65_0_, this_.version as version65_0_, this_.broadcast_ref as broadcast3_65_0_, this_.busy_retries as busy4_65_0_, this_.cli as cli65_0_, this_.cost as cost65_0_, this_.delay_status_check_until as delay7_5_0_, this_.dncr as dncr65_0_, this_.document_set_id as document9_65_0_, this_.fax_broadcast_attempt_id as fax10_65_0_, this_.fps as fps65_0_, this_.header_format as header12_65_0_, this_.last_updated as last13_65_0_, this_.max_fax_pages as max14_65_0_, this_.message_ref as message15_65_0_, this_.must_be_sent_before_date as must16_65_0_, this_.request_id as request17_65_0_, this_.resolution as resolution65_0_, this_.retries as retries65_0_, this_.send_from as send20_65_0_, this_.send_ref as send21_65_0_, this_.send_to as send22_65_0_, this_.smartblock as smartblock65_0_, this_.status as status65_0_, this_.time_zone as time25_65_0_, this_.total_pages as total26_65_0_, this_.user_id as user27_65_0_ from fax_message this_ where this_.status='TO_CHARGE_GROUP' order by id asc limit 1000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..53956.06 rows=1000 width=2234)
-> Index Scan using fax_message_pkey on fax_message this_ (cost=0.43..2601902.61 rows=48223 width=2234)
Filter: ((status)::text = 'TO_CHARGE_GROUP'::text)
(3 rows)
This one without the order by:
explain select this_.id as id65_0_, this_.version as version65_0_, this_.broadcast_ref as broadcast3_65_0_, this_.busy_retries as busy4_65_0_, this_.cli as cli65_0_, this_.cost as cost65_0_, this_.delay_status_check_until as delay7_5_0_, this_.dncr as dncr65_0_, this_.document_set_id as document9_65_0_, this_.fax_broadcast_attempt_id as fax10_65_0_, this_.fps as fps65_0_, this_.header_format as header12_65_0_, this_.last_updated as last13_65_0_, this_.max_fax_pages as max14_65_0_, this_.message_ref as message15_65_0_, this_.must_be_sent_before_date as must16_65_0_, this_.request_id as request17_65_0_, this_.resolution as resolution65_0_, this_.retries as retries65_0_, this_.send_from as send20_65_0_, this_.send_ref as send21_65_0_, this_.send_to as send22_65_0_, this_.smartblock as smartblock65_0_, this_.status as status65_0_, this_.time_zone as time25_65_0_, this_.total_pages as total26_65_0_, this_.user_id as user27_65_0_ from fax_message this_ where this_.status='TO_CHARGE_GROUP' limit 1000;
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.56..1744.13 rows=1000 width=2234)
-> Index Scan using fax_message_status_fax_broadcast_attempt_idx on fax_message this_ (cost=0.56..84080.59 rows=48223 width=2234)
Index Cond: ((status)::text = 'TO_CHARGE_GROUP'::text)
(3 rows)
The cost on the query that used the fax_message_pkey is greater than the max cost of the query that used fax_message_status_fax_broadcast_attempt_idx.
I was hoping that the query will still use the fax_message_status_fax_broadcast_attempt_idx index even with the order by there.
According to How do I force Postgres to use a particular index? (and links from answers there) there does not seem to be a way to force use of particular index .
CTEs are a optimization fence. You're not giving us enough information to tell you why your query is getting planned wrong, but this should work if you don't care to actually fix the problem.
WITH t AS (
select id, field1, field2
from table1
where field1 = 'value'
limit 1000
)
SELECT *
FROM t
order by id asc;

Any way to create referential integrity based on data values?

following is a simplified illustration
TABLE : EMPLOYEE (TENANT_ID is a FK)
ID | NAME | TENANT_ID
1 | John | 1
TABLE DEPARTMENT
ID | NAME | TENANT_ID
1 | Physics | 1
2 | Math | 2
TABLE : EMPLOYEE_DEPARTMENTS (Join between employee and department)
ID | EMPLOYEE_ID | DEPARTMENT_ID
1 | 1 | 1
Is there a way to fail inserting data into EMPLOYEE_DEPARTMENTS if EMPLOYEE value is for TENANT 1 and DEPARMENT_ID is from TENANT 2? e.g. where employee_id=1 belongs to tenant=1 and department_id=2 belongs to tenant=2
ID | EMPLOYEED_ID | DEPARTMENT_ID
2 | 1 | 2
Is there a way to prevent such data insertion either at an app or db level. PS> no room for using triggers and don't want to use triggers.
Without triggers, the only way to do this is copy the tenant id so it appears in every table, and use composite primary or unique constraint and a composite foreign key.
e.g. if you had a UNIQUE constraint on EMPLOYEE(TENANT_ID, ID) and on DEPARTMENT(TENANT_ID, ID) you could add a FOREIGN KEY (TENANT_ID, EMPLOYEE_ID) REFERENCES EMPLOYEE (TENANT_ID, ID) and FOREIGN KEY (TENANT_ID, DEPARTMENT_ID) REFERENCES DEPARTMENT (TENANT_ID, ID).
This requires that the join table incorporate the TENANT_ID.
I suggest defining the PRIMARY_KEY of EMPLOYEE_DEPARTMENTS as (TENANT_ID, DEPARTMENT_ID, EMPLOYEE_ID) and getting rid of the useless surrogate key ID on the EMPLOYEE_DEPARTMENTS table, unless your toolkit/framework/ORM can't cope without it.

Sort SELECT result by pairs of columns

In the following PostgreSQL 8.4.13 table
(where author users give grades to id users):
# \d pref_rep;
Table "public.pref_rep"
Column | Type | Modifiers
-----------+-----------------------------+-----------------------------------------------------------
id | character varying(32) | not null
author | character varying(32) | not null
good | boolean |
fair | boolean |
nice | boolean |
about | character varying(256) |
stamp | timestamp without time zone | default now()
author_ip | inet |
rep_id | integer | not null default nextval('pref_rep_rep_id_seq'::regclass)
Indexes:
"pref_rep_pkey" PRIMARY KEY, btree (id, author)
Check constraints:
"pref_rep_check" CHECK (id::text <> author::text)
Foreign-key constraints:
"pref_rep_author_fkey" FOREIGN KEY (author) REFERENCES pref_users(id) ON DELETE CASCADE
"pref_rep_id_fkey" FOREIGN KEY (id) REFERENCES pref_users(id) ON DELETE CASCADE
how to find faked entries, which have same id and same author_ip?
I.e. some users register several accounts and then submit bad notes (the good, fair, nice columns above) for other users. But I can still identify them by their author_ip addresses.
I'm trying to find them by fetching:
# select id, author_ip from pref_rep group by id, author_ip;
id | author_ip
-------------------------+-----------------
OK490496816466 | 94.230.231.106
OK360565502458 | 78.106.102.16
DE25213 | 178.216.72.185
OK331482634936 | 95.158.209.5
VK25785834 | 77.109.20.182
OK206383671767 | 80.179.90.103
OK505822972559 | 46.158.46.126
OK237791033602 | 178.76.216.77
VK90402803 | 109.68.173.37
MR16281819401420759860 | 109.252.139.198
MR5586967138985630915 | 2.93.14.248
OK341086615664 | 93.77.75.142
OK446200841566 | 95.59.127.194
But I need to sort the above result.
How can I sort it by the number of pairs (id, author_ip) desc please?
select id, pr.author_ip
from
pref_rep pr
inner join
(
select author_ip
from pref_rep
group by author_ip
having count(*) > 1
) s using(author_ip)
order by 2, 1