5

In my PostgreSQL 9.4 database, I have a table fields with a column name with unique values.

I'm creating a new table fields_new with a similar structure (not important here) and a column name as well. I need a way to constraint name values to be inserted to the fields_new not to be present in fields.name.

For example, if fields.name contains the values 'color' and 'length', I need to prevent fields_new.name from containing 'color' or 'length' values. So, in other words I need to provide that the name columns in both tables do not have any duplicate values between them. And the constraint should go both ways.

Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
onerror
  • 606
  • 5
  • 20
  • 1
    Please edit your question and provide sample data and acceptable and unacceptable records. – Gordon Linoff Jan 18 '16 at 12:49
  • I don't see why you would want two tables at all. Can't you just extend the current table to cater for additional attributes? –  Jan 18 '16 at 12:51
  • How should I edit the question? Full table structures won't help as the question is more or less purely theoretical. – onerror Jan 18 '16 at 12:57
  • as for two tables, I can't do that because of some reason. The real db structure is very complex, and the second table serves as a non-synchronized addition to the first one – onerror Jan 18 '16 at 12:59
  • Did you consider using a `hstore` column on the original table to store the additional attributes? –  Jan 18 '16 at 12:59
  • `non-synchronized addition` sounds like data-vault to me. (or just an ordinary temp "staging" import table) – joop Jan 18 '16 at 13:04
  • No, I can't change the original table in no way, I am not allowed – onerror Jan 18 '16 at 13:06
  • Actually there are two separate databases with partial replication. So, in both there are own versions of bnd_fields table, and only in one of them I need fields_new table for now. It stores the fields which won't replicate, they have their unique field type, and the fields_new table has a structure different from the fields table. I only need to provide that field names from both tables not intersect for some reasons. Can I do that? – onerror Jan 18 '16 at 13:11
  • 1
    You still need to be more specific. What if a value is inserted / updated in `fields` that was already added to `fields_new` (then being legal)? Or is it enough to constraint new entries *at the time they are entered*? Which means there can be a violation any time after that. Also important: your Postgres version? – Erwin Brandstetter Jan 18 '16 at 13:19
  • I edited the question. So, is there any way to provide that two columns in two different tables have only unique values across them? – onerror Jan 18 '16 at 13:40

2 Answers2

5

Only enforce constraint for new entries in fields_new

CHECK constraints are supposed to be immutable, which generally rules out any kind of reference to other tables, which are not immutable by nature.

To allow some leeway (especially with temporal functions) STABLE functions are tolerated. Obviously, this cannot be completely reliable in a database with concurrent write access. If rows in the referenced table change, they may be in violation of the constraint.

Declare the invalid nature of your constraint by making it NOT VALID (Postgres 9.1+). This way Postgres also won't try to enforce it during a restore (which might be bound to fail). Details here:

The constraint is only enforced for new rows.

CREATE OR REPLACE FUNCTION f_fields_name_free(_name text)
  RETURNS bool AS
$func$
SELECT NOT EXISTS (SELECT 1 FROM fields WHERE name = $1);
$func$  LANGUAGE sql STABLE;

ALTER TABLE fields_new ADD CONSTRAINT fields_new_name_not_in_fields
CHECK (f_fields_name_free(name)) NOT VALID;

Plus, of course, a UNIQUE or PRIMARY KEY constraint on fields_new(name) as well as on fields(name).

Related:

Enforce both ways

You could go one step further and mirror the above CHECK constraint on the 2nd table. Still no guarantees against nasty race conditions when two transactions write to both tables at the same time.

Or you could maintain a "materialized view" manually with triggers: a union of both name columns. Add a UNIQUE constraint there. Not as rock solid as the same constraint on a single table: there might be race conditions for writes to both tables at the same time. But the worst that can happen is a deadlock forcing transactions to be rolled back. No permanent violation can creep in if all write operations are cascaded to the "materialized view".

Similar to the "dark side" in this related answer:

Just that you need triggers for INSERT / UPDATE / DELETE on both tables.

Community
  • 1
  • 1
Erwin Brandstetter
  • 605,456
  • 145
  • 1,078
  • 1,228
0

I had a similar problem where I wanted to maintain a list of items per-company, along with a global list for all companies. If the company number is 0, it is to be treated as global and a new item cannot be inserted for ANY company using that name. The following script (based on the above solution) seems to work:

drop table if exists blech;

CREATE TABLE blech (
        company int,
        name_key text,
        unique (company, name_key)
);

create or replace function f_foobar(new_company int, new_name_key text) returns bool as
$func$
select not exists (
        select 1 from blech b
        where $1 <> 0
        and b.company = 0
        and b.name_key = $2);
$func$ language sql stable;

alter table blech add constraint global_unique_name_key
check (f_foobar(company, name_key)) not valid;

insert into blech values(0,'GLOB1');
insert into blech values(0,'GLOB2');

-- should succeed:
insert into blech values(1,'LOCAL1');
insert into blech values(2,'LOCAL1');

-- should fail:
insert into blech values(1,'GLOB1');

-- should fail:
insert into blech values(0,'GLOB1');
David Dombrowsky
  • 1,655
  • 3
  • 20
  • 32