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

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

Related

How to select from table A and then insert selected id inside table B with one query?

I'm trying to implement a very basic banking system.
the goal is to have different types of transactions ( deposit, withdraw, transfer ) inside a table and refer to them as IDs inside transaction tables.
CREATE TABLE transaction_types (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
name VARCHAR UNIQUE NOT NULL
)
CREATE TABLE transactions (
id INTEGER AUTO_INCREMENT PRIMARY KEY,
type_id INTEGER NOT NULL,
amount FLOAT NOT NULL
)
What I'm trying to accomplish is:
When inserting into transactions table no record can have an invalid type_id ( type_id should exist in transaction_types table )
First of all get type_id from transaction_types table and then insert inside transactions table, with one query ( if it's possible, I'm fairly new )
I'm using Node.js/Typescript and PostgreSQL, any help is appreciated a lot.
For (1): modify Transactions table definition by adding REFERENCES transaction_types(id) to the end of the type_id column definition prior to the comma.
For (2), assuming you know the name of the transaction_type, you can accomplish this by:
INSERT INTO transactions(type_id, amount)
VALUES ((SELECT id from transaction_types WHERE name = 'Withdrawal'), 999.99)
By the way, my PostgreSQL requires SERIAL instead of INTEGER AUTOINCREMENT

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.

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

ORACLE SQL - Creating a disjoint specialisation

The following is what I am expecting from my tables. An item can be disjointed into a 'Computer' or 'Clothes'.
Computer has attributes 'computer_type', & 'operating_system'.
Clothes has only 'clothes_type'. As I want these as sub types they'll both take the 'item_id' as a foreign key.
My plan is to have these two sub tables derived from the parent item, and insert (the imported data that I am working on) into the appropriate tables. The 'item' table stores all the 'items' but the sub tables will reference a given item ID and be populated.
Table: Item (item_id,)
table: computer (sub type) with its own attributes
table: clothes (sub type) with its own attributes.
I saw another post here:
How to write Tables in SQL with a Disjoint Connection
So I wondered (as my approach is similar to his) whether I should create the tables as they are with a foreign key within both of my disjoined tables, and use a 'select' accordingly. Or is there a way without having seperate tables to store individual attributes.
"Or is there a way without having seperate tables to store individual attributes."
There is no good way to avoid having separate tables. Bad ways include using a so-called generic data model (the entity-attribute-value pattern) or some form of quasi-structured storage such as JSON or XML.
These approaches are bad for several reasons.
It is very hard to understand the data model. The model is
embedded how the data is stored rather than being expressed in
tables and keys.
We cannot enforce keys and data integrity constraints on such
models.
It is harder to write queries against such "models" because SQL is a strongly-typed language suited to static data models. The time saved by not doing proper data modelling is lost in writing the convoluted queries needed to handle the poor quality data (because of the previous point).
A robust solution would enforce the arc across the subtype tables with the full range of keys.
create table items
(item_id number not null primary key
, item_type varchar2(10) not null
, item_name varchar2(30) not null
, constraint item_uk unique (item_id, item_type)
, constraint item_type_ck check (item_type in ('CLOTHES', 'COMPUTER'))
);
Two keys? Why two keys? So we can enforce the one-to-one relationship between parent and child records:
create table clothes
(item_id number not null primary key
, item_type varchar2(10) not null
, size not null varchar2(5)
, colour not null varchar2(5)
, category not null varchar2(5)
, constraint clothes_type_ck check (item_type = 'CLOTHES')
, constraint clothes_item_fk foreign key (item_id, item_type)
references items (item_id, item_type)
);
create table computers
(item_id number not null primary key
, item_type varchar2(10) not null
, ram_gb not null number
, storage_gb not null number
, os not null varchar2(10)
, constraint computers_type_ck check (item_type = 'COMPUTER')
, constraint computers_item_fk foreign key (item_id, item_type)
references items (item_id, item_type)
);
The primary key on ITEMS ensures we only have one record for a given item_id. The unique key provides a reference point for the child tables, so we cannot have a COMPUTERS records and a CLOTHES record pointing to the same ITEMS record.
You can try and do this with 3 tables, a CHECK constraint (for the "supertype" table), and foreign key constraints. However, a good alternative may be the object-relational approach: create 3 TYPEs, and only one table that can hold all the data.
Example (Oracle 12c)
-- supertype
create or replace type item_t as object (
name varchar2(64)
)
not final; -- subtypes can be derived
/
-- subtypes
create or replace type computer_t under item_t (
operating_system varchar2(128)
)
/
create or replace type clothes_t under item_t (
description varchar2(128)
)
/
-- Table that handles all 3 types. No foreign key constraints or CHECKS needed.
create table items (
id number generated always as identity primary key
, item item_t
);
Testing: INSERTs
begin
-- "standard" INSERTs
insert into items ( item )
values ( computer_t( 'iMac', 'Mac OS' ) );
insert into items ( item )
values ( clothes_t( 'coat', 'black' ) );
-- object of SUPERtype can be inserted
insert into items ( item )
values ( item_t( 'supertype' ) );
end;
/
-- Unknown types cannot be inserted. Good.
insert into items ( item )
values ( unknown_t( 'type unknown!', 'not available', 999 ) );
-- ORA-00904: "UNKNOWN_T": invalid identifier
insert into items ( item )
values ( item_t( 'supertype', 'not available') );
-- ORA-02315: incorrect number of arguments for default constructor
For queries, use the appropriate functions. A simple SELECT * FROM items will probably not give you the results you need eg
SQL> select * from items;
ID ITEM
1 oracle.sql.STRUCT#2d901eb0
2 oracle.sql.STRUCT#3ba987b8
3 oracle.sql.STRUCT#3f191845
Query
select
id
, treat( item as computer_t ).name as computer_name
, treat( item as clothes_t ).name as garment_name
, treat( item as item_t ).name as item_name
, case
when treat( item as item_t ) is not null
then 'ITEM_T'
when treat( item as computer_t ) is not null -- "is an" item
then 'COMPUTER_T'
when treat( item as clothes_t ) is not null -- "is an" item
then 'CLOTHES_T'
else
'TYPE unknown :-|'
end type_
, case
when treat( item as computer_t ) is not null
then 'COMPUTER_T'
when treat( item as clothes_t ) is not null
then 'CLOTHES_T'
when treat( item as item_t ) is not null
then 'ITEM_T'
else
'TYPE unknown :-|'
end type_
from items ;
-- result
ID COMPUTER_NAME GARMENT_NAME ITEM_NAME TYPE_ TYPE_
1 iMac NULL iMac ITEM_T COMPUTER_T
2 NULL coat coat ITEM_T CLOTHES_T
3 NULL NULL supertype ITEM_T ITEM_T

How can I insert into tables with relations?

I have only done databases without relations, but now I need to do something more serious and correct.
Here is my database design:
Kunde = Customer
Vare = Product
Ordre = Order (Read: I want to make an order)
VareGruppe = ehm..type? (Read: Car, chair, closet etc.)
VareOrdre = Product_Orders
Here is my SQL (SQLite) schema:
CREATE TABLE Post (
Postnr INTEGER NOT NULL PRIMARY KEY,
Bynavn VARCHAR(50) NOT NULL
);
CREATE TABLE Kunde (
CPR INTEGER NOT NULL PRIMARY KEY,
Navn VARCHAR(50) NOT NULL,
Tlf INTEGER NOT NULL,
Adresse VARCHAR(50) NOT NULL,
Postnr INTEGER NOT NULL
CONSTRAINT fk_postnr_post REFERENCES Post(Postnr)
);
CREATE TABLE Varegruppe (
VGnr INTEGER PRIMARY KEY,
Typenavn VARCHAR(50) NOT NULL
);
CREATE TABLE Vare (
Vnr INTEGER PRIMARY KEY,
Navn VARCHAR(50) NOT NULL,
Pris DEC NOT NULL,
Beholdning INTEGER NOT NULL,
VGnr INTEGER NOT NULL
CONSTRAINT fk_varegruppevgnr_vgnr REFERENCES Varegruppe(VGnr)
);
CREATE TABLE Ordre (
Onr INTEGER PRIMARY KEY,
CPR INTEGER NOT NULL
CONSTRAINT fk_kundecpr_cpr REFERENCES Kunde(CPR),
Dato DATETIME NOT NULL,
SamletPris DEC NOT NULL
);
CREATE TABLE VareOrdre (
VareOrdreID INTEGER PRIMARY KEY,
Onr INTEGER NOT NULL
CONSTRAINT fk_ordrenr_onr REFERENCES Ordre(Onr),
Vnr INTEGER NOT NULL
CONSTRAINT fk_varevnr_vnr REFERENCES Vare(Vnr),
Antal INTEGER NOT NULL
);
It should work correctly.
But I am confused about Product_Orders.
How do I create an order? For example, 2 products using SQL INSERT INTO?
I can get nothing to work.
So far:
Only when I manually insert products and data into Product_Orders and then add that data to Orders = which makes it complete. Or the other way around (create an order in with 1 SQL, then manually inserting products into Product_orders - 1 SQL for each entry)
You should first create an order and then insert products in the table Product_Orders. This is necessary because you need an actual order with an id to associate it with the table Product_Orders.
You always should create a record in the foreign-key table before being able to create one in your current table. That way you should create a "Post", customer, type, product, order and product_order.
Try this ...
first you have to insert a customer
insert into kunde values(1, 'navn', 1, 'adresse', 1)
then you insert a type
insert into VareGruppe values(1, 'Type1')
then you insert a product
insert into vare values(1, 'product1', '10.0', 1, 1)
then you add an order
insert into ordre values(1, 1, '20090101', '10.0')
then you insert a register to the product_orders table
insert into VareOrdre values (1, 1, 1, 1)
I think this is it. :-)
As the primary keys are autoincrement, don't add them to the insert and specify the columns like this
insert into vare(Nav, Pris, Beholdning, VGnr) values('product1', '10.0', 1, 1)
Use Select ##identity to see the onr value
I think you already have the hang of what needs to happen. But what I think you are getting at is how to ensure data integrity.
This is where Transactions become important.
http://www.sqlteam.com/article/introduction-to-transactions
Is it the SalesPrice (I'm guessing that's what SamletPris means) that's causing the issue? I can see that being a problem here. One common design solution is to have 2 tables: Order and OrderLine. The Order is a header table - it will have the foreign key relationship to the Customer table, and any other 'top level' data. The OrderLine table has FK relationships to the Order table and to the Product table, along with quantity, unit price, etc. that are unique to an order's line item. Now, to get the sales price for an order, you sum the (unit price * quantity) of the OrderLine table for that order. Storing the SalesPrice for a whole order is likely to cause big issues down the line.
A note just in case this is MySQL: If you're using MyISAM, the MySQL server ignores the foreign keys completely. You have to set the engine to InnoDB if you want any kind of integrity actually enforced on the database end instead of just in your logic. This isn't your question but it is something to be aware of.
fbinder got the question right :)