Unique constraints on multiple columns that cannot both be null - sql

I wanted to ask if there's a better way of modeling the following behavior in Postgres 10:
CREATE TABLE test.my_table
(
id UUID PRIMARY KEY,
id_a UUID,
id_b UUID,
some_shared_data JSONB,
UNIQUE (id_a, id_b)
);
CREATE UNIQUE INDEX IF NOT EXISTS b_null_constraint ON test.my_table (id_a) WHERE id_b IS NULL;
CREATE UNIQUE INDEX IF NOT EXISTS a_null_constraint ON test.my_table (id_b) WHERE id_a IS NULL;
ALTER TABLE test.my_table
ADD CONSTRAINT both_null_constraint CHECK (
(id_b IS NOT NULL) OR (id_a IS NOT NULL));
I.e. the constraints are:
Both id_a and id_b cannot be null
The combination of id_a and id_b must be unique (including cases where one of them is null)
It feels to me the code above to set this up is not very expressive. Would people do this in another/more normalized way? I tried splitting this up in separate tables but then constraint (1.) is hard to satisfy.

It is possible to do this with just two unique constraints. The second one is:
CREATE UNIQUE INDEX IF NOT EXISTS ab_null_constraint ON my_table ( coalesce(id_a, id_b), (id_a is null) WHERE id_a IS NULL or id_b is null;
Here is a db<>fiddle.
Actually, you can combine all this into one unique index:
CREATE UNIQUE INDEX IF NOT EXISTS ab_null_constraint ON
my_table ( coalesce(id_a, id_b),
coalesce(id_b, id_a),
(id_a is null),
(id_b is null)
);
Here is a db<>fiddle for this.
You might find your original formulation more maintainable.

Related

How to insert multiple rows into table B, and update table A's null foreign keys with the new IDs?

I've found a million things sounding kind of similar on StackOverflow, but not my case exactly. I'll simplify as much as possible:
I have two tables as follows:
CREATE TABLE B (id uuid PRIMARY KEY);
CREATE TABLE A (id uuid PRIMARY KEY, b_id uuid REFERENCES b);
There are some NULL values in A.b_id. I am trying to create a migration that does the following:
For every row in A with no b_id, create a new row in B, and assign its id to A.b_id.
How can I accomplish this in one query?
Assuming you want a distinct entry in b for every row with a missing UUID in a:
WITH upd AS (
UPDATE a
SET b_id = gen_random_uuid()
WHERE b_id IS NULL
RETURNING b_id
)
INSERT INTO b (id)
SELECT b_id FROM upd;
db<>fiddle here
This works because it's a single command, and the FK reference is only enforced at the end of the command.
See:
SET CONSTRAINTS ALL DEFERRED not working as expected
Constraint defined DEFERRABLE INITIALLY IMMEDIATE is still DEFERRED?

Unique column value combination with NULL values

I'm trying to conditionally insert some data into a table, where each combination of column values may only appear at most once in the table. The schema looks something like this:
CREATE TABLE foobar (
id SERIAL PRIMARY KEY,
a_id INTEGER,
b_id INTEGER,
c_id INTEGER,
ident VARCHAR(32),
date_a timestamp,
date_b timestamp,
FOREIGN KEY a_id REFERENCES a (id) ON DELETE CASCADE,
FOREIGN KEY b_id REFERENCES b (id) ON DELETE CASCADE,
FOREIGN KEY c_id REFERENCES c (id) ON DELETE CASCADE));
The combination of (a_id, b_id, c_id, ident) is unique, but only for rows where date_a AND date_b are both NULL.
I want to be able to insert a new row only if the a_id, b_id, c_id, task combination is not already in the db. if it is it doesn't have to do anything.
At first I tried to create a unique constraint on these columns, but the problem is that a_id, b_id and c_id are allowed to be NULL, as long as least one of them is not null. This ruins the unique constraint. Because the a, b and c_id fields are foreign keys, I can't set them to some other stub value (like -1).
I tried playing with locking (against my better judgement) which resulted in a deadlock within a couple of minutes of testing.
Is there any standard solution to this problem?
Rather than using a unique constraint, you should probably be using a unique index on the exact conditions you want to check for. You could then coalesce the null values into a dummy value, such as -1. Something like:
CREATE UNIQUE INDEX
foobar_test ON foobar
(COALESCE(a_id, -1), COALESCE(b_id, -1), COALESCE(c_id, -1), ident) -- Nulls become -1
WHERE date_a is null and date_b is null; -- Only check when date_a and date_b is null
This would make sure a_id, b_id, c_id, and ident were unique (including their combination of null values) for all rows where both date_a and date_b were null.
In a similar situations in multiple productive databases of mine I just add a "default row" to referenced tables. I use id = 0 for these and start real surrogate keys at 10 or 100. Or you could use -1 (or whatever suits you) if 0 is reserved.
This way I can set all fk columns to NOT NULL DEFAULT 0 and (partial) UNIQUE constraints work out of the box:
CREATE UNIQUE INDEX tbl_uni_idx ON tbl (a_id, b_id, c_id, ident)
WHERE date_a IS NULL AND date_b IS NULL;

Creating a Foreign Key based on an index name rather than table(columns)

I have a table with a timestamp field onto which I've created a composite index.
CREATE INDEX "IDX_NAME_A" ON TABLE "A" (a_id, extract(year FROM created_at))
I have another table which stores a year and a_id which I'd like to have a foreign key relation.
I can't seem to find the syntax to do what I want.
ALTER TABLE "B"
ADD FOREIGN KEY(a_id, a_year)
REFERENCES A(a_id, extract(YEAR FROM created_at));
produces:
ERROR: syntax error at or near "("
I've also tried ...
ALTER TABLE "B"
ADD FOREIGN KEY(a_id, a_year)
USING INDEX "IDX_NAME_A";
Any Ideas?
Table A
--------
a_id serial,
created_at timestamp default now()
Table B
-------
b_id serial
a_id integer not null,
a_year date_part('year')
A foreign key constraint cannot reference an index. It has to be a table.
A foreign key constraint cannot reference an expression. It has to point to column name(s) of the referenced table.
And there has to exist a unique index (primary key qualifies, too, implicitly) on the set of referenced columns.
Start by reading the manual about foreign keys here.
The superior design would be to just drop the column b.a_year. It is 100% redundant and can be derived from a.created_at any time.
If you positively need the column (for instance to enforce one row per year for certain criteria in table b), you can achieve your goal like this:
CREATE TABLE a (
a_id serial
,created_at timestamp NOT NULL DEFAULT now()
,a_year NOT NULL DEFAULT extract(year FROM now())::int -- redundant, for fk
,CHECK (a_year = extract(year FROM created_at)::int)
);
CREATE UNIQUE INDEX a_id_a_year_idx ON TABLE a (a_id, a_year); -- needed for fk
CREATE TABLE b (
b_id serial
,a_id integer NOT NULL
,a_year int -- doesn't have to be NOT NULL, but might
,CONSTRAINT id_year FOREIGN KEY (a_id, a_year) REFERENCES a(a_id, a_year)
);
Updated after #Catcall's comment:
The CHECK constraint in combination with the column DEFAULT and NOT NULL clauses enforces your regime.
Alternatively (less simple, but allowing for NULL values) you could maintain the values in a.a_year with a trigger:
CREATE OR REPLACE FUNCTION trg_a_insupbef()
RETURNS trigger AS
$BODY$
BEGIN
NEW.a_year := extract(year FROM NEW.created_at)::int;
RETURN NEW;
END;
$BODY$
LANGUAGE plpgsql VOLATILE;
CREATE TRIGGER insupbef
BEFORE INSERT OR UPDATE ON a
FOR EACH ROW EXECUTE PROCEDURE trg_a_insupbef();

Complex Foreign Key Constraint in SQL

Is there a way to define a constraint using SQL Server 2005 to not only ensure a foreign key exists in another table, but also meets a certain criteria?
For example, say I have two tables:
Table A
--------
Id - int
FK_BId - int
Table B
--------
Id - int
Name - string
SomeBoolean - bit
Can I define a constraint that sayd FK_BId must point to a record in Table B, AND that record in Table B must have SomeBoolean = true? Thanks in advance for any help you can provide.
You can enforce the business rule using a composite key on (Id, SomeBoolean), reference this in table A with a CHECK constraint on FK_BSomeBoolean to ensure it is always TRUE. BTW I'd recommend avoiding BIT and instead using CHAR(1) with domain checking e.g.
CHECK (SomeBoolean IN ('F', 'T'))
The table structure could look like this:
CREATE TABLE B
(
Id INTEGER NOT NULL UNIQUE, -- candidate key 1
Name VARCHAR(20) NOT NULL UNIQUE, -- candidate key 2
SomeBoolean CHAR(1) DEFAULT 'F' NOT NULL
CHECK (SomeBoolean IN ('F', 'T')),
UNIQUE (Id, SomeBoolean) -- superkey
);
CREATE TABLE A
(
Ib INTEGER NOT NULL UNIQUE,
FK_BId CHAR(1) NOT NULL,
FK_BSomeBoolean CHAR(1) DEFAULT 'T' NOT NULL
CHECK (FK_BSomeBoolean = 'T')
FOREIGN KEY (FK_BId, FK_BSomeBoolean)
REFERENCES B (Id, SomeBoolean)
);
I think what you're looking for is out of the scope of foreign keys, but you could do the check in triggers, stored procedures, or your code.
If it is possible to do, I'd say that you would make it a compound foreign key, using ID and SomeBoolean, but I don't think it actually cares what the value is.
In some databases (I can't check SQL Server) you can add a check constraint that references other tables.
ALTER TABLE a ADD CONSTRAINT fancy_fk
CHECK (FK_BId IN (SELECT Id FROM b WHERE SomeBoolean));
I don’t believe this behavior is standard.

Using references in MYSQL

I got an answer to another question here:
DB Schema For Chats?
It's a great answer, but I am not understanding the bit about references. I can do SQL statements but I have never used references.
What are they used for?
How are they used?
Give an example please
The REFERENCES keyword is part of a foreign key constraint and it causes MySQL to require that the value(s) in the specified column(s) of the referencing table are also present in the specified column(s) of the referenced table.
This prevents foreign keys from referencing ids that do not exist or were deleted, and it can optionally prevent you from deleting rows whilst they are still referenced.
A specific example is if every employee must belong to a department then you can add a foreign key constraint from employee.departmentid referencing department.id.
Run the following code to create two test tables tablea and tableb where the column a_id in tableb references the primary key of tablea. tablea is populated with a few rows.
CREATE TABLE tablea (
id INT PRIMARY KEY,
foo VARCHAR(100) NOT NULL
) Engine = InnoDB;
INSERT INTO tablea (id, foo) VALUES
(1, 'foo1'),
(2, 'foo2'),
(3, 'foo3');
CREATE TABLE tableb (
id INT PRIMARY KEY,
a_id INT NOT NULL,
bar VARCHAR(100) NOT NULL,
FOREIGN KEY fk_b_a_id (a_id) REFERENCES tablea (id)
) Engine = InnoDB;
Now try these commands:
INSERT INTO tableb (id, a_id, bar) VALUES (1, 2, 'bar1');
-- succeeds because there is a row in tablea with id 2
INSERT INTO tableb (id, a_id, bar) VALUES (2, 4, 'bar2');
-- fails because there is not a row in tablea with id 4
DELETE FROM tablea WHERE id = 1;
-- succeeds because there is no row in tableb which references this row
DELETE FROM tablea WHERE id = 2;
-- fails because there is a row in tableb which references this row
Important note: Both tables must be InnoDB tables or the constraint is ignored.
The REFERENCES keyword shows a foreign key constraint, which means that:
FOREIGN KEY (`chat_id` ) REFERENCES `chats`.`chat` (`id` )
...the chat_id column in the current table can only contain values that already exist in the chat table, id column.
For example, if the CHAT.id column contains:
id
----
a
b
c
..you can not add any values other than a/b/c into the chat_id column.