How to add a DB restriction - Check constraint or trigger - sql

I want to know how can I add a DB restriction on a table. I want to simplify the problem with a table in Oracle Database as
CREATE TABLE TEST_STUDENT
(
STUDENT VARCHAR2(30 CHAR),
SUBJECT VARCHAR2(38) ,
IS_LANG NUMBER(1,0)
);
A student can have any number of subjects but only one of them can be a language (IS_LANG).
Valid data would be
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('John','Math',);
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('John','Science',);
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('John','French',1);
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('Lily','Math',);
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('Lily','English',1);
however, I should not be able to insert fresh data like up the table, something like
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('John','English',1);
or
Insert into TEST_STUDENT (STUDENT,SUBJECT,IS_LANG) values ('Lily','French',1);
I don't want to introduce triggers here, unless it is the only way around. I want to have this restrictions because in the actual software would have multiple client implementations trying to insert data into this table.

This one of the good examples for a partial index.
Unfortunately in Oracle you need a workaround to implement a partial index (other DBMS simply allow a WHERE clause to be applied):
create unique index idx_one_language
on test_student
(
case when is_lang = 1 then student else null end
);
This exploits the fact that Oracle does not index tuples where all columns are null. With the above expression only rows where IS_LANG = 1 will be indexed for each student. As the index is defined as unique, only one such row can exist.
Here is a SQLFiddle example: http://sqlfiddle.com/#!4/43394d/1

Related

SQL Beginner trying to insert data on tables

I have started my journey in learning SQL and right I am having trouble creating and inserting data into tables. Here is the code that I have tried, I get an error message saying that there aren't enough values. I am using Oracle.
Create table project
(
proj_id number(10),
medic_name varchar2(10),
purpose varchar2(12),
start_date date,
end_date date,
pi_id null,
CONSTRAINT pkprojid primary key (proj_id),
CONSTRAINT fkproject foreign key (pi_id) references researcher
);
alter session set nls_date_format = 'mm/dd/yyyy';
Insert into project values (PR001, 'Medic1', 'heart', '09/01/2017', '07/31/2019');
Insert into project values (PR002, 'Medic1', 'diabetes', '10/01/2016', '07/31/2020);
Insert into project values (PR003, 'Medic3', 'lung', '11/1/2014', '12/31/2020');
Insert into project values (PR004, 'Medic3', 'blood', '01/10/2017', '07/31/2019');
Insert into project values (PR005, 'Medic5', 'blood', '07/10/2018', '01/31/2020');
alter session set nls_date_format = 'mm/dd/yyyy';
Insert into project values (PR001, 'Medic1', 'heart', '09/01/2017', '07/31/2019');
Issues:
Your table has 6 columns, you are only passing 5 for insert; it seems like you are missing last column (pi_id), hence the error message that you are getting. If you want to skip the last column (which is possible since it is declared as nullable), you can explictly list the column when inserting
first column (proj_id) is of number datatype; PR001 is not a number (neither a string, since it is not quoted: this is a syntax error); did you mean 1 instead? Or, if you want to insert string values, you need to change the datatype of column proj_id to varchar(N) (N being the maximum length of the string, in bytes).
Here is an insert statement that should work for your current table definition:
insert into project(proj_id, medic_name, purpose, start_date, end_date)
values (1, 'Medic1', 'heart', '09/01/2017', '07/31/2019');
Note: there is a missing quote at the end of the date on the second insert statement; I assume that this is a typo.

making a plsql comparison

I am relativity new to sql and need some help.
I have a table called TREATMENT which has 2 fields:
T_ID(primary key)
T_NAME.
Some of these treatments should not be used with others. For example.. if T_ID = 'T001' is used with T_ID = 'T002' it will cause severe headaches. and I want to do this for a few of them.
So I need the PL/SQL to take a value from the user and which a T_ID and the program will output the colliding treatment.
thanks in advance
this is my table:
DROP TABLE TREATMENT CASCADE CONSTRAINTS;
CREATE TABLE TREATMENT(
T_ID VARCHAR2(5) PRIMARY KEY NOT NULL,
T_NAME VARCHAR2(15));
INSERT INTO TREATMENT VALUES
('T001','ACCUPUNCTURE');
INSERT INTO TREATMENT VALUES
('T002','MASSAGE');
INSERT INTO TREATMENT VALUES
('T003','SKIN CARE');
INSERT INTO TREATMENT VALUES
('T004','OSTEOPATHY');
INSERT INTO TREATMENT VALUES
('T005','HOMOEOPATHY');
INSERT INTO TREATMENT VALUES
('T006','PHYSIOTHERAPY');
INSERT INTO TREATMENT VALUES
('T007','PSYCHOLOGY');
INSERT INTO TREATMENT VALUES
('T008','DEMATOLOGY');
INSERT INTO TREATMENT VALUES
('T009','YOGA');
commit;
I want for example to restrict "yoga" and "dermatology" from being taken together as they will have side effects like. user inputs T009 and system replies... yoga should not be taken with dermatology. will cause headache
This is a data modelling problem. You need another table to hold banned combinations. Eg.
create table contraindication
( treatment1 VARCHAR2(5)
, treatment2 VARCHAR2(5)
, primary key ci_pk (treatment1, treatment2) );
insert into contraindication values ('T008', 'T009');
Your procedure should look up this table.
You may find it helpful to insert both permutations, as this will make the query simpler (and more efficient if the list gets big):
insert into contraindication values ('T009', 'T008');

Database Normalization using Foreign Key

I have a sample table like below where Course Completion Status of a Student is being stored:
Create Table StudentCourseCompletionStatus
(
CourseCompletionID int primary key identity(1,1),
StudentID int not null,
AlgorithmCourseStatus nvarchar(30),
DatabaseCourseStatus nvarchar(30),
NetworkingCourseStatus nvarchar(30),
MathematicsCourseStatus nvarchar(30),
ProgrammingCourseStatus nvarchar(30)
)
Insert into StudentCourseCompletionStatus Values (1, 'In Progress', 'In Progress', 'Not Started', 'Completed', 'Completed')
Insert into StudentCourseCompletionStatus Values (2, 'Not Started', 'In Progress', 'Not Started', 'Not Applicable', 'Completed')
Now as part of normalizing the schema I have created two other tables - CourseStatusType and Status for storing the Course Status names and Status.
Create Table CourseStatusType
(
CourseStatusTypeID int primary key identity(1,1),
CourseStatusType nvarchar(100) not null
)
Insert into CourseStatusType Values ('AlgorithmCourseStatus')
Insert into CourseStatusType Values ('DatabaseCourseStatus')
Insert into CourseStatusType Values ('NetworkingCourseStatus')
Insert into CourseStatusType Values ('MathematicsCourseStatus')
Insert into CourseStatusType Values ('ProgrammingCourseStatus')
Insert into CourseStatusType Values ('OperatingSystemsCourseStatus')
Insert into CourseStatusType Values ('CompilerCourseStatus')
Create Table Status
(
StatusID int primary key identity(1,1),
StatusName nvarchar (100) not null
)
Insert into Status Values ('Completed')
Insert into Status Values ('Not Started')
Insert into Status Values ('In Progress')
Insert into Status Values ('Not Applicable')
The modified table is as below:
Create Table StudentCourseCompletionStatus1
(
CourseCompletionID int primary key identity(1,1),
StudentID int not null,
CourseStatusTypeID int not null CONSTRAINT [FK_StudentCourseCompletionStatus1_CourseStatusType] FOREIGN KEY (CourseStatusTypeID) REFERENCES dbo.CourseStatusType (CourseStatusTypeID),
StatusID int not null CONSTRAINT [FK_StudentCourseCompletionStatus1_Status] FOREIGN KEY (StatusID) REFERENCES Status (StatusID),
)
I have few question on this:
Is this the correct way to normalize it ? The old table was very helpful to get data easily - I can store a student's course status in a single row, but now 5 rows are required. Is there a better way to do it?
Moving the data from the old table to this new table seems to be not an easy task. Can I achieve this using a query or I have to manually to do this ?
Any help is appreciated.
vou could also consider storing results in flat table like this:
studentID,courseID,status
1,1,"completed"
1,2,"not started"
2,1,"not started"
2,3,"in progress"
you will also need additional Courses table like this
courserId,courseName
1, math
2, programming
3, networking
and a students table
students
1 "john smith"
2 "perry clam"
3 "john deere"
etc..you could also optionally create a status table to store the distinct statusstrings statusstings and refer to their PK instead ofthestrings
studentID,courseID,status
1,1,1
1,2,2
2,1,2
2,3,3
... etc
and status table
id,status
1,"completed"
2,"not started"
3,"in progress"
the beauty of this representation is: it is quite easy to filter and aggregate data , i.e it is easy to query which subjects a particular person have completed, how many subjects are completed by an average student, etc. this things are much more difficult in the columnar design like you had. you can also easily add new subjects without the need to adapt your tables or even queries they,will just work.
you can also always usin SQLs PIVOT query to get it to a familiar columnar presentation like
name,mathstatus,programmingstatus,networkingstatus,etc..
but now 5 rows are required
No, it's still just one row. That row simply contains identifiers for values stored in other tables.
There are pros and cons to this. One of the main reasons to normalize in this way is to protect the integrity of the data. If a column is just a string then anything can be stored there. But if there's a foreign key relationship to a table containing a finite set of values then only one of those options can be stored there. Additionally, if you ever want to change the text of an option or add/remove options, you do it in a centralized place.
Moving the data from the old table to this new table seems to be not an easy task.
No problem at all. Create your new numeric columns on the data table and populate them with the identifiers of the lookup table records associated with each data table record. If they're nullable, you can make them foreign keys right away. If they're not nullable then you need to populate them before you can make them foreign keys. Once you've verified that the data is correct, remove the old de-normalized columns. Done.
In StudentCourseCompletionStatus1 you still need 2 associations to Status and CourseStatusType. So I think you should consider following variant of normalization:
It means, that your StudentCourseCompletionStatus would hold only one CourseStatusID and another table CourseStatus would hold the associations to CourseType and Status.
To move your data you can surely use a query.

SQL : Is it possible to store duplicate null values in a column with unique constraint

How do you define unique constraint in terms of nulls ?
Is there any standard followed by all the databases to allow nulls in unique column.
Quoted from a blog, here is the amount of NULL values allowed in a column with a UNIQUE constraint, in a few DBMS :
What other DBMSs do
Trudy Pelzer and I wrote this on page 260 of our book, SQL Performance
Tuning: DBMS Maximum number of NULLs when there is a UNIQUE
constraint
IBM (DB2) One
Informix One
Ingres Zero
InterBase Zero
Microsoft (SQL Server) One
MySQL Many [although the BDB storage engine was an exception]
Oracle Many
Sybase One
Trying the following code in Sql Server (2012 Express, at least) fails, as expected :
DECLARE #T TABLE
(
value int unique
)
INSERT INTO #T VALUES(1)
INSERT INTO #T VALUES(2)
INSERT INTO #T VALUES(NULL)
INSERT INTO #T VALUES(NULL)
The article mentions that the correct rule about handling null values in a column with a unique constraint should be that the value of the unique column of any two rows, if both are not NULL, should not be equal.
In other words, non null values must be unique, and multiple null values are allowed.
But not all DBMS seems to apply that rule, so it's better to confirm it with a simple test.
"...a UNIQUE column can have multiple NULLs in a row unless you explicitly add a NOT NULL constraint to each column."
Joe Celko's "SQL for Smarties", 2nd edition, Page 16.
For some fun with NULL-s, try these:
SELECT 1 = NULL
(returns NULL)
SELECT 1 IS NULL
(returns FALSE)
It depends on the database engine. MySQL allow null duplicates but SQL Server doesn't.
You can try this on SQLFiddle. MySQL return an exception for the user 'ddd' while MySQL return an exception for the user 'ccc'
CREATE TABLE users (
username varchar(20) NOT NULL,
id int unique
);
insert into users values ('aaa', null);
insert into users values ('bbb', 1);
insert into users values ('ccc', null);
insert into users values ('ddd', 1);

SQL can I have a "conditionally unique" constraint on a table?

I've had this come up a couple times in my career, and none of my local peers seems to be able to answer it. Say I have a table that has a "Description" field which is a candidate key, except that sometimes a user will stop halfway through the process. So for maybe 25% of the records this value is null, but for all that are not NULL, it must be unique.
Another example might be a table which must maintain multiple "versions" of a record, and a bit value indicates which one is the "active" one. So the "candidate key" is always populated, but there may be three versions that are identical (with 0 in the active bit) and only one that is active (1 in the active bit).
I have alternate methods to solve these problems (in the first case, enforce the rule code, either in the stored procedure or business layer, and in the second, populate an archive table with a trigger and UNION the tables when I need a history). I don't want alternatives (unless there are demonstrably better solutions), I'm just wondering if any flavor of SQL can express "conditional uniqueness" in this way. I'm using MS SQL, so if there's a way to do it in that, great. I'm mostly just academically interested in the problem.
If you are using SQL Server 2008 a Index filter would maybe your solution:
http://msdn.microsoft.com/en-us/library/ms188783.aspx
This is how I enforce a Unique Index with multiple NULL values
CREATE UNIQUE INDEX [IDX_Blah] ON [tblBlah] ([MyCol]) WHERE [MyCol] IS NOT NULL
In the case of descriptions which are not yet completed, I wouldn't have those in the same table as the finalized descriptions. The final table would then have a unique index or primary key on the description.
In the case of the active/inactive, again I might have separate tables as you did with an "archive" or "history" table, but another possible way to do it in MS SQL Server at least is through the use of an indexed view:
CREATE TABLE Test_Conditionally_Unique
(
my_id INT NOT NULL,
active BIT NOT NULL DEFAULT 0
)
GO
CREATE VIEW dbo.Test_Conditionally_Unique_View
WITH SCHEMABINDING
AS
SELECT
my_id
FROM
dbo.Test_Conditionally_Unique
WHERE
active = 1
GO
CREATE UNIQUE CLUSTERED INDEX IDX1 ON Test_Conditionally_Unique_View (my_id)
GO
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (1, 1)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (2, 0)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (2, 1)
INSERT INTO dbo.Test_Conditionally_Unique (my_id, active)
VALUES (2, 1) -- This insert will fail
You could use this same method for the NULL/Valued descriptions as well.
Thanks for the comments, the initial version of this answer was wrong.
Here's a trick using a computed column that effectively allows a nullable unique constraint in SQL Server:
create table NullAndUnique
(
id int identity,
name varchar(50),
uniqueName as case
when name is null then cast(id as varchar(51))
else name + '_' end,
unique(uniqueName)
)
insert into NullAndUnique default values
insert into NullAndUnique default values -- Works
insert into NullAndUnique default values -- not accidentally :)
insert into NullAndUnique (name) values ('Joel')
insert into NullAndUnique (name) values ('Joel') -- Boom!
It basically uses the id when the name is null. The + '_' is to avoid cases where name might be numeric, like 1, which could collide with the id.
I'm not entirely aware of your intended use or your tables, but you could try using a one to one relationship. Split out this "sometimes" unique column into a new table, create the UNIQUE index on that column in the new table and FK back to the original table using the original tables PK. Only have a row in this new table when the "unique" data is supposed to exist.
OLD tables:
TableA
ID pk
Col1 sometimes unique
Col...
NEW tables:
TableA
ID
Col...
TableB
ID PK, FK to TableA.ID
Col1 unique index
Oracle does. A fully null key is not indexed by a Btree in index in Oracle, and Oracle uses Btree indexes to enforce unique constraints.
Assuming one wished to version ID_COLUMN based on the ACTIVE_FLAG being set to 1:
CREATE UNIQUE INDEX idx_versioning_id ON mytable
(CASE active_flag WHEN 0 THEN NULL ELSE active_flag END,
CASE active_flag WHEN 0 THEN NULL ELSE id_column END);