multiple foreign keys as primary key postgres, should I do it? - sql

This is one of those: why I should or why I should not.
So my books app has reviews, but one user must not review the same book more than one time. In my point of view makes sense to create a table for the reviews and make the user_id and the book_id (ISBN) as the PRIMARY KEY for the reviews table. But, could it be a problem at some point if the application gets too many reviews, for example, could that decision slow the queries?
I am using postgres and I am not sure if the following code is correct:
CREATE TABLE users(
user_id PRIMARY KEY SERIAL,
user_name VARCHAR NOT NULL UNIQUE,
pass_hash VARCHAR NOT NULL,
email VARCHAR NOT NULL UNIQUE,
);
CREATE TABLE books(
book_id PRIMARY KEY BIGINT,
author VARCHAR NOT NULL,
title VARCHAR NOT NULL,
year INT NOT NULL CHECK (year > 1484),
review_count INT DEFAULT 0 NOT NULL,
avrg_score FLOAT,
);
CREATE TABLE reviews(
user_id INT FOREIGN KEY REFERENCES users(user_id) NOT NULL
book_id INT FOREIGN KEY REFERENCES books(book_id) NOT NULL
score INT NOT NULL CHECK (score > 0, score < 11)
PRIMARY KEY (book_id, user_id)
);

This is a perfectly valid design choice.
You have a many-to-many relationship between books and users, which is represented by the reviews table. Having a compound primary key based on two foreign keys lets you enforce referential integrity (a given tuple may only appear once), and at the same time provide a primary key for the table.
Another option would be to have a surrogate primary key for the bridge table. This could make things easier if you need to reference the reviews from another table, but you would still need a unique constraint on both foreign key columns for integrity, so this would actually result in extra space being used.
When it comes to your code, it has a few issues:
the primary key keyword goes after the datatype
the check constraint is incorrectly formed
missing or additional commas here and there
Consider:
CREATE TABLE users(
user_id SERIAL PRIMARY KEY ,
user_name VARCHAR NOT NULL UNIQUE,
pass_hash VARCHAR NOT NULL,
email VARCHAR NOT NULL UNIQUE
);
CREATE TABLE books(
book_id BIGINT PRIMARY KEY,
author VARCHAR NOT NULL,
title VARCHAR NOT NULL,
year INT NOT NULL CHECK (year > 1484),
review_count INT DEFAULT 0 NOT NULL,
avrg_score FLOAT
);
CREATE TABLE reviews(
user_id INT REFERENCES users(user_id) NOT NULL,
book_id INT REFERENCES books(book_id) NOT NULL,
score INT NOT NULL CHECK (score > 0 and score < 11),
PRIMARY KEY (book_id, user_id)
);

I the above is good but I would drop columns review_count and avrg_score from books. These are derivable when needed. If needed for your application then instead of storing these create a view to derive them. This avoids the always complicated process of maintaining running values:
create view books_vw as
select b.book_id
, b.author
, b.title
, b.year
, count(r.*) review_count
, avg(r.score) avrg_score
from books b
left join reviews r
on r.book_id = b.book_id
group by
b.book_id
, b.author
, b.title
, b.year
;

Related

Adding an Array of INT column where each value is a primary key from another table

Given two tables like so
CREATE TABLE participants(
id SERIAL PRIMARY KEY,
Name TEXT NOT NULL,
Title TEXT NOT NULL
);
CREATE TABLE meetings (
id SERIAL PRIMARY KEY,
Organizer TEXT NOT NULL,
StartTime DATE NOT NULL,
EndTime DATE NOT NULL,
Participants INT[],
);
I want Participants column of 'meetings' table to contain set of integers which are all primary keys (specific participant) from 'participants' table.
How do I define this field in 'meetings' table ?
The old fashioned way is to create a many-many table, with a couple of commonsense constraints:
CREATE TABLE meetings_participants(
meeting_id int not null,
participant_id int not null,
primary key (meeting_id, participant_id),
foreign key(meeting_id) references meetings(id),
foreign key(participant_id) references participants(id)
)
Now it is easy to add and remove people to meetings be inserting or deleting rows or query meetings that e.g. have 4 or more participants.
A more common approach is to create a junction table for the meeting participants.
CREATE TABLE participants (
participant_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
Name TEXT NOT NULL,
Title TEXT NOT NULL
);
CREATE TABLE meetings (
meeting_id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
Organizer TEXT NOT NULL,
StartTime DATE NOT NULL,
EndTime DATE NOT NULL
);
CREATE TABLE meeting_participants(
meeting_id INT NOT NULL,
participant_id INT NOT NULL,
PRIMARY KEY (meeting_id, participant_id),
FOREIGN KEY (meeting_id) REFERENCES meetings(meeting_id),
FOREIGN KEY (participant_id) REFERENCES participants(participant_id)
);
Which is then used to join the 2 tables.
For example:
SELECT m.*, p.*
FROM meeting_participants mp
JOIN meetings m USING(meeting_id)
JOIN participants p USING(participant_id)
WHERE m.Organizer = 'John Doe';

relational model query for simple person-car example

I'd like some insight on a relational model I have created for PostgreSQL. It has to do with person and car relationships.
CREATE TABLE "person" (
"id" serial NOT NULL PRIMARY KEY,
"name" varchar(300) NOT NULL,
"car_id" integer REFERENCES car (id));
CREATE TABLE "car" (
"id" serial NOT NULL PRIMARY KEY,
"type" varchar(50) NOT NULL);
CREATE TABLE "car_person_relations" (
"id" serial NOT NULL PRIMARY KEY,
"car_type" varchar(50) NOT NULL REFERENCES "car" ("type"),
"person_id" integer NOT NULL REFERENCES "person" ("id"));
Ultimately, I'd like to get the most popular car type based on how many people have it,
i.e. how many "person"s it is associated with. What query can I use to achieve this? And is this relational table (car_person_relations) sufficient to achieve it?
Any insight would be much appreciated
If I understand correctly, this is a simple aggregation with some limiting:
select car_type, count(*) as num_persons
from car_person_relations cpr
group by car_type
order by count(*) desc
limit 1;
First off take a look at your data model, it has a couple fundamental problems.
In CAR_PERSONS_RELATIONS table you are attempting to create a FK on car_type. However, to do so car_type would have to be
unique in the car table. From the documentation section 5.4.5. Foreign Keys .
A foreign key constraint specifies that the values in a column (or a group of columns) must match the values
appearing in some row of another table. We say this maintains the
referential integrity between two related tables. ... A foreign key
must reference columns that either are a primary key or form a unique
constraint.
But this would there could only be 1 car for a given type in the
table.
Now look at the PERSON table. This table contains a FK to car_id, thus establishing a M:1 to CAR. Basically this relationship
says "A person can own only 1 car, but a car can be owned by many persons". Your description and table name indicate
this is NOT the relationship you are trying to define.
Correcting requires the following changes:
Add car_id to CAR_PERSON_RELATIONS.
Remove car_id from PERSON.
Remove car_type from CAR_PERSON_RELATIONS.
Redefine CAR_PERSON_RELATIONS PK to car_id, person_id. Alternately
leave the surrogate PK and create UK on car_id,person_id).
Create the FKs from CAR_PERSON_RELATIONS to CAR and PERSON.
Revised Model
create table person (
id serial
,name varchar(300) not null
,constraint person_pk primary key (id)
);
create table car (
id serial
,type varchar(50) not null
,constraint car_pk primary key (id)
);
create table car_person_relations (
car_id integer
,person_id integer
,constraint car_person_relations primary key (car_id, person_id)
,constraint car_person_relations_2_car_fk
foreign key (car_id)
references (car.id)
,constraint car_person_relations_2_person_fk
foreign key (person_id)
references (person.id)
):
And then the necessary query becomes:
select cpr.car_type, count(*) as num_persons
from car_person_relations cpr
join car c
on cpr.car_id = c.id
group by cpr.car_type
order by count(*) desc
limit 1;

How can I fix this PostgreSQL foreign key error?

I have created three tables (users, candidates and votes), and when a user create a vote, it is sent to the votes table which has a foreign key candidate from the candidates table, but when I create a new vote, I get this error
ERROR: insert or update on table "votes" violates foreign key constraint "votes_candidate_fkey"
DETAIL: Key (candidate)=(6) is not present in table "candidates".
The candidate table has the candidate with id 6, but when I create a vote I get a foreign key error, how can I solve this, below is the shema
CREATE TABLE IF NOT EXISTS users(
id serial PRIMARY KEY,
first_name VARCHAR (100) NOT NULL,
last_name VARCHAR (100) NOT NULL,
other_name VARCHAR (100) NOT NULL,
email VARCHAR (100) UNIQUE NOT NULL,
password VARCHAR (100) NOT NULL,
phone_Number VARCHAR (100) UNIQUE NOT NULL,
passport_Url VARCHAR (255) NOT NULL,
is_Admin BOOLEAN DEFAULT 'no'
);
CREATE TABLE IF NOT EXISTS candidates(
id serial PRIMARY KEY,
office INTEGER REFERENCES offices(id),
party INTEGER REFERENCES parties(id),
candidate INTEGER UNIQUE REFERENCES users(id)
);
CREATE TABLE IF NOT EXISTS votes(
created_by INTEGER REFERENCES users(id),
office INTEGER REFERENCES offices(id),
candidate INTEGER REFERENCES candidates(id),
created_On DATE NOT NULL DEFAULT NOW(),
PRIMARY KEY (office, created_By)
);
User 6 exists, but you only have five candidates and their ids are 1, 2, 3, 4, and 5.
Your foreign key is to id, not the candidate column:
candidate INTEGER REFERENCES candidates(id),
----------------------------------------^
The appropriate id is 5, for user 6.
You may want to set up the candidates table with the primary key being the user id (that is, candidates are subsets of users). If so remove the serial column:
CREATE TABLE IF NOT EXISTS candidates(
candidate INTEGER PRIMARY KEY REFERENCES users(id),
office INTEGER REFERENCES offices(id),
party INTEGER REFERENCES parties(id)
);
I would recommend this. If you as the database designer are already confused about the difference between a "candidate" and a "user", then no doubt future users of the database will have the same confusion.
The message is very much clear that your candidates tabled does not contain id=6 but you are trying to insert that value into votes tables that's why you got the error, cause it is a foreign key violation
in votes table candidate INTEGER REFERENCES candidates(id) this is relate to candidates tables id column not with candidate column that you thought

Postgresql multiple tables with same foreign key unique constraint

I have following tables on PostgreSQL 9.4
CREATE TABLE "user" (
id SERIAL PRIMARY KEY,
email CHARACTER VARYING NOT NULL,
password CHARACTER VARYING NOT NULL
);
CREATE TABLE "dealer" (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES "user" (id) ON DELETE RESTRICT
);
CREATE TABLE "affiliate" (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL REFERENCES "user" (id) ON DELETE RESTRICT
);
Is it possible to force user_id value to be unique across tables dealer and affiliate?
There are different setups to use for inheritance in SQL and for this you could just use an integer column type in the table user that marks the type of the user and would reference to table user_type (id,name) that would have the values 1,dealer and 2,affiliate:
CREATE TABLE user_type (
id INTEGER PRIMARY KEY, --could be SERIAL
name text
);
INSERT INTO user_type VALUES (1,'dealer'), (2, 'affiliate');
CREATE TABLE "user" (
id SERIAL PRIMARY KEY,
email CHARACTER VARYING NOT NULL,
password CHARACTER VARYING NOT NULL,
user_type INTEGER REFERENCES user_type NOT NULL,
UNIQUE(id,user_type)
);
This in itself wouldn't force uniqueness across tables so after implementing this you would have the following options:
Drop the tables dealer and affiliate - you won't need them if you rely on the type field to see which one the user is.
If you have to keep those inherited tables you can:
Use triggers - these triggers check the uniqueness and would be actived on INSERT or UPDATE
Another (a bit clumsy) solution: add user_type field to both subtables like this:
CREATE TABLE "dealer" (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
user_type INTEGER NOT NULL DEFAULT 1 check (user_type = 1),
FOREIGN KEY (user_id,user_type) REFERENCES "user"(id,user_type) ON DELETE RESTRICT
);
CREATE TABLE "affiliate" (
id SERIAL PRIMARY KEY,
user_id INTEGER NOT NULL,
user_type INTEGER NOT NULL DEFAULT 2 check (user_type = 2),
FOREIGN KEY (user_id,user_type) REFERENCES "user"(id,user_type) ON DELETE RESTRICT
);
The checks and foreign keys together make sure you cannot have both types of user in the main table. Note that user_id might be used as the PRIMARY KEY in the subtables too. Currently a row in user might have several dealer rows linked to it so at least you might want to set user_id foreign keys in subtables as UNIQUE.

SQL Unknown number of attributes

I have a one-to-many relationship between two classes for this situation. I have a swimming competition and that competition can have x swimmers.
How can I create an SQL table for this, I know I will have to use the Primary Key of Swimmers as a foreign key in the Swimming competition but I have no idea how to represent the correct number of attributes since it's unknown.
This is called a m:n relationship and usually solved with a mapping table.
Something like this:
create table swimmer
(
id integer not null primary key,
lastname varchar(100) not null,
firstname varchar(100)
)
create table competition
(
id integer not null primary key,
name varchar(50) not null,
comp_date date not null
)
create table participant
(
swimmer_id integer not null,
competition_id integer not null,
rank_in_competetion integer,
primary key (swimmer_id, competition_id),
constraint fk_participant_swimmer (swimmer_id) references swimmer(id),
constraint fk_participant_competition (competition_id) references competition(id)
)