DBMS Unit II — Normalization, Security & PL/SQL
Visual, diagram-first learning with handmade flowcharts. Every definition comes with a worked example, every algorithm with a flowchart, and every chapter ends with exam-ready notes.
flowchart TD A["Raw table with anomalies"]:::y --> B["Find functional dependencies"]:::m B --> C["Normalize: 1NF to 5NF and BCNF"]:::m C --> D["Secure with roles, views, audits"]:::g D --> E["Automate with PL/SQL"]:::g
What Unit II covers
Unit II: Database Normalization, Security, and PL/SQL Programming. This unit focuses on database design and optimization through Functional Dependencies and Normalization techniques. You learn how to identify candidate keys, compute attribute closures, and perform decomposition while ensuring lossless join and dependency preservation. It also covers advanced normalization concepts such as BCNF, 4NF, and 5NF. Additionally, you learn database security, common threats, and countermeasures to protect organizational data.
mindmap
root((Unit II))
Chapter 2.1 Functional Dependencies
FD types
Full
Partial
Transitive
Join
Multi-valued
Armstrong axioms
Attribute closure
Candidate keys
Canonical cover
Decomposition
Chapter 2.2 Normalization
Anomalies
1NF 2NF 3NF
BCNF
4NF and 5NF
Denormalization
Chapter 2.3 Database Security
CIA triad
Threats and vulnerabilities
Authentication vs authorization
Access control
Countermeasures
PL/SQL Programming
Blocks
Cursors
Procedures and functions
Triggers
Exceptions
Packages
After completing this unit, you will be able to:
- Analyze functional dependencies and determine candidate keys using closure techniques.
- Apply normalization techniques (1NF to 5NF and BCNF) to eliminate data redundancy and anomalies.
- Evaluate database decompositions for lossless join and dependency preservation properties.
- Identify database security threats and recommend appropriate security mechanisms and countermeasures.
- Design efficient and secure relational database schemas for real-world applications.
Where each outcome is covered
| Outcome | Where to study it | Skill you practise |
|---|---|---|
| 1 · FDs and candidate keys | Chapter 2.1 | Attribute closure, key finding, canonical cover |
| 2 · Apply 1NF–5NF and BCNF | Chapter 2.2 | Convert schemas step by step, spot anomalies |
| 3 · Evaluate decompositions | Section 4 and 2.1 | Lossless join test, dependency preservation test |
| 4 · Security threats & countermeasures | Chapter 2.3 | Map threat → vulnerability → control |
| 5 · Design efficient, secure schemas | Section 7, PL/SQL | Normalize, constrain, grant, audit |
How the chapters connect
Chapter 2.1 gives you the mathematics of dependencies. Chapter 2.2 uses those dependencies to restructure tables so redundancy and anomalies disappear. Chapter 2.3 protects the well-structured data. PL/SQL is the programming layer that enforces business rules, constraints, audits and access control inside the database itself.
flowchart LR A["2.1 Functional dependencies"]:::g --> B["2.2 Normalization"]:::m B --> C["Decomposition: lossless and dependency preserving"]:::m C --> D["Normalized schema"]:::g D --> E["2.3 Security controls"]:::m D --> F["PL/SQL enforcement"]:::m E --> G["Secure, efficient database"]:::g F --> G
Functional Dependencies
This chapter introduces Functional Dependencies (FDs), Full Functional Dependency, Transitive Dependency, Join Dependency, and Multi-valued Dependency. You learn attribute closure computation, canonical cover generation, candidate key identification, and decomposition techniques.
- Define and analyze different types of dependencies in relational databases.
- Compute attribute closures and identify candidate keys.
- Derive canonical covers for a set of functional dependencies.
- Perform decomposition and verify lossless join and dependency preservation properties.
- Apply dependency concepts in relational database design.
2.1.1 What is a functional dependency?
Let R be a relation schema and X, Y be sets of attributes of R. The functional dependency X → Y (“X determines Y”) holds if, in every valid instance of R, any two tuples that agree on X also agree on Y. X is the determinant, Y the dependent.
An FD is a constraint on the meaning of the data, not something you read off a single table. A table can only disprove an FD (by showing two rows that agree on X but not on Y). The designer declares FDs from business rules: “a student ID identifies exactly one student name”.
| RollNo | Name | Dept | HOD |
|---|---|---|---|
| 101 | Asha | CS | Dr. Rao |
| 102 | Ravi | CS | Dr. Rao |
| 103 | Meena | EE | Dr. Iyer |
Here RollNo → Name, Dept, HOD holds (RollNo is the key) and Dept → HOD holds. But Dept → Name does not: rows 101 and 102 agree on Dept (CS) yet differ on Name.
Trivial FD: X → Y is trivial when Y ⊆ X (e.g. {RollNo, Name} → Name). Non-trivial: Y ⊄ X. Normalization only worries about non-trivial FDs.
2.1.2 Types of dependencies
Full functional dependency
X → Y is full if Y depends on all of X and on no proper subset of X. Example: in Enrollment(StudentID, CourseID, Grade), {StudentID, CourseID} → Grade is full, because neither StudentID alone nor CourseID alone determines Grade.
Partial dependency
Y depends on only part of a composite key. Example: {StudentID, CourseID} → StudentName is partial, since StudentID → StudentName already holds. Violates 2NF.
Transitive dependency
If X → Y and Y → Z, with Y not a key and Y ↛ X, then Z depends on X transitively. Example: RollNo → Dept → HOD. Violates 3NF.
Multi-valued dependency (MVD)
X →→ Y means each value of X is associated with a set of Y values, independent of the other attributes. Example: Emp →→ Skill while Emp →→ Language. Violates 4NF.
A join dependency ⋈(R1, R2, …, Rn) holds on R if R equals the natural join of its projections on R1…Rn in every valid instance. An FD or MVD is a special case. Join dependencies that are not implied by candidate keys violate 5NF.
flowchart TD
A["Dependency X to Y in relation R"]:::g --> B{"Is Y a subset of X?"}
B -- Yes --> C["Trivial FD"]:::m
B -- No --> D["Non-trivial FD"]:::g
D --> E{"Does Y depend on every attribute of X?"}
E -- Yes --> F["Full functional dependency"]:::m
E -- No --> G["Partial dependency on part of the key"]:::y
D --> H{"Does X to Y and Y to Z hold with Y not a key?"}
H -- Yes --> I["Transitive dependency X to Z"]:::y
A --> J["Beyond FDs"]
J --> K["Multi-valued: X multi-determines Y, independent sets"]:::m
J --> L["Join dependency: R equals join of its projections"]:::m
Treating a pattern in today’s data as an FD. If two students happen to have different names for different phone numbers, you still cannot say Phone → Name unless the business rule guarantees it for all future data.
2.1.3 Armstrong’s axioms
Armstrong’s axioms are sound (they derive only true FDs) and complete (they derive every FD implied by F). The set of all implied FDs is the closure of F, written F⁺.
| Rule | Statement | Example |
|---|---|---|
| Reflexivity | If Y ⊆ X then X → Y | AB → A |
| Augmentation | If X → Y then XZ → YZ | A → B gives AC → BC |
| Transitivity | If X → Y and Y → Z then X → Z | A → B, B → C gives A → C |
| Derived rules | ||
| Union | X → Y and X → Z give X → YZ | A → B, A → C gives A → BC |
| Decomposition | X → YZ gives X → Y and X → Z | A → BC gives A → B |
| Pseudotransitivity | X → Y and WY → Z give WX → Z | A → B, CB → D gives CA → D |
Proof of the union rule from the axioms
Given X → Y and X → Z. Augment the first with X: X → XY. Augment the second with Y: XY → YZ. By transitivity X → XY and XY → YZ give X → YZ. ∎
2.1.4 Attribute closure
The closure of an attribute set X under F, written X⁺, is the set of all attributes A such that X → A is implied by F. If X⁺ contains every attribute of R, X is a superkey.
- Set
result = X. - Look for an FD
A → Bin F withA ⊆ resultandB ⊄ result. - If found, set
result = result ∪ Band repeat step 2. - When no FD adds anything new, stop.
X⁺ = result.
flowchart TD
S(["Start: attribute set X and FD set F"]):::g --> I["result = X"]
I --> L{"Is there an FD A to B in F with A inside result and B not inside result?"}
L -- Yes --> U["result = result union B"]
U --> L
L -- No --> E(["Output: X plus = result"]):::g
Worked example: computing closures
R(A, B, C, D, E) with F = { A → B, BC → D, D → E }.
Closure of {A}
Start {A}. A → B adds B: {A,B}. BC → D needs C, which we lack. D → E needs D. So A⁺ = {A, B}. Not a superkey.
Closure of {A, C}
| Step | FD used | result |
|---|---|---|
| 0 | start | {A, C} |
| 1 | A → B | {A, B, C} |
| 2 | BC → D | {A, B, C, D} |
| 3 | D → E | {A, B, C, D, E} |
(AC)⁺ = {A, B, C, D, E} = all of R, so AC is a superkey.
R(A,B,C,D), F = {A → B, B → C}. What is (AD)⁺?
Show answer
{A,D} → add B (A→B) → add C (B→C): {A,B,C,D}. AD is a superkey.
Try it: closure and candidate-key calculator
Enter attributes and FDs (one per line) to see closure steps and all candidate keys.
2.1.5 Identifying candidate keys
Superkey: attribute set whose closure is all of R. Candidate key: a minimal superkey (no proper subset is a superkey). Prime attribute: belongs to some candidate key. Non-prime attribute: belongs to none.
- Find attributes that never appear on any right-hand side. They must be in every candidate key.
- Compute the closure of that set. If it covers R, it is the only candidate key.
- If not, add other attributes one at a time (then pairs, …) and test closures.
- Keep only minimal sets. Repeat to find all keys; the union of their attributes is the set of prime attributes.
flowchart TD
A["List attributes of R and FDs in F"]:::g --> B["Find attributes never on any right-hand side"]
B --> C["Compute closure of that set"]
C --> D{"Closure contains all attributes?"}
D -- Yes --> E["It is a candidate key and the only one"]:::g
D -- No --> F["Add one more attribute from the rest"]
F --> G["Compute closure of each combination"]
G --> H{"Closure equals all attributes?"}
H -- No --> F
H -- Yes --> I["Candidate key if no proper subset is a key"]:::g
I --> J["Try other combinations of the same size for more keys"]
J --> K["Prime attributes are all attributes in any candidate key"]:::m
Worked example: relation with several candidate keys
R(A, B, C, D) with F = { AB → C, C → D, D → A }.
- B never appears on a right-hand side, so B is in every key. B⁺ = {B}: not enough.
- (AB)⁺: AB → C, then C → D: {A,B,C,D} ✔ key.
- (BC)⁺: C → D, D → A, then AB → C: {A,B,C,D} ✔ key.
- (BD)⁺: D → A, AB → C, C → D: {A,B,C,D} ✔ key.
Candidate keys: AB, BC, BD. Prime attributes: A, B, C, D (all). A relation whose attributes are all prime is always in 3NF.
Always run the “never on a right-hand side” shortcut first. It saves most of the search, and attributes that appear only on the right can never be in any key.
2.1.6 Canonical (minimal) cover
Different FD sets can imply the same F⁺. A canonical cover Fc is an equivalent set with no redundancy: every FD has a single attribute on the right (in the textbook form), no extraneous attribute on any left side, and no FD that is implied by the others. It is the starting point for 3NF synthesis.
flowchart TD
A["Given FD set F"]:::g --> B["Step 1: split right sides to single attributes"]
B --> C["Step 2: remove extraneous attributes from left sides"]
C --> D["Step 3: remove redundant FDs"]
D --> E{"Did anything change in step 2 or 3?"}
E -- Yes --> C
E -- No --> F["Canonical cover Fc, optionally merge FDs with same left side"]:::g
Extraneous attribute A in X → Y (A ∈ X): A is extraneous if (X − A) → Y can be derived, i.e. Y ⊆ (X − A)⁺ computed using F. Redundant FD: X → Y is redundant if Y ⊆ X⁺ computed with that FD removed.
Worked example: canonical cover
F = { A → BC, B → C, A → B, AB → C }
Step 1 · Single attribute on the right
A → B, A → C, B → C, A → B (duplicate, drop), AB → C. Now F = { A → B, A → C, B → C, AB → C }.
Step 2 · Extraneous left-side attributes
Check AB → C. Is A extraneous? Compute B⁺ under F: B → C gives {B, C}, which contains C. So A is extraneous: replace AB → C by B → C (a duplicate). F = { A → B, A → C, B → C }.
Step 3 · Redundant FDs
Test A → C. Without it, A⁺: A → B gives {A,B}; B → C gives {A,B,C}. C is reachable, so A → C is redundant. Remove it.
Fc = { A → B, B → C }. Check equivalence: A⁺ = {A,B,C} and B⁺ = {B,C}, so every original FD is still implied.
Removing a redundant FD before removing extraneous attributes can give a different (and sometimes wrong) result. Follow the order: split, shrink left sides, then drop redundant FDs. A canonical cover is not always unique, but any valid one is acceptable.
2.1.7 Decomposition techniques
Decomposition replaces R by smaller schemas R1…Rn such that R1 ∪ … ∪ Rn = R. We decompose to remove redundancy and anomalies. A good decomposition must be lossless (the original data can be rebuilt exactly with joins) and preferably dependency preserving (every FD can be checked inside a single table).
Lossless-join test (binary)
Decomposing R into R1, R2 is lossless iff R1 ∩ R2 → R1 − R2 or R1 ∩ R2 → R2 − R1 is in F⁺. Equivalently: the common attributes are a superkey of R1 or R2.
Dependency preservation
Let Fi be the FDs of F⁺ whose attributes lie within Ri. The decomposition preserves dependencies iff (F₁ ∪ … ∪ Fₙ)⁺ = F⁺.
Full tests, tableau (chase) method and worked examples are in Section 4. Below is the verification flow you apply to any proposed decomposition.
flowchart TD
A["Proposed decomposition of R into R1 ... Rn"]:::g --> B["Check 1: lossless join"]
B --> C{"Binary case: does R1 intersect R2 determine R1 minus R2 or R2 minus R1?"}
B --> N["More than 2 tables: use the chase tableau"]
C -- No --> X["Lossy: reject or add common key attributes"]:::y
C -- Yes --> D["Check 2: dependency preservation"]
N --> D
D --> E{"Is every FD of F implied by the projected FDs?"}
E -- No --> W["FD lost: enforcing it needs a join"]:::y
E -- Yes --> OK["Good decomposition: lossless and dependency preserving"]:::g
2.1.8 Applying dependency concepts in design
In practice you (1) gather business rules, (2) write them as FDs, MVDs and JDs, (3) compute candidate keys, (4) reduce F to a canonical cover, (5) decompose to the target normal form, (6) verify lossless join and dependency preservation, and (7) implement keys and constraints in SQL.
-- FDs RollNo -> Name, Dept and Dept -> HOD become keys in two tables
CREATE TABLE department (
dept VARCHAR2(10) PRIMARY KEY, -- determinant of HOD
hod VARCHAR2(60) NOT NULL
);
CREATE TABLE student (
roll_no NUMBER PRIMARY KEY, -- determinant of name, dept
name VARCHAR2(60) NOT NULL,
dept VARCHAR2(10) NOT NULL REFERENCES department(dept)
);
-- Rebuild the original relation: the join is lossless
SELECT s.roll_no, s.name, s.dept, d.hod
FROM student s JOIN department d ON d.dept = s.dept;
Every FD whose left side becomes a PRIMARY KEY or UNIQUE constraint is enforced for free by the DBMS. FDs that are not preserved need triggers or application checks.
Normalization
This chapter covers the normalization process for improving database design. You learn 1NF, 2NF, 3NF, BCNF, 4NF, 5NF, and denormalization techniques to reduce redundancy and prevent update anomalies.
- Explain the need and objectives of normalization.
- Convert database schemas into 1NF, 2NF, 3NF, and BCNF.
- Apply higher normal forms (4NF and 5NF) to complex database designs.
- Identify insertion, deletion, and update anomalies.
- Evaluate situations where denormalization may improve performance.
3.1 Need and objectives of normalization
Normalization is the systematic process of organizing attributes into relations using dependencies, so that each fact is stored once and only once.
Reduce redundancy
No repeated storage of the same fact, saving space and avoiding disagreement between copies.
Remove anomalies
Insert, update and delete operations work on one fact at a time without side effects.
Preserve meaning
Lossless decomposition keeps all information; keys and FDs enforce integrity.
3.2 Insertion, deletion and update anomalies
Consider this poorly designed table that mixes employees, departments and projects. Primary key: EmpID.
| EmpID | EmpName | DeptID | DeptName | DeptHead |
|---|---|---|---|---|
| E1 | Asha | D1 | Sales | Kiran |
| E2 | Ravi | D1 | Sales | Kiran |
| E3 | Meena | D2 | HR | Latha |
| Anomaly | What goes wrong in the table above |
|---|---|
| Insertion | You cannot add a new department D3 (Finance) until at least one employee is hired, since EmpID cannot be NULL. |
| Deletion | Deleting E3 (Meena) also erases the fact that department D2 (HR, head Latha) exists. |
| Update | Renaming Sales to Revenue needs edits in every Sales row; missing one leaves inconsistent data. |
flowchart TD R["Facts about different entities stored in one table"]:::y --> RD["Redundant data"] RD --> I["Insertion anomaly: cannot add one fact without another"] RD --> D["Deletion anomaly: deleting one fact loses another"] RD --> U["Update anomaly: many rows to change, risk of inconsistency"] I --> S["Remedy: decompose using FDs into Employee and Department"]:::g D --> S U --> S
3.3 Functional dependency review
Normal forms are defined entirely with FDs and keys. Keep these in mind: a determinant is the left side of an FD; a candidate key is a minimal set whose closure is all of R; a prime attribute lies in some candidate key. Revisit Chapter 2.1 if any term feels shaky.
3.4 The normalization ladder
flowchart TD U["Unnormalized: repeating groups, multi-valued cells"]:::y --> N1["1NF: atomic values, key defined"]:::m N1 --> N2["2NF: no partial dependency on a composite key"]:::m N2 --> N3["3NF: no transitive dependency for non-prime attributes"]:::m N3 --> BC["BCNF: every determinant is a superkey"]:::g BC --> N4["4NF: no non-trivial multi-valued dependency"]:::g N4 --> N5["5NF: every join dependency implied by candidate keys"]:::g
3.5 The normal forms, one by one
A relation is in First Normal Form if every attribute holds a single atomic value (no lists, sets or repeating groups) and every row can be uniquely identified by a key.
Violation: Student(ID, Name, Phones) with Phones = "98100, 98200".
Conversion: (a) put one value per cell by making a separate row or a separate table, (b) define a primary key.
| ID | Name | Phones (not 1NF) |
|---|---|---|
| 1 | Asha | 98100, 98200 |
| 2 | Ravi | 98300 |
| ID | Phone (1NF) |
|---|---|
| 1 | 98100 |
| 1 | 98200 |
| 2 | 98300 |
Name stays in Student(ID, Name); phones move to StudentPhone(ID, Phone) with key (ID, Phone).
A relation is in 2NF if it is in 1NF and no non-prime attribute is partially dependent on any candidate key (every non-prime attribute is fully dependent on each key). Only relations with composite keys can violate 2NF.
Violation: Enrollment(StudentID, CourseID, StudentName, CourseTitle, Grade) with FDs StudentID → StudentName, CourseID → CourseTitle, (StudentID, CourseID) → Grade.
Conversion: for each partial dependency, move the dependent attribute together with its determinant into a new relation.
Student(StudentID, StudentName)
Course(CourseID, CourseTitle)
Enrollment(StudentID, CourseID, Grade)
A relation is in 3NF if it is in 2NF and no non-prime attribute is transitively dependent on a key. Formally, for every non-trivial FD X → A, either X is a superkey or A is a prime attribute.
Violation: Student(RollNo, Name, Dept, HOD) with RollNo → Dept and Dept → HOD, so RollNo → HOD is transitive.
Conversion: split the chain.
Student(RollNo, Name, Dept) -- Dept is a foreign key
Department(Dept, HOD)
3NF decomposition can always be made lossless and dependency preserving using the synthesis algorithm (canonical cover → one relation per FD → add a key relation if needed → remove subsumed relations).
A relation is in Boyce–Codd Normal Form if for every non-trivial FD X → Y, X is a superkey. No exception for prime attributes.
Violation (3NF but not BCNF): CourseTeacher(student, course, teacher) with FDs (student, course) → teacher and teacher → course (each teacher teaches one course). Candidate keys: (student, course) and (student, teacher). Since course is prime, 3NF holds; but teacher is not a superkey, so BCNF fails.
BCNF decomposition algorithm: while some Ri has a violating FD X → Y, replace Ri by (Ri − Y) and (X ∪ Y). Here: TeacherCourse(teacher, course) and StudentTeacher(student, teacher).
This decomposition is lossless (common attribute teacher is the key of TeacherCourse) but loses (student, course) → teacher, because student, course and teacher no longer sit in one table. That is the classic BCNF trade-off.
flowchart TD
A["Take each non-trivial FD X to A in R"]:::g --> B{"Is X a superkey?"}
B -- Yes --> C["Allowed in both 3NF and BCNF"]:::g
B -- No --> D{"Is A a prime attribute?"}
D -- Yes --> E["3NF allows it but BCNF is violated"]:::y
D -- No --> F["Violates both 3NF and BCNF"]:::y
E --> G["Decompose into X plus A and the rest, then check dependency preservation"]
F --> H["Decompose with BCNF algorithm or 3NF synthesis"]
A relation is in Fourth Normal Form if it is in BCNF and, for every non-trivial multi-valued dependency X →→ Y, X is a superkey.
Multi-valued dependency X →→ Y holds when, for any two tuples agreeing on X, swapping their Y-values yields two tuples that are also in the relation. It arises when two independent multi-valued facts about the same entity are stored together.
Violation: EmpSkillLang(emp, skill, language) with emp →→ skill and emp →→ language. The key is all three attributes, so it is already in BCNF, yet look at the redundancy:
| emp | skill | language |
|---|---|---|
| Asha | SQL | English |
| Asha | SQL | Hindi |
| Asha | Java | English |
| Asha | Java | Hindi |
| emp | skill |
|---|---|
| Asha | SQL |
| Asha | Java |
| emp | language |
|---|---|
| Asha | English |
| Asha | Hindi |
Adding one new skill forces an insert for every language. After 4NF decomposition each fact is stored once.
flowchart TD R["EmpSkillLang with emp, skill, language"]:::g --> M["MVDs: emp multi-determines skill and emp multi-determines language"] M --> P["Every skill pairs with every language: row explosion"]:::y P --> S1["EmpSkill with emp, skill"]:::m P --> S2["EmpLanguage with emp, language"]:::m S1 --> J["Natural join on emp rebuilds the original"]:::g S2 --> J
A relation is in Fifth Normal Form (Project-Join Normal Form) if it is in 4NF and every join dependency is implied by the candidate keys. Equivalently, it cannot be decomposed losslessly into smaller relations unless the decomposition is by candidate keys.
Example: Supply(supplier, part, project) with the business rule: if supplier S supplies part P, project J uses part P, and S supplies project J, then S supplies P to J. This is a cyclic constraint, so the relation equals the join of three binary projections: ⋈(SP, PJ, SJ).
| S | P | J |
|---|---|---|
| S1 | P1 | J2 |
| S1 | P2 | J1 |
| S2 | P1 | J1 |
| S1 | P1 | J1 |
| SP | PJ | SJ |
|---|---|---|
| (S1,P1) | (P1,J2) | (S1,J2) |
| (S1,P2) | (P2,J1) | (S1,J1) |
| (S2,P1) | (P1,J1) | (S2,J1) |
Joining SP ⋈ PJ ⋈ SJ gives exactly the four original tuples (the spurious tuple (S2,P1,J2) is removed because (S2,J2) is not in SJ). Because the three-way join dependency is not implied by a key, the 3-column table is not in 5NF; replacing it by the three projections stores each fact once.
flowchart TD R["Supply with S, P, J"]:::g --> SP["SP with supplier, part"]:::m R --> PJ["PJ with part, project"]:::m R --> SJ["SJ with supplier, project"]:::m SP --> JN["Join of SP, PJ and SJ equals R"]:::g PJ --> JN SJ --> JN
5NF cases are rare in practice. In an exam, the giveaway is a three-way (or higher) cyclic business rule where no pair of projections can rebuild the table losslessly on its own.
3.6 Comparison of normal forms
| NF | Condition (in addition to the previous form) | Removes | Typical fix |
|---|---|---|---|
| 1NF | Atomic values, no repeating groups, key defined | Multi-valued cells | Separate rows or tables |
| 2NF | No partial dependency on a candidate key | Partial dependencies | Move partial dependents with their determinant |
| 3NF | For every X → A: X superkey or A prime | Transitive dependencies | Split the transitive chain |
| BCNF | For every X → Y: X superkey | Anomalies from overlapping candidate keys | Decompose on the violating FD |
| 4NF | For every non-trivial X →→ Y: X superkey | Independent multi-valued facts | One table per independent fact |
| 5NF | Every join dependency implied by candidate keys | Cyclic join redundancy | Project into the join components |
3.7 Worked example: unnormalized → 1NF → 2NF → 3NF → BCNF
Start with one wide table for course registration. Business rules (FDs): StudentID → StudentName; StudentID ↠ many phone numbers; CourseID → CourseTitle, Instructor; Instructor → Room; (StudentID, CourseID) → Grade.
Step 0 · Unnormalized table
| StudentID | StudentName | Phones | CourseID | CourseTitle | Instructor | Room | Grade |
|---|---|---|---|---|---|---|---|
| S1 | Asha | 98100, 98200 | C1, C2 | DBMS, OS | Rao, Iyer | R10, R20 | A, B |
| S2 | Ravi | 98300 | C1 | DBMS | Rao | R10 | B |
Cells hold lists, so this violates 1NF.
Step 1 · 1NF: atomic values
Give each (student, course) pair its own row and move phones into their own table.
StudentPhone(StudentID, Phone) key: (StudentID, Phone)
Registration(StudentID, StudentName, CourseID, CourseTitle,
Instructor, Room, Grade) key: (StudentID, CourseID)
Step 2 · 2NF: remove partial dependencies
StudentName depends on StudentID only; CourseTitle, Instructor and Room depend on CourseID only (partial dependencies on the composite key).
Student(StudentID, StudentName)
Course(CourseID, CourseTitle, Instructor, Room)
Enrollment(StudentID, CourseID, Grade)
StudentPhone(StudentID, Phone)
Step 3 · 3NF: remove transitive dependencies
In Course: CourseID → Instructor and Instructor → Room, so CourseID → Room is transitive.
Course(CourseID, CourseTitle, Instructor) -- Instructor is a foreign key
Instructor(Instructor, Room)
Student(StudentID, StudentName)
Enrollment(StudentID, CourseID, Grade)
StudentPhone(StudentID, Phone)
Step 4 · BCNF check
Check every determinant: StudentID (key of Student), CourseID (key of Course), Instructor (key of Instructor), (StudentID, CourseID) (key of Enrollment). Every determinant is a superkey, so the schema is already in BCNF. (A BCNF failure needs overlapping candidate keys, as in the CourseTeacher example in the BCNF tab.) With no independent multi-valued facts and no cyclic join rule, it is also in 4NF and 5NF.
R(A,B,C) with F = {AB → C, C → B}. Highest normal form?
Show answer
Keys: AB and AC. C → B has a non-superkey determinant, so BCNF fails, but B is prime so 3NF holds. Answer: 3NF.
Forgetting that BCNF has no prime-attribute exception, and forgetting that a relation with a single-attribute key is automatically in 2NF.
3.8 Denormalization
Denormalization deliberately introduces controlled redundancy to a normalized schema to speed up reads. It is a performance decision made after normalizing, never a replacement for normalization.
| Technique | Example | Cost |
|---|---|---|
| Derived / stored column | Store order_total in Orders | Must stay in sync with line items |
| Pre-joined / merged table | Copy customer_name into Orders | Update anomalies on rename |
| Summary table / materialized view | Daily sales per branch | Refresh time, storage |
| Repeating group as columns | phone1, phone2 | Violates 1NF, limited flexibility |
flowchart TD
A["Schema normalized to 3NF or BCNF"]:::g --> B{"Measured performance problem?"}
B -- No --> C["Stay normalized"]:::g
B -- Yes --> D{"Read-heavy workload with costly joins?"}
D -- No --> E["Tune indexes, queries and caching first"]:::m
D -- Yes --> F{"Can redundancy be kept consistent by triggers or batch refresh?"}
F -- No --> E
F -- Yes --> G["Denormalize selectively"]:::y
G --> H["Document it and monitor consistency"]
Use it when: reporting or analytics dominate, joins across many tables are provably the bottleneck, data is mostly read-only (data warehouses, dashboards). Avoid it when: the workload is write-heavy or data accuracy is critical (banking balances).
Always state the trade-off: faster reads versus redundancy, extra storage and the risk of update anomalies. Mention that triggers or materialized views can keep the redundant copy consistent.
Decomposition, Lossless Join & Dependency Preservation
Unit outcome 3: evaluate database decompositions for lossless join and dependency preservation properties.
4.1 Why decompose?
To eliminate the redundancy and anomalies identified by normal-form violations. Splitting a relation is safe only when it keeps (1) all the information (lossless join) and ideally (2) all the constraints (dependency preservation).
flowchart TD
A["Relation R with FD set F"]:::g --> B{"Anomalies or redundancy present?"}
B -- No --> Z["Keep R as it is"]
B -- Yes --> C{"R already in BCNF?"}
C -- Yes --> D["Check 4NF and 5NF, otherwise done"]
C -- No --> E["Pick violating FD X to Y: split into X union Y and R minus Y"]
E --> F["Lossless guaranteed since X is a key of X union Y"]:::g
F --> G{"Are all FDs preserved?"}
G -- Yes --> H["Accept BCNF decomposition"]:::g
G -- No --> I{"Is losing the FD acceptable?"}
I -- Yes --> H
I -- No --> J["Use 3NF synthesis: lossless and dependency preserving"]:::m
4.2 Lossless join
A decomposition of R into R1, …, Rn is lossless if for every valid instance r of R, r = πR1(r) ⋈ … ⋈ πRn(r). A lossy decomposition generates spurious tuples on joining.
Binary test
Example: lossy versus lossless on R(A, B, C)
Instance r: (1, x, p) and (2, x, q). FD set F = { A → B }.
Decomposition {AB, BC}: common = B
B → A? No. B → C? No. Not lossless. Check with data: πAB = {(1,x),(2,x)}, πBC = {(x,p),(x,q)}. Join gives 4 tuples: (1,x,p), (1,x,q), (2,x,p), (2,x,q). Two are spurious ✘.
Decomposition {AB, AC}: common = A
A → B holds, and B = R1 − R2. Lossless ✔. Check: πAB = {(1,x),(2,x)}, πAC = {(1,p),(2,q)}. Join on A gives exactly (1,x,p), (2,x,q).
flowchart TD
A["Decomposition D with R1 ... Rn of R"]:::g --> B{"Only two relations?"}
B -- Yes --> C["common = R1 intersect R2"]
C --> D["Compute closure of common under F"]
D --> E{"Closure contains R1 or R2?"}
E -- Yes --> L["Lossless join"]:::g
E -- No --> M["Lossy join"]:::y
B -- No --> F["Build tableau: one row per Ri, distinguished symbol where attribute in Ri"]
F --> G["Apply each FD: equate symbols, prefer distinguished ones"]
G --> H{"Any row entirely distinguished?"}
H -- Yes --> L
H -- No --> I{"Any further change possible?"}
I -- Yes --> G
I -- No --> M
Example: chase tableau for three relations
R(A,B,C,D), F = { B → C, C → D }, decomposition R1(A,B), R2(B,C), R3(C,D). Distinguished symbols are a1…a4; b-symbols are unknowns.
| Row | A | B | C | D |
|---|---|---|---|---|
| R1 | a1 | a2 | b13 | b14 |
| R2 | b21 | a2 | a3 | b24 |
| R3 | b31 | b32 | a3 | a4 |
- B → C: rows R1 and R2 agree on B (a2), so set R1.C = a3.
- C → D: R1, R2 and R3 now agree on C (a3), so equate their D values to the distinguished a4.
- R1 becomes (a1, a2, a3, a4): fully distinguished ⇒ lossless.
4.3 Dependency preservation
Let Fi be the set of FDs in F⁺ that involve only attributes of Ri. The decomposition is dependency preserving if (F₁ ∪ F₂ ∪ … ∪ Fₙ)⁺ = F⁺, i.e. every FD can be enforced by checking single tables, without joins.
Practical test (avoids computing F⁺): for each FD X → Y in F, set result = X and repeat result = result ∪ ((result ∩ Ri)⁺ ∩ Ri) for every Ri until nothing changes. X → Y is preserved iff Y ⊆ result.
flowchart TD
A["For each FD X to Y in F"]:::g --> B["result = X"]
B --> C["For each Ri: result = result union closure of result intersect Ri, restricted to Ri"]
C --> D{"Did result change in this pass?"}
D -- Yes --> C
D -- No --> E{"Is Y inside result?"}
E -- Yes --> F["This FD is preserved"]:::g
E -- No --> G["FD lost: decomposition is not dependency preserving"]:::y
Example: R(A,B,C) with F = { A → B, B → C }
Decomposition D1 = {AB, AC}
- Lossless: common = A, A → B ✔
- A → B lies in AB ✔ preserved.
- B → C: result = {B}. AB: (B ∩ AB)⁺ ∩ AB = {B,C} ∩ AB = {B}. AC: B ∩ AC = ∅, adds nothing. result stays {B}, C ∉ result ✘ lost.
Lossless but not dependency preserving.
Decomposition D2 = {AB, BC}
- Lossless: common = B, B → C (C = R2 − R1) ✔
- A → B inside AB ✔; B → C inside BC ✔.
Lossless and dependency preserving. This is the better design (also the 3NF synthesis result).
4.4 Trade-offs
| Goal | 3NF synthesis | BCNF decomposition |
|---|---|---|
| Lossless join | Always | Always |
| Dependency preservation | Always | Not guaranteed |
| Redundancy removed | Mostly (some FD-related redundancy may remain) | Maximum for FDs |
| Query cost | More joins than the original | Possibly most joins |
More decomposition means more tables and more joins. Normalization optimizes write correctness and storage; read performance may need indexes, materialized views or selective denormalization (see 3.8). Aim for a lossless, dependency-preserving 3NF or BCNF design, then measure.
Remember the headline facts: BCNF is always lossless but may not preserve dependencies; 3NF can always achieve both. A decomposition missing common attributes between its parts is almost always lossy.
Database Security
This chapter introduces database security concepts, common security threats, vulnerabilities, and protection mechanisms. It emphasizes access control, authentication, authorization, and countermeasures against attacks.
- Explain the importance of database security in organizations.
- Identify common database threats and vulnerabilities.
- Differentiate between authentication and authorization mechanisms.
- Recommend suitable countermeasures to secure database systems.
- Apply security principles to protect data confidentiality, integrity, and availability.
5.1 Why database security matters
Databases hold an organization’s most valuable assets: customer identities, financial records, health data, trade secrets. A breach causes financial loss, legal penalties (privacy laws such as GDPR and India’s DPDP Act), loss of customer trust and operational downtime. Security must be built in to the schema, the access rules and the code, not added afterwards.
5.2 The CIA triad
flowchart TD C(["CIA triad"]):::g C --> CO["Confidentiality: only authorized parties read data. Tools: access control, encryption, masking"]:::m C --> IN["Integrity: data stays accurate and changes only in authorized ways. Tools: constraints, hashing, audit, transactions"]:::m C --> AV["Availability: data is reachable when needed. Tools: backups, replication, patching, DoS defence"]:::m
| Goal | Violated by | Protected with |
|---|---|---|
| Confidentiality | Data leakage, stolen backups, SQL injection reads | Authentication, GRANT/REVOKE, views, encryption |
| Integrity | Unauthorized UPDATE, tampered logs | Constraints, triggers, hashing, least privilege, audit |
| Availability | DoS, ransomware, disk failure | Backups, replication, firewalls, patching |
5.3 Threats and vulnerabilities
A threat is a potential cause of harm (an attacker, malware, a careless insider). A vulnerability is a weakness that a threat can exploit (an unpatched server, a weak password). Risk arises when a threat meets a vulnerability and the asset has value. A countermeasure reduces that risk.
flowchart TD TA["Threat agent: external attacker, insider, malware"]:::y --> T["Threat: SQL injection, privilege escalation, data leakage"] T --> V["Vulnerability: weak password, unpatched server, misconfiguration"] V --> AS["Asset at risk: confidentiality, integrity, availability of data"]:::g C["Countermeasures: least privilege, encryption, auditing, patching"]:::m -. blocks .-> V C -. detects .-> T
| Threat / vulnerability | How it works | Impact | Countermeasures |
|---|---|---|---|
| SQL injection | Attacker input is concatenated into SQL (' OR '1'='1) | Data theft, modification, auth bypass | Bind variables, input validation, least privilege, WAF |
| Privilege escalation | Exploiting bugs or excess rights to gain DBA power | Full control of data | Least privilege, patching, role review, auditing |
| Insider threat | Employee misuses legitimate access | Leakage, fraud | Separation of duties, auditing, views, monitoring |
| Malware / ransomware | Malicious code encrypts or steals data | Downtime, data loss | Endpoint protection, offline backups, network segmentation |
| Social engineering | Phishing or impersonation to obtain credentials | Account takeover | Training, MFA, restricted admin accounts |
| Weak authentication | Default or guessable passwords, no lockout | Unauthorized login | Strong password policy, MFA, account lockout profiles |
| Misconfiguration | Open ports, default accounts, public cloud buckets | Easy attack surface | Hardening baselines, remove defaults, firewalls |
| Data leakage | Over-broad queries, logs or exports expose sensitive data | Privacy violations | Views, masking, encryption, DLP, column-level rights |
| Backup exposure | Unencrypted backups stored or shipped carelessly | Bypasses all access controls | Encrypt backups, restrict storage access, test restores |
SQL injection: vulnerable code versus safe code
-- VULNERABLE: input is concatenated into the statement
v_sql := 'SELECT * FROM customer WHERE email = ''' || p_email || '''';
EXECUTE IMMEDIATE v_sql;
-- p_email = x' OR '1'='1 returns every customer
-- SAFE: bind variable keeps input as data
EXECUTE IMMEDIATE 'SELECT full_name FROM customer WHERE email = :e'
INTO v_name USING p_email;
5.4 Authentication versus authorization
| Authentication | Authorization | |
|---|---|---|
| Question | Who are you? | What are you allowed to do? |
| Happens | First, at login | After authentication, on every operation |
| Mechanisms | Passwords, MFA, certificates, biometrics, SSO/Kerberos | GRANT/REVOKE, roles, views, row-level security |
| Failure result | Login rejected | “Insufficient privileges” |
| Example | CONNECT asha/secret | GRANT SELECT ON account TO asha |
sequenceDiagram
autonumber
participant U as User
participant A as Authentication
participant Z as Authorization
participant D as Database
U->>A: Username and password or token
A->>A: Verify identity
alt Identity invalid
A-->>U: Login rejected
else Identity valid
A-->>U: Session established
U->>Z: SELECT salary FROM employee
Z->>Z: Check privileges and roles
alt Not permitted
Z-->>U: Insufficient privileges error
else Permitted
Z->>D: Execute query
D-->>U: Rows returned
end
end
5.5 Access control models
Discretionary Access Control: the owner of an object decides who gets which privilege, and may pass the privilege on (WITH GRANT OPTION). Flexible, but risky because users can leak rights.
GRANT SELECT, UPDATE (status) ON account TO asha;
GRANT SELECT ON account TO ravi WITH GRANT OPTION;
REVOKE UPDATE ON account FROM asha;
Mandatory Access Control: a central policy assigns security labels to subjects (clearance) and objects (classification), e.g. Unclassified < Confidential < Secret < Top Secret. Bell–LaPadula rules: no read up (read only if clearance ≥ classification) and no write down. Users cannot override it. Used in military and government systems (e.g. Oracle Label Security).
Role-Based Access Control: privileges are granted to roles that reflect job functions; users are assigned to roles. Changing a person’s job means changing their role, not dozens of grants. Supports least privilege and separation of duties.
flowchart TD DBA["Role: DBA, full control"]:::g --> MGR["Role: Branch Manager inherits Teller rights"]:::m MGR --> TELLER["Role: Teller"]:::m MGR --> AUD["Role: Auditor"]:::m TELLER --> P1["Privileges: SELECT and INSERT on transactions"] AUD --> P2["Privilege: SELECT on audit_log"] U1(["User Asha"]) -.-> TELLER U2(["User Ravi"]) -.-> AUD U3(["User Meena"]) -.-> MGR
CREATE ROLE teller_role;
GRANT SELECT, INSERT ON acct_transaction TO teller_role;
GRANT teller_role TO manager_role; -- hierarchy: manager inherits teller
GRANT teller_role TO asha; -- user gets role
| Model | Who decides? | Strength | Weakness |
|---|---|---|---|
| DAC | Object owner | Flexible, simple | Rights can spread, weak against Trojans |
| MAC | Central policy and labels | Strongest control of information flow | Rigid, administrative cost |
| RBAC | Administrator via roles | Scalable, supports least privilege | Role explosion if badly designed |
5.6 Countermeasures
| Control | What it does | Example |
|---|---|---|
| Least privilege | Give only the rights needed for the job | Reporting user gets SELECT only |
| Encryption | Protects data at rest (TDE) and in transit (TLS) | Encrypted tablespaces and backups |
| Hashing | One-way digest for passwords and integrity checks (salted, slow algorithms) | Store bcrypt/PBKDF2 hash, not password |
| Views | Expose only selected rows or columns | View without salary column |
| Stored procedures | Controlled, validated access path; users get EXECUTE, not table rights | GRANT EXECUTE ON transfer_funds |
| Auditing | Record who did what and when | AUDIT SELECT ON account, audit triggers |
| Firewalls | Limit network access to the DB port | Allow only app servers |
| IDS / IPS | Detect (IDS) or block (IPS) malicious patterns | Alert on mass SELECTs |
| Backups | Recover from loss or ransomware | Encrypted, offline, tested restore |
| Patching | Closes known vulnerabilities | Apply vendor critical patch updates |
-- Views: hide sensitive columns and rows
CREATE VIEW v_emp_public AS
SELECT emp_id, name, dept_id FROM employees;
GRANT SELECT ON v_emp_public TO hr_clerk;
-- Authentication hardening: lockout and password lifetime
CREATE PROFILE secure_profile LIMIT
FAILED_LOGIN_ATTEMPTS 5
PASSWORD_LIFE_TIME 90;
ALTER USER asha PROFILE secure_profile;
-- Auditing
AUDIT SELECT, UPDATE ON account BY ACCESS;
Pick the control by the goal: C → encryption, views, GRANT; I → constraints, triggers, hashing, audit; A → backups, replication, patching, DoS protection. Combining several layers is called defence in depth.
For “recommend a countermeasure” questions, name the threat, the vulnerability it exploits and the control, e.g. SQL injection exploits unvalidated input concatenated into SQL; use bind variables and least privilege.
Confusing authentication with authorization, or thinking encryption alone solves everything. An authenticated, authorized insider can still leak data, so auditing is essential.
A report user can read the salary column although they only need names. Which principle was violated and how do you fix it?
Show answer
Least privilege. Revoke table access and grant SELECT on a view that omits salary.
PL/SQL Programming
The unit title includes PL/SQL Programming. PL/SQL is Oracle’s procedural extension to SQL: it lets you run logic, loops, error handling and reusable code inside the database, next to the data.
6.1 Block structure
Every PL/SQL program is made of blocks. A block has up to four sections: DECLARE (optional), BEGIN (mandatory), EXCEPTION (optional) and END. Blocks can be nested and can be anonymous or named (procedures, functions, triggers, packages).
flowchart TD
S(["Block starts"]):::g --> D["DECLARE optional: variables, constants, cursors, user exceptions"]
D --> B["BEGIN mandatory: executable SQL and PL/SQL statements"]
B --> Q{"Runtime error raised?"}
Q -- No --> E(["END"]):::g
Q -- Yes --> X["EXCEPTION optional: matching WHEN handler runs"]:::y
X --> E
DECLARE
v_name employees.name%TYPE;
c_bonus CONSTANT NUMBER(4,2) := 0.10;
BEGIN
SELECT name INTO v_name FROM employees WHERE emp_id = 101;
DBMS_OUTPUT.PUT_LINE('Hello ' || v_name);
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('No such employee');
END;
/
6.2 Variables, constants and data types
| Item | Syntax / meaning |
|---|---|
| Variable | v_count NUMBER := 0; |
| Constant | c_rate CONSTANT NUMBER := 0.18; (value fixed, must be initialised) |
| Scalar types | NUMBER, PLS_INTEGER, VARCHAR2, CHAR, DATE, TIMESTAMP, BOOLEAN, CLOB, RAW |
%TYPE | Copies the data type of a column or variable: v_sal employees.salary%TYPE; |
%ROWTYPE | A record shaped like a table row or cursor row: r employees%ROWTYPE; then use r.name |
%TYPE and %ROWTYPE make code adapt automatically when column definitions change, which is valuable when you normalize or restructure tables.
DECLARE
r employees%ROWTYPE;
BEGIN
SELECT * INTO r FROM employees WHERE emp_id = 101;
DBMS_OUTPUT.PUT_LINE(r.name || ' earns ' || r.salary);
END;
/
6.3 Control structures
DECLARE
v_sal NUMBER := 52000;
i PLS_INTEGER := 1;
v_tot NUMBER := 0;
BEGIN
-- IF / ELSIF / ELSE
IF v_sal >= 80000 THEN
DBMS_OUTPUT.PUT_LINE('Grade A');
ELSIF v_sal >= 50000 THEN
DBMS_OUTPUT.PUT_LINE('Grade B');
ELSE
DBMS_OUTPUT.PUT_LINE('Grade C');
END IF;
-- CASE
CASE
WHEN v_sal > 60000 THEN DBMS_OUTPUT.PUT_LINE('High');
WHEN v_sal > 30000 THEN DBMS_OUTPUT.PUT_LINE('Medium');
ELSE DBMS_OUTPUT.PUT_LINE('Low');
END CASE;
-- Basic LOOP with EXIT WHEN
LOOP
v_tot := v_tot + i;
i := i + 1;
EXIT WHEN i > 5;
END LOOP;
-- WHILE loop
WHILE i > 0 LOOP
i := i - 2;
END LOOP;
-- Numeric FOR loop (counter is implicit)
FOR k IN 1..3 LOOP
DBMS_OUTPUT.PUT_LINE('k = ' || k);
END LOOP;
END;
/
6.4 Cursors
A cursor is a pointer to the private work area that holds the rows returned by a query. Implicit cursors are created automatically for every DML statement and single-row SELECT INTO; their attributes are read as SQL%FOUND, SQL%NOTFOUND, SQL%ROWCOUNT. Explicit cursors are declared by you to process multi-row results one row at a time.
sequenceDiagram
participant P as PL/SQL program
participant C as Cursor
participant DB as Database
P->>C: DECLARE CURSOR c IS SELECT ...
P->>C: OPEN c
C->>DB: Parse and execute the query
DB-->>C: Active set of rows
loop until c%NOTFOUND
P->>C: FETCH c INTO variables
C-->>P: Next row
end
P->>C: CLOSE c
C-->>P: Resources released
Implicit and explicit cursor examples
-- Implicit cursor attribute after DML
BEGIN
UPDATE employees SET salary = salary * 1.05 WHERE dept_id = 10;
DBMS_OUTPUT.PUT_LINE(SQL%ROWCOUNT || ' rows updated');
END;
/
-- Explicit cursor: DECLARE, OPEN, FETCH, CLOSE
DECLARE
CURSOR c_emp(p_dept NUMBER) IS
SELECT emp_id, name, salary FROM employees WHERE dept_id = p_dept;
r c_emp%ROWTYPE;
BEGIN
OPEN c_emp(10);
LOOP
FETCH c_emp INTO r;
EXIT WHEN c_emp%NOTFOUND;
DBMS_OUTPUT.PUT_LINE(r.name || ': ' || r.salary);
END LOOP;
CLOSE c_emp;
END;
/
-- Cursor FOR loop: opens, fetches and closes automatically
BEGIN
FOR r IN (SELECT name, salary FROM employees WHERE dept_id = 10) LOOP
DBMS_OUTPUT.PUT_LINE(r.name || ': ' || r.salary);
END LOOP;
END;
/
Forgetting CLOSE leaks cursor resources, and checking %NOTFOUND after processing the row instead of right after FETCH makes the last row run twice.
6.5 Stored procedures and functions
A procedure performs an action and may return values through OUT parameters. A function must RETURN one value and can be used inside SQL. Both are stored in the database, compiled once and reused; they also give a controlled access path for security.
CREATE OR REPLACE PROCEDURE raise_salary (
p_emp_id IN employees.emp_id%TYPE,
p_pct IN NUMBER
) AS
BEGIN
UPDATE employees
SET salary = salary * (1 + p_pct / 100)
WHERE emp_id = p_emp_id;
IF SQL%ROWCOUNT = 0 THEN
RAISE_APPLICATION_ERROR(-20001, 'Employee not found');
END IF;
END raise_salary;
/
CREATE OR REPLACE FUNCTION get_balance (p_acct IN NUMBER)
RETURN NUMBER IS
v_bal account.balance%TYPE;
BEGIN
SELECT balance INTO v_bal FROM account WHERE account_id = p_acct;
RETURN v_bal;
EXCEPTION
WHEN NO_DATA_FOUND THEN RETURN NULL;
END get_balance;
/
-- Calling them
BEGIN raise_salary(101, 5); END;
/
SELECT get_balance(5001) FROM dual;
| Procedure | Function | |
|---|---|---|
| Returns | Nothing (or via OUT) | Exactly one value (RETURN) |
| Call from SQL | No | Yes (if side-effect free) |
| Typical use | Actions: transfer, update | Computations: tax, balance |
6.6 Triggers
A trigger is a stored block that Oracle runs automatically when a specified event occurs (DML, DDL or system events). Options: BEFORE/AFTER/INSTEAD OF, FOR EACH ROW (row-level, with :OLD and :NEW) or statement-level.
flowchart TD
A["DML statement: INSERT, UPDATE or DELETE"]:::g --> B["BEFORE STATEMENT trigger runs once"]
B --> C["For each affected row: BEFORE ROW trigger"]
C --> D["Row change applied"]
D --> E["AFTER ROW trigger"]
E --> F{"More rows?"}
F -- Yes --> C
F -- No --> G["AFTER STATEMENT trigger"]
G --> H{"Any error raised?"}
H -- No --> I["Statement completes, commit possible"]:::g
H -- Yes --> J["Statement rolled back with error"]:::y
-- Business rule: salary may never decrease
CREATE OR REPLACE TRIGGER trg_no_salary_cut
BEFORE UPDATE OF salary ON employees
FOR EACH ROW
BEGIN
IF :NEW.salary < :OLD.salary THEN
RAISE_APPLICATION_ERROR(-20002, 'Salary cannot be reduced');
END IF;
END;
/
-- Keep a denormalized summary column consistent
CREATE OR REPLACE TRIGGER trg_order_total
AFTER INSERT OR DELETE ON order_item
FOR EACH ROW
BEGIN
IF INSERTING THEN
UPDATE orders SET total = total + :NEW.qty * :NEW.price WHERE order_id = :NEW.order_id;
ELSIF DELETING THEN
UPDATE orders SET total = total - :OLD.qty * :OLD.price WHERE order_id = :OLD.order_id;
END IF;
END;
/
6.7 Exception handling
When a runtime error occurs, control jumps to the EXCEPTION section. Predefined exceptions have names; user-defined ones are declared and raised by you.
| Predefined exception | Raised when |
|---|---|
NO_DATA_FOUND | SELECT INTO returns no row |
TOO_MANY_ROWS | SELECT INTO returns more than one row |
ZERO_DIVIDE | Division by zero |
DUP_VAL_ON_INDEX | Duplicate value violates a unique index or primary key |
VALUE_ERROR | Conversion or size error |
INVALID_CURSOR | Illegal cursor operation |
OTHERS | Catch-all handler, always last |
flowchart TD
A["A statement raises an exception"]:::y --> B{"Handler for it in the current block?"}
B -- Yes --> C["Run handler, block ends normally"]:::g
B -- No --> D{"Enclosing block exists?"}
D -- Yes --> E["Propagate exception to the enclosing block"]
E --> B
D -- No --> F["Unhandled: error returned to the caller"]:::y
C --> G["Execution continues after the block"]
DECLARE
e_low_balance EXCEPTION; -- user-defined
v_bal NUMBER;
BEGIN
SELECT balance INTO v_bal FROM account WHERE account_id = 5001;
IF v_bal < 500 THEN
RAISE e_low_balance;
END IF;
DBMS_OUTPUT.PUT_LINE('OK');
EXCEPTION
WHEN NO_DATA_FOUND THEN
DBMS_OUTPUT.PUT_LINE('Account does not exist');
WHEN e_low_balance THEN
DBMS_OUTPUT.PUT_LINE('Minimum balance violated');
WHEN OTHERS THEN
DBMS_OUTPUT.PUT_LINE('Error ' || SQLCODE || ': ' || SQLERRM);
RAISE; -- re-raise to the caller
END;
/
Use RAISE_APPLICATION_ERROR(-20000 … -20999, 'message') to send custom errors to client applications, and PRAGMA EXCEPTION_INIT to give a name to an Oracle error number.
6.8 Packages
A package groups related procedures, functions, variables, cursors and types. It has a specification (public interface) and a body (implementation, plus private items). Benefits: modularity, information hiding, load-once performance and a single object to GRANT EXECUTE on.
CREATE OR REPLACE PACKAGE emp_pkg AS
PROCEDURE hire (p_name VARCHAR2, p_salary NUMBER);
FUNCTION dept_total (p_dept NUMBER) RETURN NUMBER;
END emp_pkg;
/
CREATE OR REPLACE PACKAGE BODY emp_pkg AS
FUNCTION valid_salary (p_salary NUMBER) RETURN BOOLEAN IS -- private
BEGIN
RETURN p_salary BETWEEN 10000 AND 500000;
END;
PROCEDURE hire (p_name VARCHAR2, p_salary NUMBER) IS
BEGIN
IF NOT valid_salary(p_salary) THEN
RAISE_APPLICATION_ERROR(-20003, 'Invalid salary');
END IF;
INSERT INTO employees(emp_id, name, salary)
VALUES (emp_seq.NEXTVAL, p_name, p_salary);
END;
FUNCTION dept_total (p_dept NUMBER) RETURN NUMBER IS
v_total NUMBER;
BEGIN
SELECT NVL(SUM(salary), 0) INTO v_total FROM employees WHERE dept_id = p_dept;
RETURN v_total;
END;
END emp_pkg;
/
6.9 How PL/SQL connects to security, constraints and normalized design
Security
- Users get
EXECUTEon procedures, not rights on tables (controlled access path). - Bind variables defeat SQL injection.
- Audit triggers record who changed what.
AUTHID DEFINER(default) runs with the owner’s rights;AUTHID CURRENT_USERuses the caller’s.
Constraints and integrity
- Triggers enforce rules that declarative constraints cannot (for example, an FD that was not preserved by decomposition).
- Exceptions give safe, atomic transactions (rollback on failure).
- Prefer PRIMARY KEY, FOREIGN KEY, CHECK first; use PL/SQL for the rest.
Normalized schema design
%TYPE/%ROWTYPEfollow column changes after restructuring.- Packages hide the multi-table joins that normalization creates.
- Triggers keep deliberate denormalized copies consistent.
Availability
- Stored code reduces network round trips.
- Bulk operations (
BULK COLLECT,FORALL) reduce load. - Scheduled PL/SQL jobs can refresh summaries and purge audit data.
Remember: procedure = action, function = value, trigger = automatic event reaction, package = spec + body, cursor = row-by-row query processing. Always list the four block sections in order.
Which attribute tells you how many rows the last UPDATE changed?
Show answer
SQL%ROWCOUNT (implicit cursor attribute).
Banking Application: Efficient and Secure Schema
Unit outcome 5: design efficient and secure relational schemas. We design a retail bank database, normalize it, secure it and automate rules with PL/SQL.
erDiagram
CUSTOMER ||--o{ ACCOUNT : holds
BRANCH ||--o{ ACCOUNT : hosts
BRANCH ||--o{ EMPLOYEE : employs
ACCOUNT ||--o{ ACCT_TRANSACTION : records
EMPLOYEE ||--o{ ACCT_TRANSACTION : processes
CUSTOMER {
int customer_id PK
varchar full_name
varchar email
varchar phone
raw national_id_enc
}
BRANCH {
int branch_id PK
varchar branch_name
varchar city
}
ACCOUNT {
int account_id PK
int customer_id FK
int branch_id FK
varchar account_type
number balance
varchar status
}
EMPLOYEE {
int emp_id PK
int branch_id FK
varchar name
varchar role
}
ACCT_TRANSACTION {
int txn_id PK
int account_id FK
int processed_by FK
varchar txn_type
number amount
timestamp txn_time
}
7.1 Normalization steps
A first draft stores everything in one table, BankFlat, key TxnID:
BankFlat(TxnID, AccountNo, CustName, CustEmail, CustPhones, BranchID,
BranchName, BranchCity, AccType, Balance, TxnType, TxnAmount, TxnTime)
Business rules: TxnID → AccountNo, TxnType, TxnAmount, TxnTime; AccountNo → CustName, CustEmail, BranchID, AccType, Balance; BranchID → BranchName, BranchCity.
| Step | Problem found | Fix |
|---|---|---|
| 1NF | CustPhones holds several numbers | Separate row or table per phone (or keep one primary phone, as in the ER diagram) |
| 2NF | Key is the single column TxnID, so there are no partial dependencies | Already 2NF |
| 3NF | TxnID → AccountNo → CustName / Balance (transitive) and AccountNo → BranchID → BranchName | Split into Transaction, Account, Customer, Branch |
| BCNF | Every determinant (TxnID, AccountNo, BranchID, CustomerID) is a key of its table | Already BCNF |
| 4NF / 5NF | No independent multi-valued facts or cyclic join rules in a single table | Satisfied |
The decomposition is lossless (each split table shares a foreign key that is the key of its parent) and dependency preserving (each FD is contained in one table).
CREATE TABLE branch (
branch_id NUMBER PRIMARY KEY,
branch_name VARCHAR2(60) NOT NULL,
city VARCHAR2(40) NOT NULL
);
CREATE TABLE customer (
customer_id NUMBER PRIMARY KEY,
full_name VARCHAR2(80) NOT NULL,
email VARCHAR2(100) NOT NULL UNIQUE,
phone VARCHAR2(15),
national_id_enc RAW(256) -- stored encrypted
);
CREATE TABLE employee (
emp_id NUMBER PRIMARY KEY,
branch_id NUMBER NOT NULL REFERENCES branch(branch_id),
name VARCHAR2(80) NOT NULL,
role VARCHAR2(20) CHECK (role IN ('TELLER','MANAGER','AUDITOR'))
);
CREATE TABLE account (
account_id NUMBER PRIMARY KEY,
customer_id NUMBER NOT NULL REFERENCES customer(customer_id),
branch_id NUMBER NOT NULL REFERENCES branch(branch_id),
account_type VARCHAR2(10) CHECK (account_type IN ('SAVINGS','CURRENT')),
balance NUMBER(14,2) DEFAULT 0 CHECK (balance >= 0),
status VARCHAR2(10) DEFAULT 'ACTIVE' CHECK (status IN ('ACTIVE','FROZEN','CLOSED'))
);
CREATE TABLE acct_transaction (
txn_id NUMBER PRIMARY KEY,
account_id NUMBER NOT NULL REFERENCES account(account_id),
processed_by NUMBER REFERENCES employee(emp_id),
txn_type VARCHAR2(6) CHECK (txn_type IN ('DEBIT','CREDIT')),
amount NUMBER(14,2) NOT NULL CHECK (amount > 0),
txn_time TIMESTAMP DEFAULT SYSTIMESTAMP
);
CREATE TABLE audit_log (
audit_id NUMBER PRIMARY KEY,
table_name VARCHAR2(30),
operation VARCHAR2(10),
changed_by VARCHAR2(60),
changed_at TIMESTAMP,
old_value VARCHAR2(200),
new_value VARCHAR2(200)
);
CREATE SEQUENCE audit_seq;
7.2 Security controls
| Concern | Control in this design | CIA goal |
|---|---|---|
| Who may log in | Profiles with lockout and password lifetime, MFA at the application | C |
| Who may do what | RBAC: teller, manager, auditor roles, privileges only via roles | C, I |
| Sensitive columns | National ID encrypted; tellers see a view without it | C |
| Money correctness | CHECK constraints, FKs, procedure with transaction and exceptions | I |
| Tamper evidence | Audit trigger writes to audit_log; auditors have SELECT only, nobody can UPDATE/DELETE | I |
| Injection | Only bind variables in PL/SQL; users have EXECUTE on procedures only | C, I |
| Recovery | Encrypted scheduled backups, tested restore, patched servers | A |
CREATE ROLE teller_role;
CREATE ROLE manager_role;
CREATE ROLE auditor_role;
-- Tellers see customers through a view without the national ID
CREATE VIEW v_customer_public AS
SELECT customer_id, full_name, email, phone FROM customer;
GRANT SELECT ON v_customer_public TO teller_role;
GRANT SELECT ON account TO teller_role;
-- Money moves only through the procedure
GRANT EXECUTE ON transfer_funds TO teller_role;
GRANT SELECT ON audit_log TO auditor_role;
GRANT teller_role TO manager_role; -- role hierarchy
GRANT manager_role TO meena;
GRANT teller_role TO asha;
GRANT auditor_role TO ravi;
7.3 PL/SQL for secure money transfer
CREATE OR REPLACE PROCEDURE transfer_funds (
p_from IN account.account_id%TYPE,
p_to IN account.account_id%TYPE,
p_amount IN NUMBER
) AS
v_balance account.balance%TYPE;
BEGIN
IF p_amount IS NULL OR p_amount <= 0 THEN
RAISE_APPLICATION_ERROR(-20011, 'Amount must be positive');
END IF;
-- Lock the source row so that concurrent transfers cannot overdraw it
SELECT balance INTO v_balance
FROM account WHERE account_id = p_from AND status = 'ACTIVE' FOR UPDATE;
IF v_balance < p_amount THEN
RAISE_APPLICATION_ERROR(-20010, 'Insufficient funds');
END IF;
UPDATE account SET balance = balance - p_amount WHERE account_id = p_from;
UPDATE account SET balance = balance + p_amount WHERE account_id = p_to;
INSERT INTO acct_transaction (txn_id, account_id, txn_type, amount)
VALUES (txn_seq.NEXTVAL, p_from, 'DEBIT', p_amount);
INSERT INTO acct_transaction (txn_id, account_id, txn_type, amount)
VALUES (txn_seq.NEXTVAL, p_to, 'CREDIT', p_amount);
COMMIT;
EXCEPTION
WHEN NO_DATA_FOUND THEN
ROLLBACK;
RAISE_APPLICATION_ERROR(-20012, 'Source account not found or not active');
WHEN OTHERS THEN
ROLLBACK;
RAISE;
END transfer_funds;
/
7.4 Audit and access-control triggers
-- Audit every balance/status change and every delete on ACCOUNT
CREATE OR REPLACE TRIGGER trg_account_audit
AFTER UPDATE OF balance, status OR DELETE ON account
FOR EACH ROW
BEGIN
INSERT INTO audit_log
(audit_id, table_name, operation, changed_by, changed_at, old_value, new_value)
VALUES
(audit_seq.NEXTVAL, 'ACCOUNT',
CASE WHEN DELETING THEN 'DELETE' ELSE 'UPDATE' END,
SYS_CONTEXT('USERENV', 'SESSION_USER'), SYSTIMESTAMP,
:OLD.balance || '/' || :OLD.status,
CASE WHEN DELETING THEN NULL ELSE :NEW.balance || '/' || :NEW.status END);
END;
/
-- Access control: block account changes outside business hours
CREATE OR REPLACE TRIGGER trg_block_offhours
BEFORE UPDATE OR DELETE ON account
BEGIN
IF TO_CHAR(SYSDATE, 'HH24') NOT BETWEEN '09' AND '18' THEN
RAISE_APPLICATION_ERROR(-20020, 'Account changes allowed only 09:00 to 18:00');
END IF;
END;
/
3NF/BCNF tables avoid redundancy and anomalies; primary and foreign keys create indexes for fast joins; a summary of daily totals could be a materialized view (a deliberate, documented denormalization) if reporting becomes slow.
For “design a secure schema” questions, structure the answer as: entities and keys → FDs → normal form reached → constraints → roles and views → audit and triggers → backup and encryption.
Everything on one page
FD rules
Armstrong’s axioms
Reflexivity: Y ⊆ X ⇒ X → Y
Augmentation: X → Y ⇒ XZ → YZ
Transitivity: X → Y, Y → Z ⇒ X → Z
Derived rules
Union: X → Y, X → Z ⇒ X → YZ
Decomposition: X → YZ ⇒ X → Y
Pseudotransitivity: X → Y, WY → Z ⇒ WX → Z
Closure and key steps
- result = X; repeat: if A → B and A ⊆ result then result ∪= B; stop when stable.
- Attributes never on a right side are in every key; test their closure first.
- Add attributes until closure = R; keep minimal sets; prime = attributes in any key.
- Canonical cover: split RHS → drop extraneous LHS attributes → drop redundant FDs.
Normal forms
| NF | One-line test |
|---|---|
| 1NF | Atomic values only |
| 2NF | No partial dependency on a composite key |
| 3NF | For X → A: X superkey or A prime |
| BCNF | For X → Y: X superkey |
| 4NF | For non-trivial X →→ Y: X superkey |
| 5NF | Every join dependency implied by candidate keys |
Decomposition tests
| Property | Test |
|---|---|
| Lossless (2 tables) | R1 ∩ R2 → R1 − R2 or R1 ∩ R2 → R2 − R1 |
| Lossless (n tables) | Chase tableau: some row becomes all distinguished |
| Dependency preserving | For each X → Y: iterate result ∪= (result ∩ Ri)⁺ ∩ Ri; need Y ⊆ result |
| Guarantees | 3NF synthesis: lossless + preserving; BCNF: lossless only |
Anomalies
Insertion: cannot add a fact alone. Deletion: removing a fact loses another. Update: many rows to change, inconsistency risk.
Security: threat → countermeasure
| Threat | Countermeasure |
|---|---|
| SQL injection | Bind variables, validation, least privilege |
| Privilege escalation | Least privilege, patching, role review |
| Insider threat | Separation of duties, auditing, views |
| Malware / ransomware | Offline backups, segmentation, endpoint protection |
| Social engineering | Training and MFA |
| Weak authentication | Strong policy, MFA, lockout |
| Misconfiguration | Hardening, remove defaults, firewall |
| Data leakage | Views, masking, encryption |
| Backup exposure | Encrypt and restrict backups |
AuthN = who you are. AuthZ = what you may do. DAC owner decides, MAC labels decide, RBAC roles decide.
PL/SQL syntax quick reference
DECLARE -- variables, cursors, exceptions
v x.col%TYPE; r x%ROWTYPE; CURSOR c IS SELECT ...;
BEGIN -- logic
IF cond THEN ... ELSIF cond THEN ... ELSE ... END IF;
CASE WHEN cond THEN ... ELSE ... END CASE;
LOOP ... EXIT WHEN cond; END LOOP;
WHILE cond LOOP ... END LOOP;
FOR i IN 1..n LOOP ... END LOOP;
OPEN c; FETCH c INTO r; EXIT WHEN c%NOTFOUND; CLOSE c;
EXCEPTION
WHEN NO_DATA_FOUND THEN ...
WHEN OTHERS THEN ...
END;
/
-- CREATE [OR REPLACE] PROCEDURE p (a IN NUMBER) AS BEGIN ... END;
-- CREATE [OR REPLACE] FUNCTION f (a IN NUMBER) RETURN NUMBER IS BEGIN ... RETURN x; END;
-- CREATE [OR REPLACE] TRIGGER t BEFORE|AFTER INSERT OR UPDATE ON tbl FOR EACH ROW BEGIN ... END;
-- CREATE [OR REPLACE] PACKAGE pkg AS ... END; / PACKAGE BODY pkg AS ... END;
Test yourself
Twenty questions (17 multiple choice and 3 short answer) covering all five learning outcomes: functional dependencies, closure and keys, normalization, decomposition, security and PL/SQL. You get instant feedback and a final score.