I am an absolute beginner when it comes to SQL, and I'm trying to figure out how to do a simple integrity check. I am modelling a hotel registration system with four tables: Hotel, Room, Booking and Guest. What I'm interested in is in the booking table, which has attributes hotelNo, guestNo, dateFrom, dateTo, roomNo, where the first three are a composite primary key. Now then, the issue at hand is that under the current system, two people might have the same room booked at the same time, which would obviously be a problem in real life. I imagine that a solution might start to look something like
CREATE TABLE Booking(
-- All the attribute definitions go here...
CONSTRAINT OneGuestAtATime
CHECK (NOT EXISTS(SELECT(dateFrom FROM Booking ...))) -- I become unsure of what to do around here
);
Bear in mind that while I am a computer engineering student, I've never done SQL before, so a hand-holdy walk-through would be appreciated :)
Edit: I think my problem would be solved by a constraint to the effect of
CASE 1 (Overlap): "If the dateFrom or dateTo attributes of the record I am trying to insert falls between the dateFrom and dateTo attributes of a given record previously in the table, reject this insertion since there is some overlap between the two bookings."
CASE 2 (Superset): "If I am attempting to insert record X and there is a record already in the table named Y such that its Y.dateFrom > X.dateFrom and Y.dateTo < X.dateTo, then X is a superset of Y and should be rejected."
I'm not sure of how to translate that to SQL, though.
Edit 2: The tables
CREATE TABLE Hotel (
hotelNo NUMBER NOT NULL,
hotelName VARCHAR2(1024) NOT NULL,
city VARCHAR2(1024) NOT NULL,
--
PRIMARY KEY (hotelNo)
);
CREATE TABLE Room (
roomNo NUMBER(4,0) NOT NULL,
hotelNo NUMBER(5,0) NOT NULL,
type VARCHAR2(1024),
price NUMBER(6,2) NOT NULL,
--
PRIMARY KEY (roomNo, hotelNo),
FOREIGN KEY (hotelNo) REFERENCES Hotel
);
CREATE TABLE Guest(
guestNo NUMBER(8,0) NOT NULL,
guestName VARCHAR(1024) NOT NULL,
guestAddress VARCHAR(1024) NOT NULL,
--
PRIMARY KEY (guestNo)
);
CREATE TABLE Booking(
hotelNo NUMBER(8,0) NOT NULL,
guestNo NUMBER(8,0) NOT NULL,
dateFrom DATE NOT NULL,
dateTo DATE NOT NULL,
roomNo NUMBER(4,0) NOT NULL,
--
PRIMARY KEY (hotelNo, guestNo, dateFrom),
FOREIGN KEY (hotelNo) REFERENCES Hotel,
FOREIGN KEY (guestNo) REFERENCES Guest,
FOREIGN KEY (hotelNo, roomNo) REFERENCES Room(hotelNo, roomNo),
--
CONSTRAINT DateIntegrity
CHECK (dateFrom < dateTo)
In your model, the HOTEL and GUEST tables are okay (they may need some more columns later, but that's not a problem). For ROOM, you have decided to use a composite PK. However, a single column as ID would suffice. In BOOKING, the foreign key referencing HOTEL is redundant. Guests are booking rooms (which have a unique ID, and are tied to a HOTEL already) at certain days.
It may help (your learning) to auto-generate the IDs, and define different "start" values for them - when querying the tables at a later stage, you will recognise the IDs immediately eg HOTELs
could have 1000+, rooms could have 2000+ etc (see DDL code below).
When following the link provided by #Abra, you have seen that triggers etc can be used for solving the problem. The solution below is inspired by this answer (also mentioned as "option 4" here), and uses the idea of breaking down bookings into days ("slots"), which can then be used for unique (or PK) constraints. Please read the comments, as they contain more explanations.
DDL code
create table hotels (
id number generated always as identity start with 1000 primary key
, name_ varchar2( 100 )
) ;
create table rooms (
id number generated always as identity start with 2000 primary key
, name_ varchar2( 100 )
, hotelid number references hotels( id )
) ;
create table guests (
id number generated always as identity start with 3000 primary key
, last_name varchar2( 100 )
) ;
-- additional table, populated 500 days "into the future"
-- (no bookings _before_ the sysdate) due to FK constraint in bookings
create table days ( slot primary key )
as
select trunc( sysdate ) + level
from dual
connect by level <= 500 ; -- Oracle only!
create table bookings (
roomid number references rooms( id )
, slot date references days( slot ) not null
, guestid number references guests( id ) not null
, constraint bookings_pk primary key( roomid, slot )
) ;
Model
Populate HOTELS, ROOMS, GUESTS
-- For populating HOTELS, ROOMS, and GUESTS, we are just using a little PL/SQL script.
-- You can also use single INSERTs.
begin
-- insert one hotel
insert into hotels ( name_ ) values ( 'Tiny Hotel' ) ;
-- insert 8 rooms
for r in 1 .. 8
loop
insert into rooms( name_, hotelid ) values ( 'room_' || to_char( r ), 1000 ) ;
end loop ;
-- insert 9 guests
for g in 1 .. 9
loop
insert into guests( last_name ) values ( 'guest_' || to_char( g ) ) ;
end loop ;
commit ;
end ;
/
Data in HOTELS, ROOMS, GUESTS
SQL> select * from hotels ;
ID NAME_
1000 Tiny Hotel
SQL> select * from rooms ;
ID NAME_ HOTELID
2000 room_1 1000
2001 room_2 1000
2002 room_3 1000
2003 room_4 1000
2004 room_5 1000
2005 room_6 1000
2006 room_7 1000
2007 room_8 1000
SQL> select * from guests ;
ID LAST_NAME
3000 guest_1
3001 guest_2
3002 guest_3
3003 guest_4
3004 guest_5
3005 guest_6
3006 guest_7
3007 guest_8
3008 guest_9
Testing
-- tests for bookings - unique (roomid, slot)
-- guest 3000 books room 2000, 2 days
-- these 2 inserts must succeed
insert into bookings ( roomid, guestid, slot )
values ( 2000, 3000, date '2020-10-10' ) ;
insert into bookings ( roomid, guestid, slot )
values ( 2000, 3000, date '2020-10-10' + 1 ) ; -- + 1 here could be + i in a loop ...
-- INSERT must fail - guest 3000 cannot book room 2000 twice (on the same day)
insert into bookings ( roomid, guestid, slot )
values ( 2000, 3000, date '2020-10-10' ) ;
--ERROR at line 1:
--ORA-00001: unique constraint (...BOOKINGS_PK) violated
-- this INSERT must fail
-- guest 3001 cannot have room 2000 on the same day as guest 3000
insert into bookings ( roomid, guestid, slot )
values ( 2000, 3001, date '2020-10-10' + 1 ) ;
--ERROR at line 1:
--ORA-00001: unique constraint (...BOOKINGS_PK) violated
-- guest 3001 can have a different room at the same date, though
-- this insert must succeed
insert into bookings ( roomid, guestid, slot )
values ( 2001, 3001, date '2020-10-10' + 1 ) ;
-- 1 row created.
You can insert more dates for testing by using PL/SQL and coding a loop: see dbfiddle.
Simple query
-- all current bookings
select
H.name_
, R.name_
, G.last_name
, B.slot
from hotels H
join rooms R on H.id = R.hotelid
join bookings B on R.id = B.roomid
join guests G on G.id = B.guestid
;
-- result
NAME_ NAME_ LAST_NAME SLOT
Tiny Hotel room_1 guest_1 10-OCT-20
Tiny Hotel room_1 guest_1 11-OCT-20
Tiny Hotel room_1 guest_4 01-DEC-20
Tiny Hotel room_1 guest_4 02-DEC-20
Tiny Hotel room_1 guest_4 03-DEC-20
...
Query with more test data (INSERTs: see dbfiddle)
-- query that returns
-- all current bookings with nights_booked etc
-- CAUTION: for recurring bookings (same ROOM and GUEST but different slots)
-- this query will give us misleading results
select
H.name_
, R.name_
, G.last_name
, count( B.slot) nights_booked
, min( B.slot ) arrival_date
, max( B.slot ) + 1 departure_date
from hotels H
join rooms R on H.id = R.hotelid
join bookings B on R.id = B.roomid
join guests G on G.id = B.guestid
group by H.name_, R.name_, G.last_name
;
-- result
NAME_ NAME_ LAST_NAME NIGHTS_BOOKED ARRIVAL_DATE DEPARTURE_DATE
Tiny Hotel room_1 guest_1 2 10-OCT-20 12-OCT-20
Tiny Hotel room_1 guest_4 21 01-DEC-20 22-DEC-20
Tiny Hotel room_2 guest_2 1 11-OCT-20 12-OCT-20
As your question was more about modelling and constraints, the queries will probably need more work.
Related
Oracle 19c
We have a requirement where we want to provide some report data from Event table, Event and Item table has schema like this
CREATE TABLE EVENT
(
"ID" NUMBER(19,0) NOT NULL ENABLE,
"CREATED" TIMESTAMP (6) NOT NULL,
"CUSTOMER_ID" VARCHAR2(255 CHAR) NOT NULL,
"CONF_ID" VARCHAR2(255 CHAR),
"STATE" VARCHAR2(255 CHAR) NOT NULL,
"ITEM_ID" VARCHAR2(255 CHAR) NOT NULL
...
)
CREATE TABLE ITEM
(
"ID" NUMBER(19,0) NOT NULL ENABLE,
"NAME" VARCHAR2(255 CHAR) NOT NULL
....
primary key (ID)
)
alter table EVENT
add constraint EVENT_FK_ITEM_BID
foreign key (ITEM_ID)
references ITEM;
where events are created with different states as per real time occurrance. Events are bound to Item Table with Item_id.
What we want to achieve select count of Event States (only consider the latest state per item_id), grouped by CUSTOMER_ID and CONF_ID.
Event table could have more than 2 million rows.
result should look like
CUSTOMER_ID CONF_ID ACIVATED DEACTIVATED SUSPENDED
---------- ------- -------- ------------- ---------
1 2 50000 20000 5000
1 1 70000 30000 2000
2 1 80000 10000 10000
2 2 50000 20000 5000
Could you please guide us building an efficient query?
You can find the latest rows using the RANK analytic function (or the ROW_NUMBER analytic function if there will only ever be one latest row for each customer_id, conf_id, item_id tuple) and then PIVOT:
SELECT *
FROM (
SELECT customer_id,
conf_id,
state
FROM (
SELECT customer_id,
conf_id,
state,
RANK() OVER (PARTITION BY customer_id, conf_id, item_id ORDER BY created DESC) AS rnk
FROM event
WHERE state IN ('ACTIVATED', 'SUSPENDED', 'DEACTIVATED')
)
WHERE rnk = 1
)
PIVOT (
COUNT(*)
FOR state IN (
'ACTIVATED' AS activated,
'SUSPENDED' AS suspended,
'DEACTIVATED' AS deactivated
)
);
fiddle
I will create table where I will insert multiple values for different companies. Basically I have all values that are in the table below but I want to add a column IndicatorID which is linked to IndicatorName so that every indicator has a unique id. This will obviously not be a PrimaryKey.
I will insert the data with multiple selects:
CREATE TABLE abc
INSERT INTO abc
SELECT company_id, 'roe', roevalue, metricdate
FROM TABLE1
INSERT INTO abc
SELECT company_id, 'd/e', devalue, metricdate
FROM TABLE1
So, I don't know how to add the IndicatorID I mentioned above.
EDIT:
Here is how I populate my new table:
INSERT INTO table(IndicatorID, Indicator, Company, Value, Date)
SELECT [the ID that I need], 'NI_3y' as 'Indicator', t.Company, avg(t.ni) over (partition by t.Company order by t.reportdate rows between 2 preceding and current row) as 'ni_3y',
t.reportdate
FROM table t
LEFT JOIN IndicatorIDs i
ON i.Indicator = roe3 -- the part that is not working if I have separate indicatorID table
I am going to insert different indicators for the same companies. And I want indicatorID.
Your "indicator" is a proper entity in its own right. Create a table with all indicators:
create table indicators (
indicator_id int identity(1, 1) primary key,
indicator varchar(255)
);
Then, use the id only in this table. You can look up the value in the reference table.
Your inserts are then a little more complicated:
INSERT INTO indicators (indicator)
SELECT DISTINCT roevalue
FROM table1 t1
WHERE NOT EXISTS (SELECT 1 FROM indicators i2 WHERE i2.indicator = t1.roevalue);
Then:
INSERT INTO ABC (indicatorId, companyid, value, date)
SELECT i.indicatorId, t1.company, v.value, t1.metricdate
FROM table1 t1 CROSS APPLY
(VALUES ('roe', t1.roevalue), ('d/e', t1.devalue)
) v(indicator, value) JOIN
indicators i
ON i.indicator = v.indicator;
This process is called normalization and it is the typical way to store data in a database.
DDL and INSERT statement to create an indicators table with a unique constraint on indicator. Because the ind_id is intended to be a foreign key in the abc table it's created as a non-decomposable surrogate integer primary key using the IDENTITY property.
drop table if exists test_indicators;
go
create table test_indicators (
ind_id int identity(1, 1) primary key not null,
indicator varchar(20) unique not null);
go
insert into test_indicators(indicator) values
('NI'),
('ROE'),
('D/E');
The abc table depends on the ind_id column from indicators table as a foreign key reference. To populate the abc table company_id's are associated with ind_id's.
drop table if exists test_abc
go
create table test_abc(
a_id int identity(1, 1) primary key not null,
ind_id int not null references test_indicators(ind_id),
company_id int not null,
val varchar(20) null);
go
insert into test_abc(ind_id, company_id)
select ind_id, 102 from test_indicators where indicator='NI'
union all
select ind_id, 103 from test_indicators where indicator='ROE'
union all
select ind_id, 104 from test_indicators where indicator='D/E'
union all
select ind_id, 103 from test_indicators where indicator='NI'
union all
select ind_id, 105 from test_indicators where indicator='ROE'
union all
select ind_id, 102 from test_indicators where indicator='NI';
Query to get result
select i.ind_id, a.company_id, i.indicator, a.val
from test_abc a
join test_indicators i on a.ind_id=i.ind_id;
Output
ind_id company_id indicator val
1 102 NI NULL
2 103 ROE NULL
3 104 D/E NULL
1 103 NI NULL
2 105 ROE NULL
1 102 NI NULL
I was finally able to find the solution for my problem which seems to me very simple, although it took time and asking different people about it.
First I create my indicators table where I assign primary key for all indicators I have:
CREATE TABLE indicators (
indicator_id int identity(1, 1) primary key,
indicator varchar(255)
);
Then I populate easy without using any JOINs or CROSS APPLY. I don't know if this is optimal but it seems as the simplest choice:
INSERT INTO table(IndicatorID, Indicator, Company, Value, Date)
SELECT
(SELECT indicator_id from indicators i where i.indicator = 'NI_3y) as IndicatorID,
'NI_3y' as 'Indicator',
Company,
avg(ni) over (partition by Company order by reportdate rows between 2 preceding and current row) as ni_3y,
reportdate
FROM TABLE1
I would like to generate a list of all days where every sailor booked a boat in that particular day.
The table scheme is as follows:
CREATE TABLE SAILOR(
SID INTEGER NOT NULL,
NAME VARCHAR(50) NOT NULL,
RATING INTEGER NOT NULL,
AGE FLOAT NOT NULL,
PRIMARY KEY(SID)
);
CREATE TABLE BOAT(
BID INTEGER NOT NULL,
NAME VARCHAR(50) NOT NULL,
COLOR VARCHAR(50) NOT NULL,
PRIMARY KEY(BID)
);
CREATE TABLE RESERVE (
SID INTEGER NOT NULL REFERENCES SAILOR(SID),
BID INTEGER NOT NULL REFERENCES BOAT(BID),
DAY DATE NOT NULL,
PRIMARY KEY(SID, BID, DAY));
The data is as follows:
INSERT INTO SAILOR(SID, NAME, RATING, AGE)
VALUES
(64, 'Horatio', 7, 35.0),
(74, 'Horatio', 9, 35.0);
INSERT INTO BOAT(BID, NAME, COLOR)
VALUES
(101, 'Interlake', 'blue'),
(102, 'Interlake', 'red'),
(103, 'Clipper', 'green'),
(104, 'Marine', 'red');
INSERT INTO RESERVE(SID, BID, DAY)
VALUES+
(64, 101, '09/05/98'),
(64, 102, '09/08/98'),
(74, 103, '09/08/98');
I have tried using this code:
SELECT DAY
FROM RESERVE R
WHERE NOT EXISTS (
SELECT SID
FROM SAILOR S
EXCEPT
SELECT S.SID
FROM SAILOR S, RESERVE R
WHERE S.SID = R.SID)
GROUP BY DAY;
but it returns a list of all days, no exception. The only day that it should return is "09/08/98". How do I solve this?
I would phrase your query as:
SELECT r.DAY
FROM RESERVE r
GROUP BY r.DAY
HAVING COUNT(DISTINCT r.SID) = (SELECT COUNT(*) FROM SAILOR);
Demo
The above query says to return any day in the RESERVE table whose distinct SID sailor count matches the count of every sailor.
This assumes that SID sailor entries in the RESERVE table would only be made with sailors that actually appear in the SAILOR table. This seems reasonable, and can be enforced using primary/foreign key relationships between the two tables.
Taking a slightly different approach of just counting unique sailors per day:
SELECT day FROM (
SELECT COUNT(DISTINCT sid), day FROM reserve GROUP BY day
) AS sailors_per_day
WHERE count = (SELECT COUNT(*) FROM sailor);
+------------+
| day |
|------------|
| 1998-09-08 |
+------------+
Having some tables where I store information about a patient and type of tests (there are 3 types of tests, those I store in other table) patient were exposed I got:
CREATE TABLE IF NOT EXISTS patients (
ID INTEGER PRIMARY KEY AUTOINCREMENT,
Name VARCHAR(50) NOT NULL,
Tel VARCHAR(35) NOT NULL,
Email VARCHAR(35) NOT NULL
);
CREATE TABLE IF NOT EXISTS tests (
id_test INTEGER PRIMARY KEY AUTOINCREMENT,
id_patient INTEGER,
id_type_test INTEGER,
id_graph INTEGER,
id_graph2 INTEGER,
date_test VARCHAR(10),
FOREIGN KEY (id_patient) REFERENCES patients(id_patient) ON DELETE CASCADE,
FOREIGN KEY (id_type_test) REFERENCES type_tests(id_type_test) );
CREATE TABLE IF NOT EXISTS type_tests (
id_type_test INTEGER PRIMARY KEY AUTOINCREMENT,
name_test VARCHAR(6) NOT NULL
);
INSERT INTO type_tests values (1,'Audi');
INSERT INTO type_tests values (2,'Log');
INSERT INTO type_tests values (3,'na');
INSERT INTO patients values (1,'John','5653346','john#foe.com');
INSERT INTO patients values (2,'Karla','234346','karla#foe.com');
INSERT INTO patients values (3,'Chad','6667653346','chad#foe.com');
INSERT INTO tests values (1,1,1,50,160,'02/02/2010');
INSERT INTO tests values (2,1,3,55,165,'12/05/2010');
INSERT INTO tests values (3,1,3,65,170,'03/12/2010');
I would like to retrieve all patient information of all patients plus their studies ordered by most recent date like:
ID Name Tel Email Date Test graph graph2
To solve that first I tried getting all studies of one patient like:
SELECT te.name_test AS Test,
e.date_test AS Date,
e.id_graph AS id_graph,
e.id_graph2 AS id_graph2
FROM tests e
INNER JOIN type_tests te
ON e.id_type_test = te.id_type_test
WHERE e.id_patient=1
ORDER BY
e.date_test DESC;
I got:
Test Date id_graph id_graph2
na 12/05/2010 55 165
na 03/12/2010 65 170
Audi 02/02/2010 50 160
How can I get all patient information of all patients?
Here is a sqlfiddle
desired output would be
ID Name Tel Email Test Date id_graph id_graph2
1 John 5653346 john#foe.com na 12/05/2010 55 165
1 John 5653346 john#foe.com na 03/12/2010 65 170
1 John 5653346 john#foe.com Audi 02/02/2010 50 160
If there were more patients I would like to show all patients...
Simply join the patients table:
SELECT p.ID,
p.Name,
p.Tel,
p.Email,
te.name_test AS Test,
e.date_test AS Date,
e.id_graph AS id_graph,
e.id_graph2 AS id_graph2
FROM tests e
INNER JOIN type_tests te
ON e.id_type_test = te.id_type_test
INNER JOIN patients p
ON p.ID = e.id_patient
WHERE e.id_patient=1
ORDER BY
e.date_test DESC;
SQL Fiddle
We returned a list of cardID's after a query and those cardID's belong to two tables Student and Personnel. So how can I join those cardID's with Student and Personnel so I can return a table that shows name of Student and Personnel according to cardID's?
Personnel table:
PERSONNELID NUMBER(9,0)
PERSONNELNAME VARCHAR2(20)
PERSONNELSURNAME VARCHAR2(20)
PERSONNELJOB VARCHAR2(40)
PERSONNELCARDID NUMBER(4,0)
Student table:
STUDENTID NUMBER(9,0)
STUDENTNAME VARCHAR2(20)
STUDENTSURNAME VARCHAR2(20)
STUDENTDEPT VARCHAR2(40)
STUDENTFACULTY VARCHAR2(20)
STUDENTCARDID NUMBER(4,0)
CardID table
CARDID NUMBER(4,0)
USERTYPE VARCHAR2(20)
CHARGE NUMBER(3,2)
CREDIT NUMBER(4,2)
PaymentDevice table:
ORDERNO NUMBER
PAYDEVIP NUMBER(8,0)
PAYDEVDATE DATE No
PAYDEVTIME VARCHAR2(8)
CHARGEDCARDID NUMBER(9,0)
MEALTYPE VARCHAR2(10)
I tried to return first 10 person's name and surname that eat at cafeteria on 27/12/2012
SELECT C.CARDID
FROM CARD C, PAYMENTDEVICE P
WHERE P.ORDERNO
BETWEEN (SELECT MIN(ORDERNO)
FROM PAYMENTDEVICE
WHERE PAYDEVDATE='27/12/2012') AND (SELECT MIN(ORDERNO)
FROM PAYMENTDEVICE
WHERE PAYDEVDATE='27/12/2012')+10 AND C.CARDID=P.CHARGEDCARDID;
Our orderNo isn't reset everyday but keeps increasing so we found the min orderNo that day and add 10 to this value to find first 10 person who eat on that day between those order numbers.
So what return from this query:
CARDID
1005
1000
1002
1003
1009
2000
2001
1007
2002
1004
1006
and those some of those cardId (start with 1) are studentCardId and some of them (starts with 2) are PersonnelCardId. So how can I match and write names accordingly?
SELECT *
FROM Personel p INNER JOIN Student s
ON p.PersonnelCardId = s.StudentCardId
INNER JOIN ReturnedQuery rq
ON rq.CardId = p.PersonnelCardId
updated:
SELECT p.PersonnelName, rq.CardId
FROM Personel p INNER JOIN ReturnedQuery rq
ON rq.CardId = p.PersonnelCardId
UNION
SELECT s.StudentName, rq.Cardid
FROM Student s INNER JOIN ReturnedQuery rq
ON s.StudentCardId = rq.Cardid
Your original query is actually pretty fragile. I'd rewrite it like so (and added the needed joins):
WITH First_Daily_Purchase as (SELECT chargedCardId,
MIN(payDevTime) as payDevTime,
MIN(orderNo) as orderNo
FROM PaymentDevice
WHERE payDevDate >=
TO_DATE('2012-12-27', 'YYYY-MM-DD')
AND payDevDate <
TO_DATE('2012-12-28', 'YYYY-MM-DD')
GROUP BY chargedCardId),
First_10_Daily_Purchasers as (SELECT chargedCardId
FROM (SELECT chargedCardId,
RANK() OVER(ORDER BY payDevTime,
orderNo) as rank
FROM First_Daily_Purchase) a
WHERE a.rank < 11)
SELECT a.chargedCardId, b.personnelName, b.personnelSurname
FROM First_10_Daily_Purchasers a
JOIN Personnel b
ON b.personnelCardId = a.chargedCardId
UNION ALL
SELECT a.chargedCardId, b.studentName, b.studentSurname
FROM First_10_Daily_Purchasers a
JOIN Student b
ON b.studentCardId = a.chargedCardId
(Have a working SQL Fiddle - generally bullet-proofing this took me a while.)
This should get you the first 10 people who made a purchase (not the first 11 purchases, which is what you were actually getting). This of course assumes that payDevTime is actually stored in a sortable format (if it isn't you have bigger problems than this query not working quite right).
That said, there's a number of troubling things about your schema design.