Denormalise specific bottlenecks. Premature denormalisation creates maintenance burden that is far More expensive than the joins it eliminates.
Entity-Relationship (ER) diagrams are the standard notation for conceptual database design. They Model entities (things), attributes (properties), and relationships (associations between entities).
CUSTOMER ||--o{ ORDER : "places"
ORDER ||--|{ ORDER_ITEM : "contains"
PRODUCT ||--o{ ORDER_ITEM : "referenced_in"
Notation Meaning 1:1 Each entity relates to exactly one other 1:N One entity relates to many others M:N Many entities relate to many others 0..1:1..N Optional/mandatory cardinalities
Strong entities become tables with their attributes as columnsWeak entities become tables that include the primary key of the identifying (owner) entity1:1 relationships: add the foreign key to either table (prefer the table where NULL is less common)1:N relationships: add the foreign key to the “N” sideM:N relationships: create an association table with composite primary keyRelational theory is like the rules of chess. The pieces (tables, rows, columns) have specific roles, and the rules (relational algebra) define how they can move. Understanding the theory does not mean memorizing formulas; it means understanding why the pieces move the way they do.
Codd’s rules are like the principles of good governance. They define what a relational database should guarantee: logical data independence, referential integrity, and consistent views. A database that follows these rules is like a government that follows its constitution: predictable, reliable, and trustworthy.
A natural key like email address or national ID number may seem appealing, but it creates problems When the value changes (requiring cascading updates across all referencing tables) or when the Domain is not truly unique (two people share an email during a migration). Surrogate keys (BIGSERIAL``UUID) are stable, never change, and simplify joins.
Splitting tables too aggressively (e.g., creating a separate table for every attribute “just in Case”) makes even simple queries require joins. If a group of attributes always appears together and Is always updated together, they likely belong in the same table.
If you find yourself writing application logic like “when the department changes, look up the new Department name and update this record,” you have a functional dependency that belongs in the Schema. Model it as a foreign key relationship.
NULL means “unknown” or “not applicable.” It is not the same as '' (empty string) or 0. In SQL, NULL = NULL is NULL (not TRUE), and NULL + 1 is NULL. This three-valued logic causes Subtle bugs in WHERE clauses, JOIN conditions, and CHECK constraints.
Some teams omit foreign key constraints for “performance reasons.” The cost of a foreign key check On insert/update is negligible compared to the cost of orphaned rows, inconsistent data, and the Debugging time required to find them. Only omit foreign keys if you have a proven, measured reason And a compensating data integrity mechanism.
SELECT * breaks when columns are added, reordered, or renamed. It transfers unnecessary data over The network. It prevents the query planner from using covering indexes. Always list the columns you Need explicitly.
Storing comma-separated values, JSON arrays, or space-delimited lists in a single column violates 1NF and makes queries unreliable:
article_id INTEGER PRIMARY KEY ,
tags VARCHAR ( 500 ) -- "database,performance,sql"
-- Querying requires string manipulation (fragile, slow, cannot use indexes):
SELECT * FROM articles WHERE tags LIKE ' %database% ' ;
-- Also matches "non-database", "database-admin", etc.
-- Correct: junction table
CREATE TABLE article_tags (
article_id INTEGER REFERENCES articles(article_id),
tag VARCHAR ( 50 ) NOT NULL ,
PRIMARY KEY (article_id, tag)
CREATE INDEX idx_article_tags_tag ON article_tags(tag);
SELECT a. * FROM articles a
JOIN article_tags at ON a . article_id = at . article_id
WHERE at . tag = ' database ' ;
When designing schemas, developers often guess which functional dependencies exist rather than Computing them systematically. This leads to incorrect normal forms and redundant relationships. Always compute the attribute closure for candidate keys and verify the minimal cover before Declaring a schema to be in a given normal form.
Two tables with foreign keys referencing each other create a circular dependency that complicates Inserts, deletes, and schema migrations:
emp_id INTEGER PRIMARY KEY ,
dept_id INTEGER REFERENCES departments(dept_id)
CREATE TABLE departments (
dept_id INTEGER PRIMARY KEY ,
manager_id INTEGER REFERENCES employees(emp_id)
-- Inserting requires DEFERRABLE constraints or inserting NULLs first
-- Dropping either table requires CASCADE
Break circular dependencies by making one side DEFERRABLE, using a NULL placeholder for the initial Insert, or redesigning the schema to eliminate the cycle.
Relational calculus is an alternative to relational algebra for expressing queries. It is declarative (describes what to retrieve, not how) and comes in two forms:
Tuple relational calculus: variables range over tuples.
\{ t \mid \exists s \in \mathrm{Employee(s[\mathrm{dept] = t[\mathrm{dept] \land s[\mathrm{salary] \gt 100000) \}
Domain relational calculus: variables range over attribute values (domains).
\{ \lt \mathrm{name, \mathrm{salary \gt \mid \exists d, s (\mathrm{Employee(d, \mathrm{name, s) \land s \gt 100000) \}
Codd’s theorem (1972) proves that relational algebra and relational calculus are equivalent in Expressive power: every query expressible in one is expressible in the other. SQL is based on Relational algebra with some relational calculus influences.
The universal relation assumption states that all attributes have globally unique names and that any Attribute can be related to any other. This assumption underlies many visual query tools and some ORM systems, but it does not hold in practice. Attributes named id``nameOr type appear in Many tables with completely different semantics.
Implication: always qualify column names with their table (or alias) in queries, and use descriptive Names that reflect the domain (e.g., customer_id instead of id).
A decomposition of relation R R R into R 1 R_1 R 1 and R 2 R_2 R 2 is lossless if R 1 ⋈ R 2 = R R_1 \bowtie R_2 = R R 1 ⋈ R 2 = R (no Information is lost). A decomposition is lossless if and only if:
R_1 \cap R_2 \rightarrow R_1 \mathrm{ or R_1 \cap R_2 \rightarrow R_2
That is, the common attributes form a superkey for at least one of the decomposed relations.
R(A, B, C) with FDs: {A → B, B → C}
Decompose into R1(A, B) and R2(B, C):
B → C (from the FDs), so B is a superkey for R2? No, R2 needs {B, C} and B → C, so B is a
superkey for R2. Lossless decomposition. ✓
Decompose into R1(A, B) and R1(A, C):
A → B (from the FDs), so A is a superkey for R1. Lossless decomposition. ✓
A decomposition is dependency-preserving if every functional dependency in the original set can Be checked by examining only the decomposed relations (without joining them back together).
Not every lossless decomposition is dependency-preserving, and not every dependency-preserving Decomposition is lossless. The goal is to achieve both.
R(A, B, C) with FDs: {A → B, B → C}
Decompose into R1(A, B) and R1(A, C):
Lossless? Yes (A → B, so A is a superkey for R1).
B → C: NOT checkable on either R1 or R2 alone ✗
This decomposition loses the dependency B → C.
When a BCNF decomposition is not dependency-preserving, you have a choice: stay in 3NF (which always Has a dependency-preserving, lossless decomposition) or accept the non-dependency-preserving BCNF Decomposition and enforce the lost dependency through application logic or triggers.
A join dependency (JD) is a generalisation of a multivalued dependency. A relation R R R satisfies a Join dependency J D ( R 1 , R 2 , … , R n ) JD(R_1, R_2, \ldots, R_n) J D ( R 1 , R 2 , … , R n ) if R R R is equal to the join of its projections on R 1 , R 2 , … , R n R_1, R_2, \ldots, R_n R 1 , R 2 , … , R n :
R = R 1 ⋈ R 2 ⋈ … ⋈ R n R = R_1 \bowtie R_2 \bowtie \ldots \bowtie R_n R = R 1 ⋈ R 2 ⋈ … ⋈ R n
5NF states that every non-trivial join dependency must be implied by candidate keys. When this Condition is violated, the relation encodes a constraint that is not captured by functional or Multivalued dependencies.
Classic 5NF example: supplier-part-project
R(supplier, part, project)
Business rule: if a supplier supplies a part and works on a project, then that supplier
supplies that part for that project. This is a ternary relationship that cannot be decomposed
into binary relationships without losing the constraint.
Supplier S supplies Part P and works on Project J:
(S, P, J) must exist if (S, P) and (S, J) both exist.
Decomposing into SP(S, P), SJ(S, J), PJ(P, J) loses the ternary constraint.
The join SP ⋈ SJ ⋈ PJ may contain rows not in the original relation.
5NF violation: the constraint requires all three components together.
The following workflow applies normalisation systematically to a real-world schema:
1. Identify all attributes and their semantics (what does each column mean?)
2. Identify all candidate keys (compute attribute closures)
3. List all functional dependencies (from business rules and key analysis)
4. Compute the minimal cover (canonical form)
5. Decompose into 1NF: ensure atomic values
6. Decompose into 2NF: eliminate partial dependencies on composite keys
7. Decompose into 3NF: eliminate transitive dependencies
8. Verify BCNF: check that every determinant is a superkey
9. For each decomposition, verify lossless-join and dependency-preservation
10. Evaluate denormalisation for known performance bottlenecks
Schema: R(student_id, course_id, instructor_name, instructor_office, grade)
Step 1: Identify attributes
student_id -- identifies a student
course_id -- identifies a course
instructor_name -- name of the instructor teaching the course
instructor_office -- office of the instructor
grade -- grade the student received in the course
Step 2: Identify candidate keys
{student_id, course_id} -- a student takes a course once
Step 3: Functional dependencies
{student_id, course_id} → grade -- a student's grade for a course
course_id → instructor_name, instructor_office -- a course has one instructor
Step 4: 1NF? Yes, all attributes are atomic.
Step 5: 2NF? No partial dependencies:
instructor_name and instructor_office depend on course_id (a subset of the key).
They do NOT depend on student_id at all.
Step 6: Decompose into 2NF:
R1(student_id, course_id, grade) -- key: {student_id, course_id}
R2(course_id, instructor_name, instructor_office) -- key: {course_id}
Step 7: 3NF? Check transitive dependencies:
R1: {student_id, course_id} → grade. No transitive dependency. 3NF. ✓
R2: course_id → instructor_name → instructor_office? Only if one instructor
has one office. If so, this is a transitive dependency.
If instructor_name → instructor_office:
R2a(course_id, instructor_name)
R2b(instructor_name, instructor_office)
If instructor_office is not functionally dependent on instructor_name
(e.g., shared offices), R2 is already in 3NF.
Step 8: BCNF? All determinants are superkeys. ✓
This systematic approach prevents the common mistake of over-normalising (splitting tables that have No redundancy) or under-normalising (leaving transitive dependencies that cause update anomalies).
Worked examples demonstrating the application of key concepts are covered in the detailed sub-pages linked above.
<Citations sources={[ {title=“Database System Concepts”, author=“Silberschatz, Korth and Sudarshan”, year=“2019”, type=“book”}, {title=“An Introduction to Database Systems”, author=“Date”, year=“2003”, type=“book”}, ]} />
Normalization - How normal forms apply relational theory to eliminate redundancySQL - The query language that implements relational algebra and relational calculusData Modeling - How conceptual models map to the relational structures described here