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

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

Related

Directionless relationship failing in PostgreSQL

I am trying to create a 2-way relationship table in PostgreSQL for my 3 objects. This idea has stemmed from the following question https://dba.stackexchange.com/questions/48568/how-to-relate-two-rows-in-the-same-table where I also want to store the relationship and its reverse between rows.
For context on my database: Object 1 which contains (aka relates to many) object2s. In turn, these object2s also relate to many object3s. A 1-to-many relationship (object 1 to object 2) and many-to-many relationship (object 2 to object 3)
Each of the objects have been assigned a UUID in other tables which contain info regarding them. Based on their UUID's I want to be able to query them and get the associated objects UUID as well. This in turn will show me the associations and direct me as to which object I should be looking at for location, info, etc just by knowing the UUID.
PLEASE NOTE - THAT ONE BOX MAY HAVE A RELTIONSHIP OF 10 SLOTS. THEREFORE THAT ONE UUID ASSIGNED FOR THE BOX WILL APPEAR IN MY UUID1 COLUMN 10 TIMES!! THIS IS A MUST!
My next step was to try and create a directionless relationship using this query:
CREATE TABLE bridge_x
(uuid1 UUID NOT NULL REFERENCES temp (uuid1), uuid2 UUID NOT NULL REFERENCES temp (uuid2),
PRIMARY KEY(uuid1, uuid2),
CONSTRAINT temp_temp_directionless
FOREIGN KEY (uuid2, uuid1)
REFERENCES bridge_x (uuid1, uuid2)
);
Is there any other way I can store ALL the information mentioned and be able to query the UUID in order to see the relationship between the objects?
You'll need a composite primary key in the bridge table. An example, using polygameous marriages:
CREATE TABLE person
(person_id INTEGER NOT NULL PRIMARY KEY
, name varchar NOT NULL
);
CREATE TABLE marriage
( person1 INTEGER NOT NULL
, person2 INTEGER NOT NULL
, comment varchar
, CONSTRAINT marriage_1 FOREIGN KEY (person1) REFERENCES person(person_id)
, CONSTRAINT marriage_2 FOREIGN KEY (person2) REFERENCES person(person_id)
, CONSTRAINT order_in_court CHECK (person1 < person2)
, CONSTRAINT polygamy_allowed UNIQUE (person1,person2)
);
INSERT INTO person(person_id,name) values (1,'Bob'),(2,'Alice'),(3,'Charles');
INSERT INTO marriage(person1,person2, comment) VALUES(1,2, 'Crypto marriage!') ; -- Ok
INSERT INTO marriage(person1,person2, comment) VALUES(2,1, 'Not twice!' ) ; -- Should fail
INSERT INTO marriage(person1,person2, comment) VALUES(3,3, 'No you dont...' ) ; -- Should fail
INSERT INTO marriage(person1,person2, comment) VALUES(2,3, 'OMG she did it again.' ) ; -- Should fail (does not)
INSERT INTO marriage(person1,person2, comment) VALUES(3,4, 'Non existant persons are not allowed to marry !' ) ; -- Should fail
SELECT p1.name, p2.name, m.comment
FROM marriage m
JOIN person p1 ON m.person1 = p1.person_id
JOIN person p2 ON m.person2 = p2.person_id
;
Result:
CREATE TABLE
CREATE TABLE
INSERT 0 3
INSERT 0 1
ERROR: new row for relation "marriage" violates check constraint "order_in_court"
DETAIL: Failing row contains (2, 1, Not twice!).
ERROR: new row for relation "marriage" violates check constraint "order_in_court"
DETAIL: Failing row contains (3, 3, No you dont...).
INSERT 0 1
ERROR: insert or update on table "marriage" violates foreign key constraint "marriage_2"
DETAIL: Key (person2)=(4) is not present in table "person".
name | name | comment
-------+---------+-----------------------
Bob | Alice | Crypto marriage!
Alice | Charles | OMG she did it again.
(2 rows)

How to return materials from a product or product levels

I have a table that contains products and each product has other products that are used to manufacture. The products that are used to manufacture the parent product can be manufactured as well and contain other manufactured products. Example:
ID_PRODUCT_MATERIAL PRODUCTION ID_MATERIAL
1 2 2
1 2 3
1 2 4
2 1 9
2 1 10
3 1 8
8 1 5
Product 1 needs products 2,3 and 4, but product 2 and 3 are also manufactured. Product 2 needs 9 and 10 and 3 needs 8 and finally 8 needs 5. In this case we have levels here, product 1 is at level 1, products 2,3 and 4 are at level 2 and product 8 is at level 3. I tried to use a hierarchy of levels with recursion to make the select, but it turns out that it is not quite a hierarchy. SELECT used below returns me the following error:
SQL error: ORA-32044: Cycle detected when executing the recursive WITH query
WITH CTE (ID_PRODUCT_MATERIAL,PRODUCTION,ID_MATERIAL) AS (
SELECT ID_PRODUCT_MATERIAL,
PRODUCTION,
ID_MATERIAL,
FROM MATERIAL
UNION ALL
SELECT ID_PRODUCT_MATERIAL,
M.PRODUCTION,
M.ID_MATERIAL,
FROM MATERIAL M
INNER JOIN CTE C
ON M.ID_PRODUCT_MATERIAL = C.ID_MATERIAL
)
SELECT * FROM CTE;
The columns ID_PRODUCT_MATERIAL and ID_MATERIAL are derived from the products table. How to proceed in this case? Here is the structure of the tables:
CREATE TABLE PRODUCT(
ID_PRODUCT NUMBER(6,0) PRIMARY KEY,
NAME VARCHAR2(200),
WEIGHT NUMBER(6,2),
PRICE NUMBER(6,2)
);
CREATE TABLE PRODUCTION(
ID_PRODUCT_PRODUCTION NUMBER(6,0) PRIMARY KEY,
PRODUCTION NUMBER(3,0),
CONSTRAINT FK_PRODUCTIONPRODUCT FOREIGN KEY(ID_PRODUCT_PRODUCTION) REFERENCES PRODUCT(ID_PRODUCT)
);
CREATE TABLE MATERIAL(
ID_PRODUCT_MATERIAL NUMBER(6,0),
PRODUCTION NUMBER(3,0),
ID_MATERIAL NUMBER(6,0),
CONSTRAINT PK_MATERIAL PRIMARY KEY(ID_PRODUCT_MATERIAL, ID_MATERIAL),
CONSTRAINT FK_PRODUCT FOREIGN KEY (ID_PRODUCT_MATERIAL) REFERENCES PRODUCT(ID_PRODUCT),
CONSTRAINT FK_PRODUCTION FOREIGN KEY (ID_PRODUCT_MATERIAL) REFERENCES PRODUCTION(ID_PRODUCT_PRODUCTION),
CONSTRAINT FK_PRODUCT2 FOREIGN KEY (ID_MATERIAL) REFERENCES PRODUCT(ID_PRODUCT)
);
INSERT INTO PRODUCT (ID_PRODUCT,NAME,WEIGHT,PRICE) VALUES (1,'PRODUCT A',10,5),
(2,'PRODUCT B',10,5),
(3,'PRODUCT C',10,5),
(4,'PRODUCT D',10,5),
(5,'PRODUCT E',10,5),
(6,'PRODUCT F',10,5),
(7,'PRODUCT G',10,5),
(8,'PRODUCT H',10,5),
(9,'PRODUCT I',10,5),
(10,'PRODUCT J',10,5)
INSERT INTO PRODUCT (ID_PRODUCT_PRODUCTION,PRODUCTION) VALUES (1,2),
(2,1),
(3,1)
INSERT INTO MATERIAL (ID_PRODUCT_MATERIAL, PRODUCTION, ID_MATERIAL) VALUES (1,2,2),
(1,2,3),
(1,2,4),
(2,1,9),
(2,1,10),
(3,1,8)
Your query is OK, but somewhere in your real data appears a loop, for instance material 1 requires 3, 3 requires 8 and 8 requires 1. This way query would produce infinite rows, so Oracle reports cycle. This loop does not exists in sample data you provided.
There are several ways you can deal with cycles, but in this case I suspect you want to correct data. You can find problematic rows, for example use pseudocolumn connect_by_is_cycle:
select m.*, connect_by_iscycle
from material m
connect by nocycle prior id_material = id_product_material
In this dbfiddle you can see the same error, cycle for material 8. I added one row to simulate this behavior.
You can also use RCTE with these clauses:
SEARCH DEPTH FIRST BY id SET order1
CYCLE id SET cycle TO 1 DEFAULT 0
as described in documentation

Multiple autoincrement ids based on table column

I need help in database design.
I have following tables.
Pseudo code:
Table order_status {
id int[pk, increment]
name varchar
}
Table order_status_update {
id int[pk, increment]
order_id int[ref: > order.id]
order_status_id int[ref: > order_status.id]
updated_at datetime
}
Table order_category {
id int[pk, increment]
name varchar
}
Table file {
id int[pk, increment]
order_id int[ref: > order.id]
key varchar
name varchar
path varchar
}
Table order {
id int [pk] // primary key
order_status_id int [ref: > order_status.id]
order_category_id int [ref: > order_category.id]
notes varchar
attributes json // no of attributes is not fixed, hence needed a json column
}
Everything was okay, but now I need an auto-increment id for each type of order_category_id column.
For example, if I have 2 categories electronics and toys , then I would need electronics-1, toy-1, toy-2, electronics-2, electronics-3, toy-3, toy-4, toy-5 values associated with rows of order table. But it's not possible as auto-increment increments based on each new row, not column type.
In other words, for table order instead of
id order_category_id
---------------------
1 1
2 1
3 1
4 2
5 1
6 2
7 1
I need following,
id order_category_id pretty_ids
----------------------------
1 1 toy-1
2 1 toy-2
3 1 toy-3
4 2 electronics-1
5 1 toy-4
6 2 electronics-2
7 1 toy-5
What I tried:
I created separate table for each order category (not an ideal solution but currently I have 6 order categories, so it works for now )
Now, I have table for electronics_order and toys_order. Columns are repetitive, but it works. But now I have another problem, my every relationship with other tables got ruined. Since, both electronics_order and toys_orders can have same id, I cannot use id column to reference order_status_update, order_status, file tables.
I can create another column order_category in each of these tables, but will it be the right way? I am not experienced in database design, so I would like to know how others do it.
I also have a side question.
Do I need tables for order_category and order_status just to store names? Because these values will not change much and I can store them in code and save in columns of order table.
I know separate tables are good for flexibility, but I had to query database 2 times to fetch order_status and order_category by name before inserting new row to order table. And later it will be multiple join for querying order table.
--
If it helps, I am using flask-sqlalchemy in backend and postgresql as database server.
In order to track the increment id which is based on the order_category, we can keep track of this value on another table. Let us call this table: order_category_sequence. To show my solution, I just created simplified version of order table with order_category.
CREATE TABLE order_category (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NULL
);
CREATE TABLE order_category_sequence (
id SERIAL PRIMARY KEY,
order_category_id int NOT NULL,
current_key int not null
);
Alter Table order_category_sequence Add Constraint "fk_order_category_id" FOREIGN KEY (order_category_id) REFERENCES order_category (id);
Alter Table order_category_sequence Add Constraint "uc_order_category_id" UNIQUE (order_category_id);
CREATE TABLE "order" (
id SERIAL PRIMARY KEY,
order_category_id int NOT NULL,
pretty_id VARCHAR(100) null
);
Alter Table "order" Add Constraint "fk_order_category_id" FOREIGN KEY (order_category_id) REFERENCES order_category (id);
The order_category_id column in order_category_sequence table refers the order_category. The current_key column holds the last value in order.
When a new order row is added, we can use a trigger to read the last value from order_category_sequence and update pretty_id. The following trigger definition can be used to achieve this.
--function called everytime a new order is added
CREATE OR REPLACE FUNCTION on_order_created()
RETURNS trigger AS
$BODY$
DECLARE
current_pretty_id varchar(100);
BEGIN
-- increment last value of the corresponding order_category_id in the sequence table
Update order_category_sequence
set current_key = (current_key + 1)
where order_category_id = NEW.order_category_id;
--prepare the pretty_id
Select
oc.name || '-' || s.current_key AS current_pretty_id
FROM order_category_sequence AS s
JOIN order_category AS oc on s.order_category_id = oc.id
WHERE s.order_category_id = NEW.order_category_id
INTO current_pretty_id;
--update order table
Update "order"
set pretty_id = current_pretty_id
where id = NEW.id;
RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER order_created
AFTER INSERT
ON "order"
FOR EACH ROW
EXECUTE PROCEDURE on_order_created();
If we want to synchronize the two table, order_category and order_category_sequence, we can use another trigger to have a row in the latter table every time a new order category is added.
//function called everytime a new order_category is added
CREATE OR REPLACE FUNCTION on_order_category_created()
RETURNS trigger AS
$BODY$
BEGIN
--insert a new row for the newly inserted order_category
Insert into order_category_sequence(order_category_id, current_key)
values (NEW.id, 0);
RETURN NEW;
END;
$BODY$ LANGUAGE plpgsql;
CREATE TRIGGER order_category_created
AFTER INSERT
ON order_category
FOR EACH ROW
EXECUTE PROCEDURE on_order_category_created();
Testing query and result:
Insert into order_category(name)
values ('electronics'),('toys');
Insert into "order"(order_category_id)
values (1),(2),(2);
select * from "order";
Regarding your side question, I prefer to store the lookup values like order_status and order_category in separate tables. Doing this allows to have the above flexibility and it is easy when we have changes.
To answer your side question: yes, you should keep tables with names in them, for a number of reasons. First of all, such tables are small and generally kept in memory by the database, so there is negligible performance benefit to not using the tables. Second, you want to be able to use external tools to query the database and generate reports, and you want these kind of labels available to those tools. Third, you want to minimize the coupling of your software to the actual data so that they can evolve independently. Adding a new category should not require modifying your software.
Now, to the main question, there is no built-in facility for the kind of auto-increment you want. You have to build it yourself.
I suggest you keep the sequence number for each category as a column in the category table. Then you can update it and use the updated sequence number in the order table, like this (which is specific to PostgreSQL):
-- set up the tables
create table orders (
id SERIAL PRIMARY KEY,
order_category_id int,
pretty_id VARCHAR
);
create unique index order_category_pretty_id_idx
on orders (pretty_id);
create table order_category (
id SERIAL PRIMARY KEY,
name varchar NOT NULL,
seq int NOT NULL default 0
);
-- create the categories
insert into order_category
(name) VALUES
('toy'), ('electronics');
-- create orders, specifying the category ID and generating the pretty ID
WITH
new_category_id (id) AS (VALUES (1)), -- 1 here is the category ID for the new order
pretty AS (
UPDATE order_category
SET seq = seq + 1
WHERE id = (SELECT id FROM new_category_id)
RETURNING *
)
INSERT into orders (order_category_id, pretty_id)
SELECT new_category_id.id, concat(pretty.name, '-', pretty.seq)
FROM new_category_id, pretty;
You just plug in your category ID where I have 1 in the example and it will create the new pretty_id for that category. The first category will be toy-1, the next toy-2, etc.
| id | order_category_id | pretty_id |
| --- | ----------------- | ------------- |
| 1 | 1 | toy-1 |
| 2 | 1 | toy-2 |
| 3 | 2 | electronics-1 |
| 4 | 1 | toy-3 |
| 5 | 2 | electronics-2 |
In order to do toys-1 toys-2 toys-3 you should repeat the logic of order_status update, There is no difference between track some status by time or by count.
Just in the order_status update it is simpler you just put now() into updated_at for lets say order_category_track you would take last value + 1 or have different sequences respectively category (would not recommend to do like this because it binds database objects with data in the DB).
I would change a schema to:
In this schema might be in inconsistent state. But in my opinion in your application there are three different entities "order","order_status","order category track" which live their own lives.
And still it is almost impossible to achieve consistent state for this task with out locks for example. This task is complicated by condition that next rows depends on previous what contradicts with SQL.
I would suggest to split category into 2-level hierarchy: category (toy, electronic) and subcategory (toy-1, toy-2, electronic-1, etc.):
So you can use column order_subcategory.full_name contain compiled "toy-1" value, or you can create view to make this field on the fly:
select oc.name || "-" || os.number
from order_category as oc
join order_subcategory as os on oc.id = os.category_id
https://dbdiagram.io/d/5dd6a132edf08a25543e34f8
Regarding your questions "Do I need tables for order_category and order_status just to store names?":
It is best practice to store this kind of data as a separate dictionary table. It gives you consistency and reliability. Querying those tables is very fast and easy for RDBMS, so feel free to use it.
I'll focus on only 3 tables you showed: order, order_status and order_category.
Creating a new table for a new record is not the right way. As your explanation, I think you trying to use order and order_category tables as many to many relationship. If it's so, the thing you need is a pivot table like this:
I currently add order_status column in order table,
you can add this column one of these tables as your need.
side question:
for order_status, if order status is fixed,( like only ACTIVE,INACTIVE and it won't be more values in the future) it would be better to use a column with ENUM type.
The easy answer would be to answer directly to your question. But I do not think it is a good thing in this case. So I will do otherwise.
I think that maybe the whole conception is wrong.
First things first : clarification of your business needs and assertions.
One order can have multiple categories
One category can concern multiple orders
One order can only have one status at a time but multiple through time
One status can be used by multiple orders
One order correspond to a file (probably a billing proof)
One file concerns only one order
Second : Advices
There is a little amount of reserved key words that you must not use in production environment. (https://www.postgresql.org/docs/current/sql-keywords-appendix.html). So for example I replace the word 'order' by 'command'.
Remaining questions that mandatory needs an answer before production : why the attributes attribute in your 'order' table? There is a risk of non respect of normal forms here. (https://www.geeksforgeeks.org/normal-forms-in-dbms/)
Third : conception solution
This normally is enough to give you a good start. But I wanna have fun a little more :) So...
Fourth : interrogation on needed performance
estimation of load per day/month in order (ten million rows per month?)
Fifth : physical solution proposition
Archiving in another tablespace (trigger when cancel or terminated => archived)
Indexes in another tablespace (your dba will thank you for that)
Possible partitionning of order table (https://pgxn.org/dist/pg_partman/doc/pg_partman.html, https://www.postgresql.org/docs/current/ddl-partitioning.html)
Hardware and option choosings (high availibility? disaster management? if it is: the elaboration needs further study but few)
Data transposition (is it really needed? if it is: the elaboration needs further study but few)
The finaaaaaal code-down ! (with the good music)
-- as a postgres user
CREATE DATABASE command_system;
CREATE SCHEMA in_prgoress_command;
CREATE SCHEMA archived_command;
--DROP SCHEMA public;
-- create tablespaces on other location than below
CREATE TABLESPACE command_indexes_tbs location 'c:/Data/indexes';
CREATE TABLESPACE archived_command_tbs location 'c:/Data/archive';
CREATE TABLESPACE in_progress_command_tbs location 'c:/Data/command';
CREATE TABLE in_prgoress_command.command
(
id bigint /*or bigserial if you use a INSERT RETURNING clause*/ primary key
, notes varchar(500)
, fileULink varchar (500)
)
TABLESPACE in_progress_command_tbs;
CREATE TABLE archived_command.command
(
id bigint /*or bigserial if you use a INSERT RETURNING clause*/ primary key
, notes varchar(500)
, fileULink varchar (500)
)
TABLESPACE archived_command_tbs;
CREATE TABLE in_prgoress_command.category
(
id int primary key
, designation varchar(45) NOT NULL
)
TABLESPACE in_progress_command_tbs;
INSERT INTO in_prgoress_command.category
VALUES (1,'Toy'), (2,'Electronic'), (3,'Leather'); --non-exaustive list
CREATE TABLE in_prgoress_command.status
(
id int primary key
, designation varchar (45) NOT NULL
)
TABLESPACE in_progress_command_tbs;
INSERT INTO in_prgoress_command.status
VALUES (1,'Shipping'), (2,'Cancel'), (3,'Terminated'), (4,'Payed'), (5,'Initialised'); --non-exaustive list
CREATE TABLE in_prgoress_command.command_category
(
id bigserial primary key
, idCategory int
, idCommand bigint
)
TABLESPACE in_progress_command_tbs;
ALTER TABLE in_prgoress_command.command_category
ADD CONSTRAINT fk_command_category_category FOREIGN KEY (idCategory) REFERENCES in_prgoress_command.category(id);
ALTER TABLE in_prgoress_command.command_category
ADD CONSTRAINT fk_command_category_command FOREIGN KEY (idCommand) REFERENCES in_prgoress_command.command(id);
CREATE INDEX idx_command_category_category ON in_prgoress_command.command_category USING BTREE (idCategory) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_category_command ON in_prgoress_command.command_category USING BTREE (idCommand) TABLESPACE command_indexes_tbs;
CREATE TABLE archived_command.command_category
(
id bigserial primary key
, idCategory int
, idCommand bigint
)
TABLESPACE archived_command_tbs;
ALTER TABLE archived_command.command_category
ADD CONSTRAINT fk_command_category_category FOREIGN KEY (idCategory) REFERENCES in_prgoress_command.category(id);
ALTER TABLE archived_command.command_category
ADD CONSTRAINT fk_command_category_command FOREIGN KEY (idCommand) REFERENCES archived_command.command(id);
CREATE INDEX idx_command_category_category ON archived_command.command_category USING BTREE (idCategory) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_category_command ON archived_command.command_category USING BTREE (idCommand) TABLESPACE command_indexes_tbs;
CREATE TABLE in_prgoress_command.command_status
(
id bigserial primary key
, idStatus int
, idCommand bigint
, change_timestamp timestamp --anticipate if you can the time-zone problematic
)
TABLESPACE in_progress_command_tbs;
ALTER TABLE in_prgoress_command.command_status
ADD CONSTRAINT fk_command_status_status FOREIGN KEY (idStatus) REFERENCES in_prgoress_command.status(id);
ALTER TABLE in_prgoress_command.command_status
ADD CONSTRAINT fk_command_status_command FOREIGN KEY (idCommand) REFERENCES in_prgoress_command.command(id);
CREATE INDEX idx_command_status_status ON in_prgoress_command.command_status USING BTREE (idStatus) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_status_command ON in_prgoress_command.command_status USING BTREE (idCommand) TABLESPACE command_indexes_tbs;
CREATE UNIQUE INDEX idxu_command_state ON in_prgoress_command.command_status USING BTREE (change_timestamp, idStatus, idCommand) TABLESPACE command_indexes_tbs;
CREATE OR REPLACE FUNCTION sp_trg_archiving_command ()
RETURNS TRIGGER
language plpgsql
as $function$
DECLARE
BEGIN
-- Copy the data
INSERT INTO archived_command.command
SELECT *
FROM in_prgoress_command.command
WHERE new.idCommand = idCommand;
INSERT INTO archived_command.command_status (idStatus, idCommand, change_timestamp)
SELECT idStatus, idCommand, change_timestamp
FROM in_prgoress_command.command_status
WHERE idCommand = new.idCommand;
INSERT INTO archived_command.command_category (idCategory, idCommand)
SELECT idCategory, idCommand
FROM in_prgoress_command.command_category
WHERE idCommand = new.idCommand;
-- Delete the data
DELETE FROM in_prgoress_command.command_status
WHERE idCommand = new.idCommand;
DELETE FROM in_prgoress_command.command_category
WHERE idCommand = new.idCommand;
DELETE FROM in_prgoress_command.command
WHERE idCommand = new.idCommand;
END;
$function$;
DROP TRIGGER IF EXISTS t_trg_archiving_command ON in_prgoress_command.command_status;
CREATE TRIGGER t_trg_archiving_command
AFTER INSERT
ON in_prgoress_command.command_status
FOR EACH ROW
WHEN (new.idstatus = 2 or new.idStatus = 3)
EXECUTE PROCEDURE sp_trg_archiving_command();
CREATE TABLE archived_command.command_status
(
id bigserial primary key
, idStatus int
, idCommand bigint
, change_timestamp timestamp --anticipate if you can the time-zone problematic
)
TABLESPACE archived_command_tbs;
ALTER TABLE archived_command.command_status
ADD CONSTRAINT fk_command_command_status FOREIGN KEY (idStatus) REFERENCES in_prgoress_command.category(id);
ALTER TABLE archived_command.command_status
ADD CONSTRAINT fk_command_command_status FOREIGN KEY (idCommand) REFERENCES archived_command.command(id);
CREATE INDEX idx_command_status_status ON archived_command.command_status USING BTREE (idStatus) TABLESPACE command_indexes_tbs;
CREATE INDEX idx_command_status_command ON archived_command.command_status USING BTREE (idCommand) TABLESPACE command_indexes_tbs;
CREATE UNIQUE INDEX idxu_command_state ON archived_command.command_status USING BTREE (change_timestamp, idStatus, idCommand) TABLESPACE command_indexes_tbs;
Conclusion:
In many cases, when you are worried by the disposition of your keys it is because they are not in the good place. Same goes for cars ones! :D
Do not take any solution as prophetic solution : benchmark it.

Is there any way to reference column with different datatype?

I have 2 schemas/tables as shown:
CREATE TABLE schema1.code_tbl
( code CHAR(6) PRIMARY KEY,
description CHAR(30)
);
CREATE TABLE schema2.record_tbl
( rec_id VARCHAR(10) PRIMARY KEY,
curr_code VARCHAR(6),
remarks VARCHAR(30)
);
I need to create a foreign key reference from curr_code in RECORD_TBL to code in CODE_TBL.
ALTER TABLE schema2.record_tbl
ADD CONSTRAINT record_code_fk
FOREIGN KEY (curr_code)
REFERENCES schema1.code_tbl (code);
This obviously gives me an ORA-02267 (column type incompatible with referenced column) error.
I cannot alter the code column in CODE_TBL because I do not own or control schema1. I cannot alter the curr_code column in RECORD_TBL because it would break many functions in my application because we don't account for trailing whitespaces.
Is there any other way to enforce referential integrity between the 2 columns?
If schema2 is on Oracle 11g and above, using virtual column may solve your problem, but that will change the structure of your table which you are trying to avoid. If you can manage it, here is how it can be done
SQL> CREATE TABLE code_tbl
2 ( code CHAR(6) PRIMARY KEY,
3 description CHAR(30)
4 );
Table created
SQL>
SQL> CREATE TABLE record_tbl
2 ( rec_id VARCHAR2(10) PRIMARY KEY,
3 curr_code VARCHAR2(6),
4 remarks VARCHAR2(30)
5 );
Table created
SQL> INSERT INTO code_tbl(code, description) VALUES ('ABC', 'Test Data');
1 row inserted
SQL> INSERT INTO record_tbl(rec_id, curr_code, remarks) VALUES ('1', 'ABC', 'Test Row');
1 row inserted
SQL> SELECT * FROM record_tbl;
REC_ID CURR_CODE REMARKS
---------- --------- ------------------------------
1 ABC Test Row
SQL> SELECT * FROM code_tbl;
CODE DESCRIPTION
------ ------------------------------
ABC Test Data
SQL> ALTER TABLE record_tbl ADD curr_code_v CHAR(6) AS (trim(curr_code));
Table altered
SQL> SELECT * FROM record_tbl;
REC_ID CURR_CODE REMARKS CURR_CODE_V
---------- --------- ------------------------------ -----------
1 ABC Test Row ABC
SQL>
SQL> ALTER TABLE record_tbl
2 ADD CONSTRAINT record_code_fk
3 FOREIGN KEY (curr_code_v)
4 REFERENCES code_tbl (CODE);
Table altered
SQL> INSERT INTO record_tbl(rec_id, curr_code, remarks) VALUES ('2', 'ABC', 'Test Row 2');
1 row inserted
SQL> INSERT INTO record_tbl(rec_id, curr_code, remarks) VALUES ('3', 'XYZ', 'Test Row 2');
INSERT INTO record_tbl(rec_id, curr_code, remarks) VALUES ('3', 'XYZ', 'Test Row 2')
ORA-02291: integrity constraint (USER_X.RECORD_CODE_FK) violated - parent key not found
Here are the words from Tom Kyte regarding virtual columns:
https://asktom.oracle.com/pls/asktom/f?p=100:11:0::::P11_QUESTION_ID:676611400346196844
So the situation is this. You have an existing table record_tbl which is well established ( obviously, because changing it "break many functions"). Belatedly somebody has decided to enforce relational integrity on this table but has chosen to do so referencing a table in a different schema with a column of a different datatype.
Hmmmm.
Your options are:
Do nothing. Always an option; your application has survived with the current state for some length of time, maybe you can continue to live with the accrued technical debt.
Refactor one of the schemas. If you need to enforce the foreign key - and let's face it, relational integrity is a good thing - then you are going to have to change the datatype of one of the columns. Which one you choose is a project decision: changing a column in a schema you don't own is a political problem (initially), and political problems are usually harder than technical problems. Refactoring a schema by changing a column type is a matter of testing, testing, testing.
Replication. Build a materialized view in schema2 which copies the data from schema1.code_tbl. Crucially, define the MView code column match the datatype of schema2.record_tbl.curr_code i.e. varchar2(6). You will now be able to enforce a foreign key against schema2.mv_code_tbl.code. Note: the data in the MView column will be formatted as CHAR i.e. with trailing spaces.
You can not create relation in different type column.
Why you must create relation in database? You can connect the table using only the code.

Can I use a PL/SQL trigger to check category and concat to a incremental number based on category?

I got a productID - P0001KTC and P0001DR.
If product category is kitchen, I will assign a productID - PROD001KTC, else if the category is dining room, then the productID should be PROD001DR.
Is it possible to write a sequence inside a trigger to check the product category and assign an id as mentioned above?
if there is another living room category product inserted then the id will be PROD001LR.
Kitchen - PROD001KTC,PROD002KTC...
Dining Room - PROD001DR,PROD002DR....
Living Room - PROD001LR,PROD002LR...
P0001KTC is the sort of smart key users love and developers hate. But the customer is king, so here we are.
The customer's requirement is to increment the numeric element within the product category, so that the same number is used for different categories: P0001KTC , P0001DR , P0002KTC, P0001LR, P0002LR, etc. A monotonically increasing sequence cannot do this.
The best implementation is a code control table, that is a table to manage the assigned numbers. Such an approach entails pessimistic locking, which serializes access to a Product Category (e.g. KTC). Presumably the users won't be creating new Products very often, so the scaling implications aren't severe.
Working PoC
Here's our reference table:
create table product_categories (
product_category_code varchar2(3) not null
, category_description varchar2(30) not null
, constraint product_categories_pk primary key (product_category_code)
)
/
create table product_ids (
product_category_code varchar2(3) not null
, last_number number(38) default 0 not null
, constraint product_ids_pk primary key (product_category_code)
, constraint product_ids_categories_fk foreign key (product_category_code)
references product_categories (product_category_code)
) organization index
/
May these two tables could be one table, but this implementation offers greater flexibility. Let's create our Product Categories:
insert all
into product_categories (product_category_code, category_description)
values (cd, descr)
into product_ids (product_category_code)
values (cd)
select * from
( select 'KTC' as cd, 'Kitchen' as descr from dual union all
select 'LR' as cd, 'Living Room' as descr from dual union all
select 'DR' as cd, 'Dining Room' as descr from dual )
/
Here's the target table:
create table products (
product_id varchar2(10) not null
, product_category_code varchar2(3) not null
, product_description varchar2(30) not null
, constraint products_pk primary key (product_id)
, constraint products_fk foreign key (product_category_code)
references product_categories (product_category_code)
)
/
This function is where the magic happens. The function formats the new Product ID. It does this by taking out a pre-emptive lock on the row for the assigned Category. These locks are retained for the length of the transaction i.e. until the locking session commits or rolls back. So if there are two users creating Kitchen Products one will be left hanging on the other: this is why we generally try to avoid serializing table access in multi-user environments.
create or replace function get_product_id
( p_category_code in product_categories.product_category_code%type)
return products.product_id%type
is
cursor lcur (p_code varchar2)is
select last_number + 1
from product_ids
where product_category_code = p_code
for update of last_number;
next_number product_ids.last_number%type;
return_value products.product_id%type;
begin
open lcur( p_category_code);
fetch lcur into next_number;
if next_number > 999 then
raise_application_error (-20000
, 'No more numbers available for ' || p_category_code);
else
return_value := 'PROD' || lpad(next_number, 3, '0') || p_category_code;
end if;
update product_ids t
set t.last_number = next_number
where current of lcur;
close lcur;
return return_value;
end get_product_id;
/
And here's the trigger:
create or replace trigger products_ins_trg
before insert on products
for each row
begin
:new.product_id := get_product_id (:new.product_category_code);
end;
/
Obviously, we could put the function code in the trigger body but it's good practice to keep business logic out of triggers.
Lastly, here's some test data...
insert into products ( product_category_code, product_description)
values ('KTC', 'Refrigerator')
/
insert into products ( product_category_code, product_description)
values ('DR', 'Dining table')
/
insert into products ( product_category_code, product_description)
values ('KTC', 'Microwave oven')
/
insert into products ( product_category_code, product_description)
values ('DR', 'Dining chair')
/
insert into products ( product_category_code, product_description)
values ('DR', 'Hostess trolley')
/
insert into products ( product_category_code, product_description)
values ('LR', 'Sofa')
/
And, lo!
SQL> select * from products
2 /
PRODUCT_ID PRO PRODUCT_DESCRIPTION
---------- --- ------------------------------
PROD001KTC KTC Refrigerator
PROD001DR DR Dining table
PROD002KTC KTC Microwave oven
PROD002DR DR Dining chair
PROD003DR DR Hostess trolley
PROD001LR LR Sofa
6 rows selected.
SQL>
Note that modelling the smart key as a single column is a bad idea. It is better to build it as a composite key, say unique (product_category, product_number), where product_number is generated from the code control table above. We still need the product_id for display purposes, but it should be derived from the underlying columns. This is easy using virtual columns, like this:
create table products (
product_id varchar2(10)
generated always as 'PROD' || to_char(product_no,'FM003') || product_category_code;
, product_category_code varchar2(3) not null
, product_no number not null
, product_description varchar2(30) not null
, constraint products_pk primary key (product_id)
, constraint products_uk unique (product_category_code, product_no)
, constraint products_fk foreign key (product_category_code)
references product_categories (product_category_code)
)
/
The image shows a different format for productid than what you write in the question. I will assume you want the "PROD" prefix as in the image, and that you can deal with changing those characters in the solution below, if needed.
Also, you write twice the same number (001) in the question, yet in the image, and in the comments you provided, you indicate the numbering should increment always. So this solution will have an always incrementing number.
Proposed Solution
You should store the incremental number separately, and have that as the real id.
The formatted productID could then be a derived column. Since Oracle 11g R1 you can create virtual columns in a table, so you don't really need a trigger for that:
Here is an example script, which creates the table and the sequence:
create table products (
id number not null,
category varchar2(100),
productid as (
'PROD'||
to_char(id, 'FM000') ||
case category when 'Kitchen' then 'KTC'
when 'LivingRoom' then 'LR'
else '???'
end ) virtual,
constraint pk_product_id primary key (id),
);
-- create sequence for inserting incremental id value
create sequence product_seq start with 1 increment by 1;
You insert data like this, without specifying values for the virtual productid column:
-- Insert data
insert into products (id, category) values (product_seq.nextval, 'Kitchen');
insert into products (id, category) values (product_seq.nextval, 'LivingRoom');
And when you select data from the table:
select * from products
You get:
ID | CATEGORY | PRODUCTID
---+------------+-----------
1 | Kitchen | PROD001KTC
2 | LivingRoom | PROD002LR
Note that you'll get into trouble if your id surpasses 999, as then the 3-digit format will not work any more. Oracle will then generate ### for the to_char result, so you'll run into duplicate productid values soon.
If you have many more categories than those two (Kitchen & LivingRoom), then you should not extend the earlier mentioned case statement with those values. Instead you should create a reference table for it (let's call it categories), with values like this:
Code | Name
-----+---------------
KTC | Kitchen
LR | Living Room
... | ...
Once you have that table, where Code should be unique, you can just store the code in the products table, not the description:
create table products (
id number not null,
category_code varchar2(10),
productid as (
'PROD'||
to_char(id, 'FM000') ||
category_code) virtual,
constraint pk_product_id primary key (id),
constraint fk_product_category foreign key (category_code)
references catgories(code)
);
You would insert values like this:
insert into products (id, category_code) values (product_seq.nextval, 'KTC');
insert into products (id, category_code) values (product_seq.nextval, 'LR');
And when you want to select data from the table with the category names included:
select product.productid, categories.name
from products
inner join categories on product.category_code = categories.code