I have a table of the form (based on):
create table foo (
name text unique,
ref_id int,
mod_time timestamptz
);
I would like to be able to insert into it, but
instead updating mod_time if (name, ref_id) pair was in use, and failing if name were in use for another ref_id.
I can do this by creating another unique constraint as follows:
alter table foo add unique (name, ref_id);
Then
insert into foo values ( $1, $2, $3 )
on conflict (name, ref_id) do update set
mod_time = excluded.mod_time;
will function as I want:
If new name, inserts
If same name and ref_id, changes mod_time
If same name for different ref_id, fails
However, the cost is a 2nd unique index, which is in fact superfluous for enforcing the constraint as records unique by name will automatically be unique by name and ref_id.
Is there some workaround that will get me this behavior without the 2nd index, and without additional roundtrips to the database?
It depends on what you mean by fail. If it's okay to just not do anything in this case: where If same name for different ref_id you could do the following:
insert into foo values ($1, $2, $3)
on conflict (name) DO update set
mod_time = excluded.mod_time
WHERE foo.ref_id = excluded.ref_id;
You could check how many rows were modified after running the query and raise an exception if none were modified.
Related
I'm getting the following error when doing the following type of insert:
Query:
INSERT INTO accounts (type, person_id) VALUES ('PersonAccount', 1) ON
CONFLICT (type, person_id) WHERE type = 'PersonAccount' DO UPDATE SET
updated_at = EXCLUDED.updated_at RETURNING *
Error:
SQL execution failed (Reason: ERROR: there is no unique or exclusion
constraint matching the ON CONFLICT specification)
I also have an unique INDEX:
CREATE UNIQUE INDEX uniq_person_accounts ON accounts USING btree (type,
person_id) WHERE ((type)::text = 'PersonAccount'::text);
The thing is that sometimes it works, but not every time. I randomly get
that exception, which is really strange. It seems that it can't access that
INDEX or it doesn't know it exists.
Any suggestion?
I'm using PostgreSQL 9.5.5.
Example while executing the code that tries to find or create an account:
INSERT INTO accounts (type, person_id, created_at, updated_at) VALUES ('PersonAccount', 69559, '2017-02-03 12:09:27.259', '2017-02-03 12:09:27.259') ON CONFLICT (type, person_id) WHERE type = 'PersonAccount' DO UPDATE SET updated_at = EXCLUDED.updated_at RETURNING *
SQL execution failed (Reason: ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification)
In this case, I'm sure that the account does not exist. Furthermore, it never outputs the error when the person has already an account. The problem is that, in some cases, it also works if there is no account yet. The query is exactly the same.
Per the docs,
All table_name unique indexes that, without regard to order, contain exactly the
conflict_target-specified columns/expressions are inferred (chosen) as arbiter
indexes. If an index_predicate is specified, it must, as a further requirement
for inference, satisfy arbiter indexes.
The docs go on to say,
[index_predicate are u]sed to allow inference of partial unique indexes
In an understated way, the docs are saying that when using a partial index and
upserting with ON CONFLICT, the index_predicate must be specified. It is not
inferred for you. I learned this
here, and the following example demonstrates this.
CREATE TABLE test.accounts (
id int PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY,
type text,
person_id int);
CREATE UNIQUE INDEX accounts_note_idx on accounts (type, person_id) WHERE ((type)::text = 'PersonAccount'::text);
INSERT INTO test.accounts (type, person_id) VALUES ('PersonAccount', 10);
so that we have:
unutbu=# select * from test.accounts;
+----+---------------+-----------+
| id | type | person_id |
+----+---------------+-----------+
| 1 | PersonAccount | 10 |
+----+---------------+-----------+
(1 row)
Without index_predicate we get an error:
INSERT INTO test.accounts (type, person_id) VALUES ('PersonAccount', 10) ON CONFLICT (type, person_id) DO NOTHING;
-- ERROR: there is no unique or exclusion constraint matching the ON CONFLICT specification
But if instead you include the index_predicate, WHERE ((type)::text = 'PersonAccount'::text):
INSERT INTO test.accounts (type, person_id) VALUES ('PersonAccount', 10)
ON CONFLICT (type, person_id)
WHERE ((type)::text = 'PersonAccount'::text) DO NOTHING;
then there is no error and DO NOTHING is honored.
A simple solution of this error
First of all let's see the cause of error with a simple example. Here is the table mapping products to categories.
create table if not exists product_categories (
product_id uuid references products(product_id) not null,
category_id uuid references categories(category_id) not null,
whitelist boolean default false
);
If we use this query:
INSERT INTO product_categories (product_id, category_id, whitelist)
VALUES ('123...', '456...', TRUE)
ON CONFLICT (product_id, category_id)
DO UPDATE SET whitelist=EXCLUDED.whitelist;
This will give you error No unique or exclusion constraint matching the ON CONFLICT because there is no unique constraint on product_id and category_id. There could be multiple rows having the same combination of product and category id (so there can never be a conflict on them).
Solution:
Use unique constraint on both product_id and category_id like this:
create table if not exists product_categories (
product_id uuid references products(product_id) not null,
category_id uuid references categories(category_id) not null,
whitelist boolean default false,
primary key(product_id, category_id) -- This will solve the problem
-- unique(product_id, category_id) -- OR this if you already have a primary key
);
Now you can use ON CONFLICT (product_id, category_id) for both columns without any error.
In short: Whatever column(s) you use with on conflict, they should have unique constraint.
The easy way to fix it is by setting the conflicting column as UNIQUE
I did not have a chance to play with UPSERT, but I think you have a case from
docs:
Note that this means a non-partial unique index (a unique index
without a predicate) will be inferred (and thus used by ON CONFLICT)
if such an index satisfying every other criteria is available. If an
attempt at inference is unsuccessful, an error is raised.
I solved the same issue by creating one UNIQUE INDEX for ALL columns you want to include in the ON CONFLICT clause, not one UNIQUE INDEX for each of the columns.
CREATE TABLE table_name (
element_id UUID NOT NULL DEFAULT gen_random_uuid(),
timestamp TIMESTAMP NOT NULL DEFAULT now():::TIMESTAMP,
col1 UUID NOT NULL,
col2 STRING NOT NULL ,
col3 STRING NOT NULL ,
CONSTRAINT "primary" PRIMARY KEY (element_id ASC),
UNIQUE (col1 asc, col2 asc, col3 asc)
);
Which will allow to query like
INSERT INTO table_name (timestamp, col1, col2, col3) VALUES ('timestamp', 'uuid', 'string', 'string')
ON CONFLICT (col1, col2, col3)
DO UPDATE timestamp = EXCLUDED.timestamp, col1 = EXCLUDED.col1, col2 = excluded.col2, col3 = col3.excluded;
I have the following query:
INSERT INTO hosts (name, domain, ip)
SELECT name, domain, ip
FROM staging_hosts
ON CONFLICT (ip) DO UPDATE
SET name = excluded.name, domain = excluded.domain;
It works fine aside from the fact that the IDs in my "hosts" table are not being incremented sequentially. For example, I'll get the following new IDs after running this query:
114855
114859
114873
114977
117389
115326
The ID column on the hosts table is serial so I'm not sure why the IDs are incrementing sequentially.
If you insist on minimizing gaps, you could do that, with example tables included:
CREATE TABLE foo (
id SERIAL PRIMARY KEY,
ip INT,
name TEXT,
UNIQUE(ip) );
CREATE TABLE staging (
ip INT,
name TEXT );
INSERT INTO foo (ip,name) VALUES (1,'hello'),(2,'abc'),(10,'world');
INSERT INTO staging VALUES (1,'byebye'),(2,'def'),(11,'world');
WITH s AS (
SELECT COALESCE( foo.id, nextval('foo_id_seq'::regclass)) AS id,
staging.ip,staging.name
FROM staging LEFT JOIN foo USING (ip))
INSERT INTO foo (id,ip,name) SELECT * FROM s
ON CONFLICT(id) DO UPDATE
SET ip=excluded.ip, name=excluded.name;
COALESCE does not evaluate the second argument if it is not needed, which means it only calls nextval when required.
I've being trying to update a row with a value from another table but I keep getting the same error:
table name "chats" specified more than once
I'm trying to insert a row into the messages table, and then use that timestamp (message_timestamp) to update the last_updated field in the chats table.
Any help would be appreciated! Been stuck on this for a day now:
WITH result AS
(INSERT INTO messages (user_id, chat_id, message_timestamp, users_read, message_text)
VALUES ($1, $2, NOW(), '{}', $3) RETURNING message_timestamp, chat_id)
UPDATE chats SET chats.last_updated=result.message_timestamp FROM result, chats WHERE chats.id=result.chat_id;
Edit:
On the other hand, removing chats from my FROM clause as so:
UPDATE chats SET chats.last_updated=result.message_timestamp FROM result WHERE chats.id=result.chat_id;
results in a different error:
column "chats" of relation "chats" does not exist
Which is weird considering I never call chats.chats
Edit 2: Create statement for the chat table:
CREATE TABLE IF NOT EXISTS chats (
id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
chat_name VARCHAR,
last_message TIMESTAMP NOT NULL
);
From UPDATE:
Do not include the table's name in the specification of a target
column
So the column last_updated that you want to update should not be qualified with the table's name like chats.last_updated:
UPDATE chats
SET last_updated = result.message_timestamp
FROM result
WHERE chats.id = result.chat_id;
See a simplified demo.
I have an INSERT rule in an updatable view system, for which I would like to realize an UPSERT, such as :
CREATE OR REPLACE RULE _insert AS
ON INSERT TO vue_pays_gex.bals
DO INSTEAD (
INSERT INTO geo_pays_gex.voie(name, code, district) VALUES (new.name, new.code, new.district)
ON CONFLICT DO NOTHING;
But my since there can be many different combinations of these three columns, I don't think I can set a CONSTRAINT including them all (although I may be missing a point of understanding in the SQL logics), hence nullifying the ON CONFLIT DO NOTHING part.
The ideal solution would seem to be the use of an EXCEPT, but it only works in an INSERT INTO SELECT statement. Is there a way to use an INSERT INTO SELECT statement referring to the newly inserted row? Something like FROM new.bals (in my case)?
If not I could imagine a WHERE NOT EXISTS condition, but the same problem than before arises.
I'm guessing it is a rather common SQL need, but cannot find how to solve it. Any idea?
EDIT :
As requested, here is the table definition :
CREATE TABLE geo_pays_gex.voie
(
id_voie serial NOT NULL,
name character varying(50),
code character varying(15),
district character varying(50),
CONSTRAINT prk_constraint_voie PRIMARY KEY (id_voie),
CONSTRAINT voie_unique_key UNIQUE (name, code, district)
);
How do you define uniqueness? If it is the combination of name + code + district, then just add a constraint UNIQUE(name, code, district) on the table geo_pays_gex.voie. The 3, together, must be unique... but you can have several time the same name, or code, or district.
See it at http://rextester.com/EWR73154
EDIT ***
Since you can have Nulls and want to treat them as a unique value, you can replace the constraint creation by a unique index that replace the nulls
CREATE UNIQUE INDEX
voie_uniq ON voie
(COALESCE(name,''), code, COALESCE(district,''));
In addition to #JGH's answer.
INSERT in rule for INSERT will lead to infinity recursion (Postgres 9.6).
Full (NOT)runnable example:
CREATE SCHEMA ttest;
CREATE TABLE ttest.table_1 (
id bigserial
CONSTRAINT pk_table_1 PRIMARY KEY,
col_1 text,
col_2 text
);
CREATE OR REPLACE RULE table_1_always_upsert AS
ON INSERT TO ttest.table_1
DO INSTEAD (
INSERT INTO ttest.table_1(id, col_1, col_2)
VALUES (new.id, new.col_1, new.col_2)
ON CONFLICT ON CONSTRAINT pk_table_1
DO UPDATE
SET col_1 = new.col_1,
col_2 = new.col_2
);
INSERT INTO ttest.table_1(id, col_1, col_2) -- will result error: infinity recursion in rules
VALUES (1, 'One', 'A'),
(2, 'Two', 'B');
INSERT INTO ttest.table_1(id, col_1, col_2)
VALUES (1, 'One_updated', 'A_updated'),
(2, 'Two_updated', 'B_updated'),
(3, 'Three_inserted', 'C_inserted');
SELECT *
FROM ttest.table_1;
I have two tables, connected in E/R by a is-relation. One representing the "mother table"
CREATE TABLE PERSONS(
id SERIAL NOT NULL,
name character varying NOT NULL,
address character varying NOT NULL,
day_of_creation timestamp NOT NULL DEFAULT current_timestamp,
PRIMARY KEY (id)
)
the other representing the "child table"
CREATE TABLE EMPLOYEES (
id integer NOT NULL,
store character varying NOT NULL,
paychecksize integer NOT NULL,
FOREIGN KEY (id)
REFERENCES PERSONS(id),
PRIMARY KEY (id)
)
Now those two tables are joined in a view
CREATE VIEW EMPLOYEES_VIEW AS
SELECT
P.id,name,address,store,paychecksize,day_of_creation
FROM
PERSONS AS P
JOIN
EMPLOYEES AS E ON P.id = E.id
I want to write either a rule or a trigger to enable a db user to make an insert on that view, sparing him the nasty details of the splitted columns into different tables.
But I also want to make it convenient, as the id is a SERIAL and the day_of_creation has a default value there is no actual need that a user has to provide those, therefore a statement like
INSERT INTO EMPLOYEES_VIEW (name, address, store, paychecksize)
VALUES ("bob", "top secret", "drugstore", 42)
should be enough to result in
PERSONS
id|name|address |day_of_creation
-------------------------------
1 |bob |top secret| 2013-08-13 15:32:42
EMPLOYEES
id| store |paychecksize
---------------------
1 |drugstore|42
A basic rule would be easy as
CREATE RULE EMPLOYEE_VIEW_INSERT AS ON INSERT TO EMPLOYEE_VIEW
DO INSTED (
INSERT INTO PERSONS
VALUES (NEW.id,NEW.name,NEW.address,NEW.day_of_creation),
INSERT INTO EMPLOYEES
VALUES (NEW.id,NEW.store,NEW.paychecksize)
)
should be sufficient. But this will not be convenient as a user will have to provide the id and timestamp, even though it actually is not necessary.
How can I rewrite/extend that code base to match my criteria of convenience?
Something like:
CREATE RULE EMPLOYEE_VIEW_INSERT AS ON INSERT TO EMPLOYEES_VIEW
DO INSTEAD
(
INSERT INTO PERSONS (id, name, address, day_of_creation)
VALUES (default,NEW.name,NEW.address,default);
INSERT INTO EMPLOYEES (id, store, paychecksize)
VALUES (currval('persons_id_seq'),NEW.store,NEW.paychecksize)
);
That way the default values for persons.id and persons.day_of_creation will be the default values. Another option would have been to simply remove those columns from the insert:
INSERT INTO PERSONS (name, address)
VALUES (NEW.name,NEW.address);
Once the rule is defined, the following insert should work:
insert into employees_view (name, address, store, paychecksize)
values ('Arthur Dent', 'Some Street', 'Some Store', 42);
Btw: with a current Postgres version an instead of trigger is the preferred way to make a view updateable.