How to implement constraints that go beyond a single row in PostgreSQL?Data inconsistency prohibition if a...
Why do objects rebound after hitting the ground?
Do the speed limit reductions due to pollution also apply to electric cars in France?
Is layered encryption more secure than long passwords?
Why did Ylvis use "go" instead of "say" in phrases like "Dog goes 'woof'"?
Why write a book when there's a movie in my head?
Does rolling friction increase speed of a wheel?
Would water spill from a bowl in a Bag of Holding?
What happened to Hermione’s clothing and other possessions after she wiped her parents’ memories of her?
Buying a "Used" Router
Was there a pre-determined arrangement for the division of Germany in case it surrendered before any Soviet forces entered its territory?
Are all power cords made equal?
Is practicing on a digital piano harmful to an experienced piano player?
How do I avoid the "chosen hero" feeling?
What are some idioms that means something along the lines of "switching it up every day to not do the same thing over and over"?
What are some ways of extending a description of a scenery?
What does an unprocessed RAW file look like?
Identical projects by students at two different colleges: still plagiarism?
Why "rm -r" is unable to delete this folder?
smartctl reports overall health test as passed but the tests failed?
Is there a way to pause a running process on Linux systems and resume later?
How unreachable are Jupiter's moons from Mars with the technology developed for going to Mars?
Why don't you get burned by the wood benches in a sauna?
Was Opportunity's last message to Earth "My battery is low and it's getting dark"?
Minimum Viable Product for RTS game?
How to implement constraints that go beyond a single row in PostgreSQL?
Data inconsistency prohibition if a table refers to another via two many-to-many relationshipsHow to implement a 'default' flag that can only be set on a single rowHow to constrain dataHow to change schema so that account_id reference is unique among 3 tablesTables names in a supertype/subtype schemaHow can I implement a sequence for each foreign key value?Foreign Key Constraint over two tablesHow to store user activity that supports undo?How should I model a database structure to store word information on flashcards?How to check if there are no time gaps in PostgreSQL?
How to implement constraints that go beyond a single row in PostgreSQL?
Examples 1:
With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
Examples 2:
With 5 tables: Session, Auth which has a sessionId and accountId fields, Account, Link which links Accounts to Users, and User. How can I enforce that a Session can only link to Accounts (via Auth) that are linked to the same User (via Link)? In other words, a Session cannot link to Accounts that are linked to different Users.
Thank you!
postgresql constraint
bumped to the homepage by Community♦ 15 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
|
show 3 more comments
How to implement constraints that go beyond a single row in PostgreSQL?
Examples 1:
With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
Examples 2:
With 5 tables: Session, Auth which has a sessionId and accountId fields, Account, Link which links Accounts to Users, and User. How can I enforce that a Session can only link to Accounts (via Auth) that are linked to the same User (via Link)? In other words, a Session cannot link to Accounts that are linked to different Users.
Thank you!
postgresql constraint
bumped to the homepage by Community♦ 15 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
2
A unique constraint goes beyond a single row and so do exclusion constraint or foreign key constraints. What exactly is your question?
– a_horse_with_no_name
Jul 11 '17 at 5:45
Here is an example with 3 tables Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type of previous purchases if any?
– geeko
Jul 11 '17 at 5:50
Surprising scenario... I'd go for a trigger, or a user-defined function for such a case. It is also possible that a sufficiently strange exclusion constraint (that would be more or less an inclusion) would do the trick as well.
– joanolo
Jul 11 '17 at 6:29
2
@geeko edit the question and put all relevant information there, like the example in above comment. What you describe can probably be solved with a simple UNIQUE constraint, if I understand correctly.
– ypercubeᵀᴹ
Jul 11 '17 at 8:09
2
Constraints are part of the database design as much as tables. For instance the (person,food_type) is a relationship that might be expressed as a table, with a unique constraint, and it doesn't go beyond a single row. Or food_type can be a field in the Person table.
– Daniel Vérité
Jul 12 '17 at 18:26
|
show 3 more comments
How to implement constraints that go beyond a single row in PostgreSQL?
Examples 1:
With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
Examples 2:
With 5 tables: Session, Auth which has a sessionId and accountId fields, Account, Link which links Accounts to Users, and User. How can I enforce that a Session can only link to Accounts (via Auth) that are linked to the same User (via Link)? In other words, a Session cannot link to Accounts that are linked to different Users.
Thank you!
postgresql constraint
How to implement constraints that go beyond a single row in PostgreSQL?
Examples 1:
With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
Examples 2:
With 5 tables: Session, Auth which has a sessionId and accountId fields, Account, Link which links Accounts to Users, and User. How can I enforce that a Session can only link to Accounts (via Auth) that are linked to the same User (via Link)? In other words, a Session cannot link to Accounts that are linked to different Users.
Thank you!
postgresql constraint
postgresql constraint
edited Jul 11 '17 at 11:12
geeko
asked Jul 11 '17 at 5:38
geekogeeko
18916
18916
bumped to the homepage by Community♦ 15 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
bumped to the homepage by Community♦ 15 mins ago
This question has answers that may be good or bad; the system has marked it active so that they can be reviewed.
2
A unique constraint goes beyond a single row and so do exclusion constraint or foreign key constraints. What exactly is your question?
– a_horse_with_no_name
Jul 11 '17 at 5:45
Here is an example with 3 tables Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type of previous purchases if any?
– geeko
Jul 11 '17 at 5:50
Surprising scenario... I'd go for a trigger, or a user-defined function for such a case. It is also possible that a sufficiently strange exclusion constraint (that would be more or less an inclusion) would do the trick as well.
– joanolo
Jul 11 '17 at 6:29
2
@geeko edit the question and put all relevant information there, like the example in above comment. What you describe can probably be solved with a simple UNIQUE constraint, if I understand correctly.
– ypercubeᵀᴹ
Jul 11 '17 at 8:09
2
Constraints are part of the database design as much as tables. For instance the (person,food_type) is a relationship that might be expressed as a table, with a unique constraint, and it doesn't go beyond a single row. Or food_type can be a field in the Person table.
– Daniel Vérité
Jul 12 '17 at 18:26
|
show 3 more comments
2
A unique constraint goes beyond a single row and so do exclusion constraint or foreign key constraints. What exactly is your question?
– a_horse_with_no_name
Jul 11 '17 at 5:45
Here is an example with 3 tables Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type of previous purchases if any?
– geeko
Jul 11 '17 at 5:50
Surprising scenario... I'd go for a trigger, or a user-defined function for such a case. It is also possible that a sufficiently strange exclusion constraint (that would be more or less an inclusion) would do the trick as well.
– joanolo
Jul 11 '17 at 6:29
2
@geeko edit the question and put all relevant information there, like the example in above comment. What you describe can probably be solved with a simple UNIQUE constraint, if I understand correctly.
– ypercubeᵀᴹ
Jul 11 '17 at 8:09
2
Constraints are part of the database design as much as tables. For instance the (person,food_type) is a relationship that might be expressed as a table, with a unique constraint, and it doesn't go beyond a single row. Or food_type can be a field in the Person table.
– Daniel Vérité
Jul 12 '17 at 18:26
2
2
A unique constraint goes beyond a single row and so do exclusion constraint or foreign key constraints. What exactly is your question?
– a_horse_with_no_name
Jul 11 '17 at 5:45
A unique constraint goes beyond a single row and so do exclusion constraint or foreign key constraints. What exactly is your question?
– a_horse_with_no_name
Jul 11 '17 at 5:45
Here is an example with 3 tables Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type of previous purchases if any?
– geeko
Jul 11 '17 at 5:50
Here is an example with 3 tables Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type of previous purchases if any?
– geeko
Jul 11 '17 at 5:50
Surprising scenario... I'd go for a trigger, or a user-defined function for such a case. It is also possible that a sufficiently strange exclusion constraint (that would be more or less an inclusion) would do the trick as well.
– joanolo
Jul 11 '17 at 6:29
Surprising scenario... I'd go for a trigger, or a user-defined function for such a case. It is also possible that a sufficiently strange exclusion constraint (that would be more or less an inclusion) would do the trick as well.
– joanolo
Jul 11 '17 at 6:29
2
2
@geeko edit the question and put all relevant information there, like the example in above comment. What you describe can probably be solved with a simple UNIQUE constraint, if I understand correctly.
– ypercubeᵀᴹ
Jul 11 '17 at 8:09
@geeko edit the question and put all relevant information there, like the example in above comment. What you describe can probably be solved with a simple UNIQUE constraint, if I understand correctly.
– ypercubeᵀᴹ
Jul 11 '17 at 8:09
2
2
Constraints are part of the database design as much as tables. For instance the (person,food_type) is a relationship that might be expressed as a table, with a unique constraint, and it doesn't go beyond a single row. Or food_type can be a field in the Person table.
– Daniel Vérité
Jul 12 '17 at 18:26
Constraints are part of the database design as much as tables. For instance the (person,food_type) is a relationship that might be expressed as a table, with a unique constraint, and it doesn't go beyond a single row. Or food_type can be a field in the Person table.
– Daniel Vérité
Jul 12 '17 at 18:26
|
show 3 more comments
2 Answers
2
active
oldest
votes
Using an EXCLUSION CONSTRAINT
Examples 1: With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
CREATE TABLE person ( pid serial PRIMARY KEY, pname text );
CREATE TABLE food ( fid serial PRIMARY KEY, fname text );
CREATE TABLE person_food (
pid int REFERENCES person NOT NULL,
fid int REFERENCES food NOT NULL,
EXCLUDE USING gist( pid WITH =, fid WITH != )
);
INSERT INTO person (pid,pname) VALUES (1,'John'),(2,'Mary');
INSERT INTO food (fid,fname) VALUES (1,'Pickles'),(2,'Glue');
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,2); -- boom.
ERROR: conflicting key value violates exclusion constraint "person_food_pid_fid_excl"
DETAIL: Key (pid, fid)=(1, 2) conflicts with existing key (pid, fid)=(1, 1).
add a comment |
For your first example, let's assume your tables are defined in the following way:
CREATE TABLE person
(
person_id integer PRIMARY KEY,
person_name text NOT NULL
) ;
CREATE TABLE food
(
food_id integer PRIMARY KEY,
food_name text NOT NULL,
food_type text NOT NULL /* Unnormalized, for simplicity */
) ;
CREATE TABLE purchase
(
purchased_at timestamp not null default now(),
person_id integer NOT NULL REFERENCES person(person_id),
food_id integer NOT NULL REFERENCES food(food_id),
PRIMARY KEY (person_id, food_id, purchased_at)
) ;
CREATE INDEX idx_purchaser_food_id
ON purchase(food_id, person_id) ;
I would create a function that checks for your constraint. It's a simple SQL statement, with one corner case (the first purchase), and a "literal" translation of your requirement: the current food type is in (the set of already existing ones):
CREATE FUNCTION
allowed_food_type_for_person(in _person_id integer, in _food_id integer)
RETURNS
boolean AS
$body$
SELECT
-- Corner case for first purchase
(NOT EXISTS
(SELECT
*
FROM
purchase
WHERE
person_id = _person_id
)
)
OR
(
-- Current food types
(SELECT
food_type
FROM
food
WHERE
food_id = _food_id)
IN
-- Existing food types
(SELECT
food_type
FROM
purchase
JOIN food USING(food_id)
WHERE
person_id = _person_id)
)
$body$
LANGUAGE SQL STABLE ;
Now you can add the constraint you wish:
ALTER TABLE purchase
ADD CONSTRAINT check_purchase_food_type_is_limited
CHECK(allowed_food_type_for_person(person_id, food_id));
Now, let's imagine this is your data:
INSERT INTO person
(person_id, person_name)
VALUES
(1, 'Alice Cooper'),
(2, 'Bob Geldorf') ;
INSERT INTO food
(food_id, food_name, food_type)
VALUES
(1, 'Banana', 'Fruit'),
(2, 'Orange', 'Fruit'),
(3, 'Pear', 'Fruit'),
(4, 'Lettuce', 'Vegetable'),
(5, 'Coliflower', 'Vegetable'),
(6, 'Cellery', 'Vegetable'),
(7, 'Asparagus', 'Vegetable') ;
You can have 'Alice' do a first purchase:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 1) ; -- Alice purchased a Banana (fruit)
Now, she buys an orange, OK:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 2) ; -- Alice purchased an Orange (fruit), Ok
But if she tries to buy 'Lettuce':
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 4) ; -- Alice tries to purchase a Lettuce => fail
ERROR: new row for relation "purchase" violates check constraint "check_purchase_food_type_is_limited"
DETAIL: Failing row contains (2017-07-11 19:46:08.830481, 1, 4).
You can play with it at dbfiddle here
For your 2nd example, you'd do something equivalent, but more sophisticated. In any case, there are other ways to solve the "diamond problem" you seem to have, by defining the tables in a different way. Check Data inconsistency prohibition if a table refers to another via two many-to-many relationships, for a very good explanation.
Caveat: As per comment from @DanielVerité: this can have concurrency problems that actual constraints don't have. They can (most probably) be solved by using TRANSACTION ISOLATION LEVEL SERIALIZABLE
; incurring the heavy penalty it implies.
Side question: Why are you against people having a diverse diet? ;-)
1
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk underISOLATION LEVEL SERIALIZABLE
.
– joanolo
Jul 14 '17 at 19:24
add a comment |
Your Answer
StackExchange.ready(function() {
var channelOptions = {
tags: "".split(" "),
id: "182"
};
initTagRenderer("".split(" "), "".split(" "), channelOptions);
StackExchange.using("externalEditor", function() {
// Have to fire editor after snippets, if snippets enabled
if (StackExchange.settings.snippets.snippetsEnabled) {
StackExchange.using("snippets", function() {
createEditor();
});
}
else {
createEditor();
}
});
function createEditor() {
StackExchange.prepareEditor({
heartbeatType: 'answer',
autoActivateHeartbeat: false,
convertImagesToLinks: false,
noModals: true,
showLowRepImageUploadWarning: true,
reputationToPostImages: null,
bindNavPrevention: true,
postfix: "",
imageUploader: {
brandingHtml: "Powered by u003ca class="icon-imgur-white" href="https://imgur.com/"u003eu003c/au003e",
contentPolicyHtml: "User contributions licensed under u003ca href="https://creativecommons.org/licenses/by-sa/3.0/"u003ecc by-sa 3.0 with attribution requiredu003c/au003e u003ca href="https://stackoverflow.com/legal/content-policy"u003e(content policy)u003c/au003e",
allowUrls: true
},
onDemand: true,
discardSelector: ".discard-answer"
,immediatelyShowMarkdownHelp:true
});
}
});
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fdba.stackexchange.com%2fquestions%2f179542%2fhow-to-implement-constraints-that-go-beyond-a-single-row-in-postgresql%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
2 Answers
2
active
oldest
votes
2 Answers
2
active
oldest
votes
active
oldest
votes
active
oldest
votes
Using an EXCLUSION CONSTRAINT
Examples 1: With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
CREATE TABLE person ( pid serial PRIMARY KEY, pname text );
CREATE TABLE food ( fid serial PRIMARY KEY, fname text );
CREATE TABLE person_food (
pid int REFERENCES person NOT NULL,
fid int REFERENCES food NOT NULL,
EXCLUDE USING gist( pid WITH =, fid WITH != )
);
INSERT INTO person (pid,pname) VALUES (1,'John'),(2,'Mary');
INSERT INTO food (fid,fname) VALUES (1,'Pickles'),(2,'Glue');
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,2); -- boom.
ERROR: conflicting key value violates exclusion constraint "person_food_pid_fid_excl"
DETAIL: Key (pid, fid)=(1, 2) conflicts with existing key (pid, fid)=(1, 1).
add a comment |
Using an EXCLUSION CONSTRAINT
Examples 1: With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
CREATE TABLE person ( pid serial PRIMARY KEY, pname text );
CREATE TABLE food ( fid serial PRIMARY KEY, fname text );
CREATE TABLE person_food (
pid int REFERENCES person NOT NULL,
fid int REFERENCES food NOT NULL,
EXCLUDE USING gist( pid WITH =, fid WITH != )
);
INSERT INTO person (pid,pname) VALUES (1,'John'),(2,'Mary');
INSERT INTO food (fid,fname) VALUES (1,'Pickles'),(2,'Glue');
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,2); -- boom.
ERROR: conflicting key value violates exclusion constraint "person_food_pid_fid_excl"
DETAIL: Key (pid, fid)=(1, 2) conflicts with existing key (pid, fid)=(1, 1).
add a comment |
Using an EXCLUSION CONSTRAINT
Examples 1: With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
CREATE TABLE person ( pid serial PRIMARY KEY, pname text );
CREATE TABLE food ( fid serial PRIMARY KEY, fname text );
CREATE TABLE person_food (
pid int REFERENCES person NOT NULL,
fid int REFERENCES food NOT NULL,
EXCLUDE USING gist( pid WITH =, fid WITH != )
);
INSERT INTO person (pid,pname) VALUES (1,'John'),(2,'Mary');
INSERT INTO food (fid,fname) VALUES (1,'Pickles'),(2,'Glue');
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,2); -- boom.
ERROR: conflicting key value violates exclusion constraint "person_food_pid_fid_excl"
DETAIL: Key (pid, fid)=(1, 2) conflicts with existing key (pid, fid)=(1, 1).
Using an EXCLUSION CONSTRAINT
Examples 1: With 3 tables: Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type from previous purchases if any?
CREATE TABLE person ( pid serial PRIMARY KEY, pname text );
CREATE TABLE food ( fid serial PRIMARY KEY, fname text );
CREATE TABLE person_food (
pid int REFERENCES person NOT NULL,
fid int REFERENCES food NOT NULL,
EXCLUDE USING gist( pid WITH =, fid WITH != )
);
INSERT INTO person (pid,pname) VALUES (1,'John'),(2,'Mary');
INSERT INTO food (fid,fname) VALUES (1,'Pickles'),(2,'Glue');
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,1); -- works
INSERT INTO person_food (pid,fid) VALUES (1,2); -- boom.
ERROR: conflicting key value violates exclusion constraint "person_food_pid_fid_excl"
DETAIL: Key (pid, fid)=(1, 2) conflicts with existing key (pid, fid)=(1, 1).
answered Jul 11 '17 at 20:19
Evan CarrollEvan Carroll
32.5k970221
32.5k970221
add a comment |
add a comment |
For your first example, let's assume your tables are defined in the following way:
CREATE TABLE person
(
person_id integer PRIMARY KEY,
person_name text NOT NULL
) ;
CREATE TABLE food
(
food_id integer PRIMARY KEY,
food_name text NOT NULL,
food_type text NOT NULL /* Unnormalized, for simplicity */
) ;
CREATE TABLE purchase
(
purchased_at timestamp not null default now(),
person_id integer NOT NULL REFERENCES person(person_id),
food_id integer NOT NULL REFERENCES food(food_id),
PRIMARY KEY (person_id, food_id, purchased_at)
) ;
CREATE INDEX idx_purchaser_food_id
ON purchase(food_id, person_id) ;
I would create a function that checks for your constraint. It's a simple SQL statement, with one corner case (the first purchase), and a "literal" translation of your requirement: the current food type is in (the set of already existing ones):
CREATE FUNCTION
allowed_food_type_for_person(in _person_id integer, in _food_id integer)
RETURNS
boolean AS
$body$
SELECT
-- Corner case for first purchase
(NOT EXISTS
(SELECT
*
FROM
purchase
WHERE
person_id = _person_id
)
)
OR
(
-- Current food types
(SELECT
food_type
FROM
food
WHERE
food_id = _food_id)
IN
-- Existing food types
(SELECT
food_type
FROM
purchase
JOIN food USING(food_id)
WHERE
person_id = _person_id)
)
$body$
LANGUAGE SQL STABLE ;
Now you can add the constraint you wish:
ALTER TABLE purchase
ADD CONSTRAINT check_purchase_food_type_is_limited
CHECK(allowed_food_type_for_person(person_id, food_id));
Now, let's imagine this is your data:
INSERT INTO person
(person_id, person_name)
VALUES
(1, 'Alice Cooper'),
(2, 'Bob Geldorf') ;
INSERT INTO food
(food_id, food_name, food_type)
VALUES
(1, 'Banana', 'Fruit'),
(2, 'Orange', 'Fruit'),
(3, 'Pear', 'Fruit'),
(4, 'Lettuce', 'Vegetable'),
(5, 'Coliflower', 'Vegetable'),
(6, 'Cellery', 'Vegetable'),
(7, 'Asparagus', 'Vegetable') ;
You can have 'Alice' do a first purchase:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 1) ; -- Alice purchased a Banana (fruit)
Now, she buys an orange, OK:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 2) ; -- Alice purchased an Orange (fruit), Ok
But if she tries to buy 'Lettuce':
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 4) ; -- Alice tries to purchase a Lettuce => fail
ERROR: new row for relation "purchase" violates check constraint "check_purchase_food_type_is_limited"
DETAIL: Failing row contains (2017-07-11 19:46:08.830481, 1, 4).
You can play with it at dbfiddle here
For your 2nd example, you'd do something equivalent, but more sophisticated. In any case, there are other ways to solve the "diamond problem" you seem to have, by defining the tables in a different way. Check Data inconsistency prohibition if a table refers to another via two many-to-many relationships, for a very good explanation.
Caveat: As per comment from @DanielVerité: this can have concurrency problems that actual constraints don't have. They can (most probably) be solved by using TRANSACTION ISOLATION LEVEL SERIALIZABLE
; incurring the heavy penalty it implies.
Side question: Why are you against people having a diverse diet? ;-)
1
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk underISOLATION LEVEL SERIALIZABLE
.
– joanolo
Jul 14 '17 at 19:24
add a comment |
For your first example, let's assume your tables are defined in the following way:
CREATE TABLE person
(
person_id integer PRIMARY KEY,
person_name text NOT NULL
) ;
CREATE TABLE food
(
food_id integer PRIMARY KEY,
food_name text NOT NULL,
food_type text NOT NULL /* Unnormalized, for simplicity */
) ;
CREATE TABLE purchase
(
purchased_at timestamp not null default now(),
person_id integer NOT NULL REFERENCES person(person_id),
food_id integer NOT NULL REFERENCES food(food_id),
PRIMARY KEY (person_id, food_id, purchased_at)
) ;
CREATE INDEX idx_purchaser_food_id
ON purchase(food_id, person_id) ;
I would create a function that checks for your constraint. It's a simple SQL statement, with one corner case (the first purchase), and a "literal" translation of your requirement: the current food type is in (the set of already existing ones):
CREATE FUNCTION
allowed_food_type_for_person(in _person_id integer, in _food_id integer)
RETURNS
boolean AS
$body$
SELECT
-- Corner case for first purchase
(NOT EXISTS
(SELECT
*
FROM
purchase
WHERE
person_id = _person_id
)
)
OR
(
-- Current food types
(SELECT
food_type
FROM
food
WHERE
food_id = _food_id)
IN
-- Existing food types
(SELECT
food_type
FROM
purchase
JOIN food USING(food_id)
WHERE
person_id = _person_id)
)
$body$
LANGUAGE SQL STABLE ;
Now you can add the constraint you wish:
ALTER TABLE purchase
ADD CONSTRAINT check_purchase_food_type_is_limited
CHECK(allowed_food_type_for_person(person_id, food_id));
Now, let's imagine this is your data:
INSERT INTO person
(person_id, person_name)
VALUES
(1, 'Alice Cooper'),
(2, 'Bob Geldorf') ;
INSERT INTO food
(food_id, food_name, food_type)
VALUES
(1, 'Banana', 'Fruit'),
(2, 'Orange', 'Fruit'),
(3, 'Pear', 'Fruit'),
(4, 'Lettuce', 'Vegetable'),
(5, 'Coliflower', 'Vegetable'),
(6, 'Cellery', 'Vegetable'),
(7, 'Asparagus', 'Vegetable') ;
You can have 'Alice' do a first purchase:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 1) ; -- Alice purchased a Banana (fruit)
Now, she buys an orange, OK:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 2) ; -- Alice purchased an Orange (fruit), Ok
But if she tries to buy 'Lettuce':
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 4) ; -- Alice tries to purchase a Lettuce => fail
ERROR: new row for relation "purchase" violates check constraint "check_purchase_food_type_is_limited"
DETAIL: Failing row contains (2017-07-11 19:46:08.830481, 1, 4).
You can play with it at dbfiddle here
For your 2nd example, you'd do something equivalent, but more sophisticated. In any case, there are other ways to solve the "diamond problem" you seem to have, by defining the tables in a different way. Check Data inconsistency prohibition if a table refers to another via two many-to-many relationships, for a very good explanation.
Caveat: As per comment from @DanielVerité: this can have concurrency problems that actual constraints don't have. They can (most probably) be solved by using TRANSACTION ISOLATION LEVEL SERIALIZABLE
; incurring the heavy penalty it implies.
Side question: Why are you against people having a diverse diet? ;-)
1
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk underISOLATION LEVEL SERIALIZABLE
.
– joanolo
Jul 14 '17 at 19:24
add a comment |
For your first example, let's assume your tables are defined in the following way:
CREATE TABLE person
(
person_id integer PRIMARY KEY,
person_name text NOT NULL
) ;
CREATE TABLE food
(
food_id integer PRIMARY KEY,
food_name text NOT NULL,
food_type text NOT NULL /* Unnormalized, for simplicity */
) ;
CREATE TABLE purchase
(
purchased_at timestamp not null default now(),
person_id integer NOT NULL REFERENCES person(person_id),
food_id integer NOT NULL REFERENCES food(food_id),
PRIMARY KEY (person_id, food_id, purchased_at)
) ;
CREATE INDEX idx_purchaser_food_id
ON purchase(food_id, person_id) ;
I would create a function that checks for your constraint. It's a simple SQL statement, with one corner case (the first purchase), and a "literal" translation of your requirement: the current food type is in (the set of already existing ones):
CREATE FUNCTION
allowed_food_type_for_person(in _person_id integer, in _food_id integer)
RETURNS
boolean AS
$body$
SELECT
-- Corner case for first purchase
(NOT EXISTS
(SELECT
*
FROM
purchase
WHERE
person_id = _person_id
)
)
OR
(
-- Current food types
(SELECT
food_type
FROM
food
WHERE
food_id = _food_id)
IN
-- Existing food types
(SELECT
food_type
FROM
purchase
JOIN food USING(food_id)
WHERE
person_id = _person_id)
)
$body$
LANGUAGE SQL STABLE ;
Now you can add the constraint you wish:
ALTER TABLE purchase
ADD CONSTRAINT check_purchase_food_type_is_limited
CHECK(allowed_food_type_for_person(person_id, food_id));
Now, let's imagine this is your data:
INSERT INTO person
(person_id, person_name)
VALUES
(1, 'Alice Cooper'),
(2, 'Bob Geldorf') ;
INSERT INTO food
(food_id, food_name, food_type)
VALUES
(1, 'Banana', 'Fruit'),
(2, 'Orange', 'Fruit'),
(3, 'Pear', 'Fruit'),
(4, 'Lettuce', 'Vegetable'),
(5, 'Coliflower', 'Vegetable'),
(6, 'Cellery', 'Vegetable'),
(7, 'Asparagus', 'Vegetable') ;
You can have 'Alice' do a first purchase:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 1) ; -- Alice purchased a Banana (fruit)
Now, she buys an orange, OK:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 2) ; -- Alice purchased an Orange (fruit), Ok
But if she tries to buy 'Lettuce':
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 4) ; -- Alice tries to purchase a Lettuce => fail
ERROR: new row for relation "purchase" violates check constraint "check_purchase_food_type_is_limited"
DETAIL: Failing row contains (2017-07-11 19:46:08.830481, 1, 4).
You can play with it at dbfiddle here
For your 2nd example, you'd do something equivalent, but more sophisticated. In any case, there are other ways to solve the "diamond problem" you seem to have, by defining the tables in a different way. Check Data inconsistency prohibition if a table refers to another via two many-to-many relationships, for a very good explanation.
Caveat: As per comment from @DanielVerité: this can have concurrency problems that actual constraints don't have. They can (most probably) be solved by using TRANSACTION ISOLATION LEVEL SERIALIZABLE
; incurring the heavy penalty it implies.
Side question: Why are you against people having a diverse diet? ;-)
For your first example, let's assume your tables are defined in the following way:
CREATE TABLE person
(
person_id integer PRIMARY KEY,
person_name text NOT NULL
) ;
CREATE TABLE food
(
food_id integer PRIMARY KEY,
food_name text NOT NULL,
food_type text NOT NULL /* Unnormalized, for simplicity */
) ;
CREATE TABLE purchase
(
purchased_at timestamp not null default now(),
person_id integer NOT NULL REFERENCES person(person_id),
food_id integer NOT NULL REFERENCES food(food_id),
PRIMARY KEY (person_id, food_id, purchased_at)
) ;
CREATE INDEX idx_purchaser_food_id
ON purchase(food_id, person_id) ;
I would create a function that checks for your constraint. It's a simple SQL statement, with one corner case (the first purchase), and a "literal" translation of your requirement: the current food type is in (the set of already existing ones):
CREATE FUNCTION
allowed_food_type_for_person(in _person_id integer, in _food_id integer)
RETURNS
boolean AS
$body$
SELECT
-- Corner case for first purchase
(NOT EXISTS
(SELECT
*
FROM
purchase
WHERE
person_id = _person_id
)
)
OR
(
-- Current food types
(SELECT
food_type
FROM
food
WHERE
food_id = _food_id)
IN
-- Existing food types
(SELECT
food_type
FROM
purchase
JOIN food USING(food_id)
WHERE
person_id = _person_id)
)
$body$
LANGUAGE SQL STABLE ;
Now you can add the constraint you wish:
ALTER TABLE purchase
ADD CONSTRAINT check_purchase_food_type_is_limited
CHECK(allowed_food_type_for_person(person_id, food_id));
Now, let's imagine this is your data:
INSERT INTO person
(person_id, person_name)
VALUES
(1, 'Alice Cooper'),
(2, 'Bob Geldorf') ;
INSERT INTO food
(food_id, food_name, food_type)
VALUES
(1, 'Banana', 'Fruit'),
(2, 'Orange', 'Fruit'),
(3, 'Pear', 'Fruit'),
(4, 'Lettuce', 'Vegetable'),
(5, 'Coliflower', 'Vegetable'),
(6, 'Cellery', 'Vegetable'),
(7, 'Asparagus', 'Vegetable') ;
You can have 'Alice' do a first purchase:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 1) ; -- Alice purchased a Banana (fruit)
Now, she buys an orange, OK:
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 2) ; -- Alice purchased an Orange (fruit), Ok
But if she tries to buy 'Lettuce':
INSERT INTO
purchase
(person_id, food_id)
VALUES
(1, 4) ; -- Alice tries to purchase a Lettuce => fail
ERROR: new row for relation "purchase" violates check constraint "check_purchase_food_type_is_limited"
DETAIL: Failing row contains (2017-07-11 19:46:08.830481, 1, 4).
You can play with it at dbfiddle here
For your 2nd example, you'd do something equivalent, but more sophisticated. In any case, there are other ways to solve the "diamond problem" you seem to have, by defining the tables in a different way. Check Data inconsistency prohibition if a table refers to another via two many-to-many relationships, for a very good explanation.
Caveat: As per comment from @DanielVerité: this can have concurrency problems that actual constraints don't have. They can (most probably) be solved by using TRANSACTION ISOLATION LEVEL SERIALIZABLE
; incurring the heavy penalty it implies.
Side question: Why are you against people having a diverse diet? ;-)
edited Jul 14 '17 at 19:27
answered Jul 11 '17 at 18:55
joanolojoanolo
9,73342153
9,73342153
1
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk underISOLATION LEVEL SERIALIZABLE
.
– joanolo
Jul 14 '17 at 19:24
add a comment |
1
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk underISOLATION LEVEL SERIALIZABLE
.
– joanolo
Jul 14 '17 at 19:24
1
1
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
What happens if the 3rd insert happens when the 2nd one is not yet committed? I think it passes without error and you end up with what's stored in the db violating the constraint. This is not the case for "real" constraints where concurrent inserts are automatically blocked until commit of other TXs involved in the constraint check.
– Daniel Vérité
Jul 12 '17 at 18:44
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk under
ISOLATION LEVEL SERIALIZABLE
.– joanolo
Jul 14 '17 at 19:24
@DanielVérité: Very good point. I am not 100% sure. I actually do not (explicitly) lock anything, so, I guess some scenarios with high concurrency could cause some race conditions where this can happen. It also would depend on whether this CHECK is performed at the moment the rows are inserted, or when the transaction is commited. In PostgreSQL, as of 9.6, CHECK constraints cannot be deferred. I guess then, this would only work without risk under
ISOLATION LEVEL SERIALIZABLE
.– joanolo
Jul 14 '17 at 19:24
add a comment |
Thanks for contributing an answer to Database Administrators Stack Exchange!
- Please be sure to answer the question. Provide details and share your research!
But avoid …
- Asking for help, clarification, or responding to other answers.
- Making statements based on opinion; back them up with references or personal experience.
To learn more, see our tips on writing great answers.
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
StackExchange.ready(
function () {
StackExchange.openid.initPostLogin('.new-post-login', 'https%3a%2f%2fdba.stackexchange.com%2fquestions%2f179542%2fhow-to-implement-constraints-that-go-beyond-a-single-row-in-postgresql%23new-answer', 'question_page');
}
);
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Sign up or log in
StackExchange.ready(function () {
StackExchange.helpers.onClickDraftSave('#login-link');
});
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Sign up using Google
Sign up using Facebook
Sign up using Email and Password
Post as a guest
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
Required, but never shown
2
A unique constraint goes beyond a single row and so do exclusion constraint or foreign key constraints. What exactly is your question?
– a_horse_with_no_name
Jul 11 '17 at 5:45
Here is an example with 3 tables Person, Food which has a field called type, and Purchase which links Persons to Food. How can I enforce that a person can only purchase food of the same type of previous purchases if any?
– geeko
Jul 11 '17 at 5:50
Surprising scenario... I'd go for a trigger, or a user-defined function for such a case. It is also possible that a sufficiently strange exclusion constraint (that would be more or less an inclusion) would do the trick as well.
– joanolo
Jul 11 '17 at 6:29
2
@geeko edit the question and put all relevant information there, like the example in above comment. What you describe can probably be solved with a simple UNIQUE constraint, if I understand correctly.
– ypercubeᵀᴹ
Jul 11 '17 at 8:09
2
Constraints are part of the database design as much as tables. For instance the (person,food_type) is a relationship that might be expressed as a table, with a unique constraint, and it doesn't go beyond a single row. Or food_type can be a field in the Person table.
– Daniel Vérité
Jul 12 '17 at 18:26