How do you design a table with 3 compound primary keys - sql

I have a table with columns Form, Appraiser, date and Level
A form can never have same appraisers, and a form can never have same Levels.
I tried to make primary key(Form,appraiser) and primary key(Form,level) but it says that form has multiple primary keys
If i put primary key(form,appraiser,level) people can just insert the same form and appraiser twice but just with a different level and that violates my rules.
|Form|Appraiser|Level|
1 A 1
1 B 2
1 C 3
2 A 1
2 B 2
2 C 3

I believe you could use :-
CREATE TABLE IF NOT EXISTS mytable (form INTEGER, appraiser TEXT, level INTEGER, UNIQUE(form,appraiser), UNIQUE(form,level));
e.g. Using the following
DROP TABLE IF EXISTS mytable;
CREATE TABLE IF NOT EXISTS mytable (form INTEGER, appraiser TEXT, level INTEGER, UNIQUE(form,appraiser), UNIQUE(form,level));
INSERT INTO mytable VALUES
(1,'A',1),(1,'B',2),(1,'C',3),
(2,'A',1),(2,'B',2),(2,'C',3)
;
INSERT OR IGNORE INTO mytable VALUES (1,'A',4);
INSERT OR IGNORE INTO mytable values (1,'Z',1);
The results are :-
INSERT INTO mytable VALUES
(1,'A',1),(1,'B',2),(1,'C',3),
(2,'A',1),(2,'B',2),(2,'C',3)
> Affected rows: 6
> Time: 0.083s
all added
but
INSERT OR IGNORE INTO mytable VALUES (1,'A',4)
> Affected rows: 0
> Time: 0s
not added as A has already apprasied form 1.
and also
INSERT OR IGNORE INTO mytable values (1,'Z',1)
> Affected rows: 0
> Time: 0s
not added as for 1 has already been appraised at level 1

We can try using two junction tables here:
CREATE TABLE form_appraiser (
form_id INTEGER NOT NULL,
appraiser_id INTEGER NOT NULL,
PRIMARY KEY (form_id, appraiser_id)
);
CREATE TABLE form_level (
form_id INTEGER NOT NULL,
level_id INTEGER NOT NULL,
PRIMARY KEY (form_id, level_id)
);
Each of these two tables would ensure that a given form can only be associated with a single appraiser or level.
Then, maintain a third table forms containing one record for each unique form. If you have the additional requirement that a given form can only have one appraiser or level, then add a unique constraint on the form, on one/both of the junction tables.

You can try add the unique constraint as a column identifier.
like
appraiser VARCHAR(50) UNIQUE,
form VARCHAR(50) UNIQUE,
level VARCHAR(50) UNIQUE,
In this case non of the values repeats. If you want a combination of values not to repeat you can use
UNIQUE(form, level)
This means you cannot have a form of the same level repeating.

Related

How to UPDATE or INSERT in PostgreSQL

I want to UPDATE or INSERT a column in PostgreSQL instead of doing INSERT or UPDATE using INSERT ... ON CONFLICT ... because there will be more updates than more inserts and also I have an auto incrementing id column that's defined using SERIAL so it increments the id column everytime it tries to INSERT or UPDATE and that's not what I want, I want the id column to increase only if it's an INSERT so that all ids would be in an order instead
The table is created like this
CREATE TABLE IF NOT EXISTS table_name (
id SERIAL PRIMARY KEY,
user_id varchar(30) NOT NULL,
item_name varchar(50) NOT NULL,
code_uses bigint NOT NULL,
UNIQUE(user_id, item_name)
)
And the query I used was
INSERT INTO table_name
VALUES (DEFAULT, 'some_random_id', 'some_random_name', 1)
ON CONFLICT (user_id, item_name)
DO UPDATE SET code_uses = table_name.code_uses + 1;
Thanks :)
Upserts in PostgreSQL do exactly what you described.
Consider this table and records
CREATE TABLE t (id SERIAL PRIMARY KEY, txt TEXT);
INSERT INTO t (txt) VALUES ('foo'),('bar');
SELECT * FROM t ORDER BY id;
id | txt
----+-----
1 | foo
2 | bar
(2 Zeilen)
Using upserts the id will only increment if a new record is inserted
INSERT INTO t VALUES (1,'foo updated'),(3,'new record')
ON CONFLICT (id) DO UPDATE SET txt = EXCLUDED.txt;
SELECT * FROM t ORDER BY id;
id | txt
----+-------------
1 | foo updated
2 | bar
3 | new record
(3 Zeilen)
EDIT (see coments): this is the expected behaviour of a serial column, since they're nothing but a fancy way to use sequences. Long story short: using upserts the gaps will be inevitable. If you're worried the value might become too big, use bigserial instead and let PostgreSQL do its job.
Related thread: serial in postgres is being increased even though I added on conflict do nothing

Check constraint to prevent 2 or more rows from having numeric value of 1

I have a SQL table with a column called [applied], only one row from all rows can be applied ( have the value of 1) all other rows should have the value 0
Is there a check constraint that i can write to force such a case?
If you use null instead of 0, it will be much easier.
Have a CHECK constraint to make sure the (non-null) value = 1. Also have a UNIQUE constraint to only allow a single value 1.
create table testtable (
id int primary key,
applied int,
constraint applied_unique unique (applied),
constraint applied_eq_1 check (applied = 1)
);
Core ANSI SQL, i.e. expected to work with any database.
Most databases support filtered indexes:
create unique index unq_t_applied on t(applied) where applied = 1;
To know exactly how to write trigger that will help you an info of a database you use is needed.
You wil need a trigger where this will be your test control:
SELECT COUNT(APPLIED)
FROM TEST
WHERE APPLIED = 1
If it is > 0 then do not allow insert else allow.
While this can be done with triggers and constraints, they probably require an index. Instead, consider a join table.
create table things_applied (
id smallint primary key default 1,
thing_id bigint references things(id) not null,
check(id = 1)
);
Because the primary key is unique, there can only ever be one row.
The first is activated with an insert.
insert into things_applied (thing_id) values (1);
Change it by updating the row.
update things_applied set thing_id = 2;
To deactivate completely, delete the row.
delete things_applied;
To find the active row, join with the table.
select t.*
from things t
join things_applied ta on ta.thing_id = t.id
To check if it's active at all, count the rows.
select count(id) as active
from things_applied
Try it.

SQL set constraint on how many times a PK can be referenced

I'm building a demo database of zoo for my school project and I've encountered following problem: I have a table Pavilion, which has some primary key id_pavilion and column capacity (this is information about what is the highest number of animals which can live in this pavilion).
Let's say that each pavilion can contain 2 animals at maximum.
Pavilion
id_pavilion capacity
-----------------------
1 2
2 2
3 2
4 2
Animal
id_an-column2-column3 id_pavilion
---------------------------------------
1 2
2 2
3 2
4 2
(This shows what I'm trying to prevent)
Then I have table animal, which contains some information about the animal and mainly the id_pavilion from Pavilion as a foreign key.
My question is: how can I add such a constraint that the PK id_pavilion from Pavilion can be referenced in table Animal only so many times as the capacity allows?
Looking at your example data, one could argue that every PAVILION can accommodate 2 animals, right? One could also say that the "accommodations" need to be in place before the animals can be kept in an appropriate manner. Thus, we could create a table called ACCOMMODATION, listing all available spaces.
create table pavilion( id primary key, capacity )
as
select level, 2 from dual connect by level <= 4 ;
create table accommodation(
id number generated always as identity start with 1000 primary key
, pavilionid number references pavilion( id )
) ;
Generate all accommodations
-- No "human intervention" here.
-- Only the available spaces will be INSERTed.
insert into accommodation ( pavilionid )
select id
from pavilion P1, lateral (
select 1
from dual
connect by level <= ( select capacity from pavilion where id = P1.id )
) ;
-- we can accommodate 8 animals ...
select count(*) from accommodation ;
COUNT(*)
----------
8
-- accommodations and pavilions
SQL> select * from accommodation ;
ID PAVILIONID
---------- ----------
1000 1
1001 1
1002 2
1003 2
1004 3
1005 3
1006 4
1007 4
8 rows selected.
Each animal should be in a single (defined) location. When an animal is "added" to the zoo, it can only (physically) be in a single location/accommodation. We can use a UNIQUE key and a FOREIGN key (referencing ACCOMMODATION) to enforce this.
-- the ANIMAL table will have more columns eg GENUS, SPECIES, NAME etc
create table animal(
id number generated always as identity start with 2000
-- , name varchar2( 64 )
, accommodation number
) ;
alter table animal
add (
constraint animal_pk primary key( id )
, constraint accommodation_unique unique( accommodation )
, constraint accommodation_fk
foreign key( accommodation ) references accommodation( id )
);
Testing
-- INSERTs will also affect the columns GENUS, SPECIES, NAME etc
-- when the final version of the ANIMAL table is in place.
insert into animal( accommodation ) values ( 1001 ) ;
SQL> insert into animal( accommodation ) values ( 1000 ) ;
1 row inserted.
SQL> insert into animal( accommodation ) values ( 1001 ) ;
1 row inserted.
-- trying to INSERT into the same location again
-- MUST fail (due to the unique constraint)
SQL> insert into animal( accommodation ) values ( 1000 );
Error starting at line : 1 in command -
insert into animal( accommodation ) values ( 1000 )
Error report -
ORA-00001: unique constraint (...ACCOMMODATION_UNIQUE) violated
SQL> insert into animal( accommodation ) values ( 1001 );
Error starting at line : 1 in command -
insert into animal( accommodation ) values ( 1001 )
Error report -
ORA-00001: unique constraint (...ACCOMMODATION_UNIQUE) violated
-- trying to INSERT into a location that does not exist
-- MUST fail (due to the foreign key constraint)
SQL> insert into animal( accommodation ) values ( 9999 ) ;
Error starting at line : 1 in command -
insert into animal( accommodation ) values ( 9999 )
Error report -
ORA-02291: integrity constraint (...ACCOMMODATION_FK) violated - parent key not found
Animals and accommodations
select
A.id as animal
, P.id as pavilion
, AC.id as location --(accommodation)
from pavilion P
join accommodation AC on P.id = AC.pavilionid
join animal A on AC.id = A.accommodation
;
ANIMAL PAVILION LOCATION
---------- ---------- ----------
2000 1 1000
2001 1 1001
DBfiddle here. Tested with Oracle 12c and 18c. (You'll need version 12c+ for LATERAL join to work.)
What you are trying to enforce at the database level is more of a 'business logic' rule rather than a hard data constraint. You can not implement it directly in your table designs; even if you could (as #serg mentions in the comments) it would require a very expensive (in terms of CPU/resources) lock on the table to perform the counting.
Another option, that would achieve your goal and keep the business logic separate from the data design, is to use a SQL Trigger.
A trigger can run before the data is inserted into your table; here you can check how many rows have already been inserted for that 'pavilion entity' and abort or allow the insert.
A comment for the "school project" side of things:
This being said, the sort of logic you are talking about is much better served within your consuming application rather than the database (my opinion, others may disagree). Also perhaps think about defining the size limit in the data, so you can have different sized pavilions.
Notes:
For anyone visiting this question in the future, the above link is for an oracle trigger (as OP has tagged the question for oracle). This link is for Microsoft SQL Server Triggers.
The answer is "not easily". Although the idea of keeping the "accommodations" in the pavilions as a separate table is a clever one, animals are put into pavilions, not accommodations. Modeling accommodations makes it much trickier to move animals around.
Perhaps the simplest approach is to use triggers. This starts with an animal_count column in pavilions. This column starts at zero and is incremented or decremented as animals move in or out. You can use a check constraint to validate that the pavilion is not over-capacity.
Unfortunately, maintaining this column requires triggers on the animals table, one for insert, update, and delete.
In the end, the trigger is maintaining the count and if you attempt to put an animal in a full pavilion, you will violate the check constraint.
You need a column (say "NrOccupants") that is updated when an animal is placed into or removed from each pavilion. Then you add a check constraint to that column that prevents the application code from adding more animals to a pavilion than is permitted by the rule that is enforced by the check constraint.
Here is an example of the SQL DDL that would do that.
CREATE SCHEMA Pavilion
GO
CREATE TABLE Pavilion.Pavilion
(
pavilionNr int NOT NULL,
capacity tinyint CHECK (capacity IN (2)) NOT NULL,
nrOccupants tinyint CHECK (nrOccupants IN (0, 2)) NOT NULL,
CONSTRAINT Pavilion_PK PRIMARY KEY(pavilionNr)
)
GO
CREATE TABLE Pavilion.Animal
(
animalNr int NOT NULL,
name nchar(50) NOT NULL,
pavilionNr int NOT NULL,
type nchar(50) NOT NULL,
weight smallint NOT NULL,
CONSTRAINT Animal_PK PRIMARY KEY(animalNr)
)
GO
ALTER TABLE Pavilion.Animal ADD CONSTRAINT Animal_FK FOREIGN KEY (pavilionNr) REFERENCES Pavilion.Pavilion (pavilionNr) ON DELETE NO ACTION ON UPDATE NO ACTION
GO

ORA-00904: "NO_OF_PROJ_PER_CON_PY": invalid identifier

I am trying to create a fact table which will display the number of projects per consultant per year. It has 2 dimension tables 1 for time (report_time_dim) and the other for consultants(consultant_dim) then the main fact table (fact_table).
CREATE TABLE fact_table(
fact_key INTEGER NOT NULL,
consultant_key INTEGER NOT NULL,
time_key INTEGER NOT NULL,
no_of_projects_py INTEGER,
no_of_consultants_py INTEGER,
no_of_accounts_py INTEGER,
no_of_proj_per_con_py INTEGER,
fk1_time_key INTEGER NOT NULL,
fk2_consultant_key INTEGER NOT NULL,
-- Specify the PRIMARY KEY constraint for table "fact_table".
-- This indicates which attribute(s) uniquely identify each row of data.
CONSTRAINT pk_fact_table PRIMARY KEY (consultant_key,time_key)
);
CREATE TABLE report_time_dim(
time_key INTEGER NOT NULL,
year INTEGER,
-- Specify the PRIMARY KEY constraint for table "time_dim".
-- This indicates which attribute(s) uniquely identify each row of data.
CONSTRAINT pk_report_time_dim PRIMARY KEY (time_key)
);
CREATE TABLE consultant_dim(
consultant_key INTEGER NOT NULL,
project_id INTEGER,
consultant_id INTEGER,
-- Specify the PRIMARY KEY constraint for table "consultant_dim".
-- This indicates which attribute(s) uniquely identify each row of data.
CONSTRAINT pk_consultant_dim PRIMARY KEY (consultant_key)
);
Each table has it's own surrogate key and I have managed to populate the time and consultant tables successfully, however the issue I'm having is with the fact table. When I try to populate it I get the error ORA-00904: "NO_OF_PROJ_PER_CON_PY": invalid identifier. I am unsure how I can go about fixing this and populating the fact table so it will display the information I want. Any help would be appreciated.
--populate fact_table
--table that lists consultant ids, project ids and years
DROP TABLE temp_fact1;
CREATE TABLE temp_fact1 AS
SELECT project_id, fk2_consultant_id, to_number(to_char(lds_project.pj_actual_start_date, 'YYYY')) as which_year FROM lds_project;
--display table
SELECT * FROM temp_fact1;
--list that counts the number of projects for each consultant and specify the year
DROP TABLE temp_fact2;
CREATE TABLE temp_fact2 AS
SELECT which_year, fk2_consultant_id, COUNT(*) project_id FROM temp_fact1 GROUP by fk2_consultant_id, which_year;
--display table
SELECT * FROM temp_fact2;
--fact table surrogate key
DROP SEQUENCE fact_seq;
CREATE SEQUENCE fact_seq
START WITH 1
INCREMENT BY 1
MAXVALUE 1000000
MINVALUE 1
NOCACHE
NOCYCLE;
--load data
INSERT INTO fact_table (fact_key, consultant_key, time_key, no_of_proj_per_con_py)
SELECT fact_seq.nextval, consultant_key, report_time_dim.time_key, no_of_proj_per_con_py FROM temp_fact2, report_time_dim WHERE temp_fact2.which_year = report_time_dim.year;
Try just running this select by itself - it's the last line in your script.
SELECT fact_seq.nextval,
consultant_key,
report_time_dim.time_key,
no_of_proj_per_con_py
FROM temp_fact2, report_time_dim
WHERE temp_fact2.which_year = report_time_dim.year;
It doesn't look like either TEMP_FACT2 or REPORT_TIME_DIM has a column named no_of_proj_per_con_py. I'm not sure where you want to pull that data from, actually.

In Postgresql, force unique on combination of two columns

I would like to set up a table in PostgreSQL such that two columns together must be unique. There can be multiple values of either value, so long as there are not two that share both.
For instance:
CREATE TABLE someTable (
id int PRIMARY KEY AUTOINCREMENT,
col1 int NOT NULL,
col2 int NOT NULL
)
So, col1 and col2 can repeat, but not at the same time. So, this would be allowed (Not including the id)
1 1
1 2
2 1
2 2
but not this:
1 1
1 2
1 1 -- would reject this insert for violating constraints
CREATE TABLE someTable (
id serial PRIMARY KEY,
col1 int NOT NULL,
col2 int NOT NULL,
UNIQUE (col1, col2)
)
autoincrement is not postgresql. You want a integer primary key generated always as identity (or serial if you use PG 9 or lower. serial was soft-deprecated in PG 10).
If col1 and col2 make a unique and can't be null then they make a good primary key:
CREATE TABLE someTable (
col1 int NOT NULL,
col2 int NOT NULL,
PRIMARY KEY (col1, col2)
)
Create unique constraint that two numbers together CANNOT together be repeated:
ALTER TABLE someTable
ADD UNIQUE (col1, col2)
If, like me, you landed here with:
a pre-existing table,
to which you need to add a new column, and
also need to add a new unique constraint on the new column as well as an old one, AND
be able to undo it all (i.e. write a down migration)
Here is what worked for me, utilizing one of the above answers and expanding it:
-- up
ALTER TABLE myoldtable ADD COLUMN newcolumn TEXT;
ALTER TABLE myoldtable ADD CONSTRAINT myoldtable_oldcolumn_newcolumn_key UNIQUE (oldcolumn, newcolumn);
---
ALTER TABLE myoldtable DROP CONSTRAINT myoldtable_oldcolumn_newcolumn_key;
ALTER TABLE myoldtable DROP COLUMN newcolumn;
-- down
Seems like regular UNIQUE CONSTRAINT :)
CREATE TABLE example (
a integer,
b integer,
c integer,
UNIQUE (a, c));
More here