Skip to main content

Database Normalization and Indexing

Learning Objectives

By the end of this page, you should be able to:

  • Identify functional dependencies in a table and explain how they cause insertion, update, and deletion anomalies.
  • Normalize an unnormalized table step by step through 1NF, 2NF, 3NF, and BCNF, showing the table structure at each stage.
  • Explain when deliberately denormalizing a schema is a legitimate engineering trade-off rather than a mistake.
  • Describe how B-tree and hash indexes work internally and when each is the better choice.
  • Distinguish clustered from non-clustered indexes and composite indexes from single-column indexes.
  • Explain why indexes speed up reads but slow down writes, and what a covering index is.
  • Reason about how normalization and indexing pull a schema in opposite directions and how to balance them.

Quick Answer

Normalization is the process of structuring database tables so that each fact is stored exactly once, based on the functional dependencies between columns — this prevents insertion, update, and deletion anomalies that occur when the same piece of data is duplicated across rows. It proceeds through a series of normal forms (1NF, 2NF, 3NF, BCNF), each removing a specific kind of redundancy by splitting tables apart. Indexing solves a different, related problem: once your data is well-structured, how do you find rows quickly without scanning the entire table? An index is an auxiliary data structure (usually a B-tree, sometimes a hash table) built on one or more columns that lets the database jump directly to matching rows. The two techniques interact: normalization splits data across more tables, which means more joins, and indexes are what make those joins fast — but indexes themselves cost storage and slow down writes, so neither technique is "free."

Functional Dependencies: The Foundation of Normalization

Before you can normalize anything, you need a precise way to say "this column determines that column." That's a functional dependency (FD): given a table, column (or set of columns) A functionally determines column B — written A → B — if knowing the value of A always tells you the value of B. For example, in a student table, Student_ID → Student_Name — one student ID always corresponds to exactly one name.

Functional dependencies are not something you invent; they're facts about your data's real-world meaning. Roll_Number → Student_Name holds because no student has two names. But Student_Name → Student_ID does not hold, because two different students could share the same name. Normalization is entirely about detecting these dependencies and organizing tables so that non-key data depends on the whole key, and nothing else.

Anomalies: Why Unnormalized Data Causes Problems

Consider a single, unnormalized table tracking student course enrollments at a college:

| Student_ID | Student_Name | Course_ID | Course_Name | Instructor | Instructor_Office |
|------------|---------------|-----------|--------------|-------------|--------------------|
| S01 | Asha Rao | C101 | Databases | Dr. Mehta | Room 204 |
| S01 | Asha Rao | C102 | Networks | Dr. Iyer | Room 310 |
| S02 | Rahul Jain | C101 | Databases | Dr. Mehta | Room 204 |

This single table produces three distinct kinds of anomalies:

  • Insertion anomaly: You cannot record that a new course, say C103 — Operating Systems, exists unless at least one student has already enrolled in it, because Course_ID and Course_Name only appear as part of an enrollment row. The course shouldn't need a student to exist.
  • Update anomaly: If Dr. Mehta moves to Room 401, you must update every row where Instructor = Dr. Mehta. Miss even one row and the table now contradicts itself about where Dr. Mehta sits — there is no longer a single source of truth.
  • Deletion anomaly: If Rahul Jain drops course C101 and his is the only row referencing that course, deleting his enrollment silently deletes all record that course C101 (and its instructor's office) ever existed.

All three anomalies exist because the table mixes together facts about students, facts about courses, and facts about instructors into one place, even though those facts have different functional dependencies. Normalization's entire job is teasing them apart.

First Normal Form (1NF): Atomic Values, No Repeating Groups

A table is in 1NF if every column holds a single, indivisible (atomic) value, and there are no repeating groups of columns.

Before (violates 1NF) — an order table storing multiple products in one cell:

| Order_ID | Customer_Name | Products |
|----------|----------------|----------------------|
| 001 | John Doe | Laptop, Mouse |
| 002 | Jane Smith | Keyboard, Monitor |

The Products column packs multiple values into a single cell, which makes it impossible to query "how many orders contain a Mouse?" without parsing strings. It also can't be indexed meaningfully.

After (1NF) — one product per row:

| Order_ID | Customer_Name | Product |
|----------|----------------|-----------|
| 001 | John Doe | Laptop |
| 001 | John Doe | Mouse |
| 002 | Jane Smith | Keyboard |
| 002 | Jane Smith | Monitor |
CREATE TABLE order_items_1nf (
order_id INT,
customer_name VARCHAR(100),
product VARCHAR(100),
PRIMARY KEY (order_id, product)
);

This is now queryable and indexable, but it still has a problem: Customer_Name is repeated for every product a customer orders, and the primary key is now the composite (Order_ID, Product) — which sets up the anomaly that 2NF fixes.

Second Normal Form (2NF): Full Functional Dependency on the Key

A table is in 2NF if it's in 1NF and every non-key column depends on the entire primary key, not just part of it. This only matters when you have a composite primary key — if your key is a single column, 1NF-compliant tables are automatically in 2NF.

In the 1NF table above, the primary key is (Order_ID, Product). But Customer_Name depends only on Order_ID, not on Product — that's a partial dependency, and it's exactly what causes redundancy (John Doe's name is stored twice).

Fix: split into two tables so each non-key column depends on its whole key.

CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_name VARCHAR(100)
);

CREATE TABLE order_items (
order_id INT,
product VARCHAR(100),
PRIMARY KEY (order_id, product),
FOREIGN KEY (order_id) REFERENCES orders(order_id)
);
-- orders
| Order_ID | Customer_Name |
|----------|----------------|
| 001 | John Doe |
| 002 | Jane Smith |

-- order_items
| Order_ID | Product |
|----------|-----------|
| 001 | Laptop |
| 001 | Mouse |
| 002 | Keyboard |
| 002 | Monitor |

Now Customer_Name is stored exactly once per order, regardless of how many products that order contains.

Third Normal Form (3NF): No Transitive Dependencies

A table is in 3NF if it's in 2NF and no non-key column depends on another non-key column (a transitive dependency). In other words, every non-key attribute must depend on the key, the whole key, and nothing but the key.

Suppose the orders table above grows to include customer address details:

Before (violates 3NF):

| Order_ID | Customer_ID | Customer_Name | Customer_Address |
|----------|-------------|-----------------|--------------------|
| 001 | C1 | John Doe | 123 Main St |
| 002 | C2 | Jane Smith | 456 Elm St |
| 003 | C1 | John Doe | 123 Main St |

Here, Order_ID → Customer_ID (fine, that's the key relationship), but Customer_ID → Customer_Name and Customer_ID → Customer_Address — both non-key columns depend on another non-key column (Customer_ID), not directly on Order_ID. This is a transitive dependency, and it's why John Doe's address is duplicated across every order he places. Update his address in one row and miss another, and you again have contradictory data.

Fix: pull customer details into their own table.

CREATE TABLE customers (
customer_id INT PRIMARY KEY,
name VARCHAR(100),
address VARCHAR(200)
);

CREATE TABLE orders_3nf (
order_id INT PRIMARY KEY,
customer_id INT,
order_date DATE,
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);

Now a customer's name and address live in exactly one row, referenced by ID from as many orders as needed.

Boyce-Codd Normal Form (BCNF): The Stricter Version of 3NF

BCNF fixes an edge case 3NF misses: it requires that for every functional dependency A → B in the table, A must be a candidate key (a column or column-set that could uniquely identify a row) — not just "part of a key" or "a non-key column that happens to determine something."

Classic example — a table of course sections where each instructor teaches in only one fixed room, but a room can host multiple courses:

| Course_ID | Instructor | Room |
|-----------|-------------|----------|
| C101 | Dr. Mehta | Room 204 |
| C102 | Dr. Iyer | Room 310 |
| C103 | Dr. Mehta | Room 204 |

Assume (Course_ID, Instructor) is the candidate key (a course can theoretically be co-taught), but in practice Instructor → Room also holds — every instructor is assigned exactly one room. Instructor is not a candidate key by itself, yet it determines Room. That's a BCNF violation: if Dr. Mehta moves offices, you must update every row where he appears, and if his last course row is deleted, the room assignment fact disappears with it — the same anomalies as before, just in a subtler place.

Fix: separate the instructor-to-room fact from the course-to-instructor fact.

CREATE TABLE instructor_rooms (
instructor VARCHAR(100) PRIMARY KEY,
room VARCHAR(20)
);

CREATE TABLE course_sections (
course_id VARCHAR(10),
instructor VARCHAR(100),
PRIMARY KEY (course_id, instructor),
FOREIGN KEY (instructor) REFERENCES instructor_rooms(instructor)
);

In practice, most real-world schemas stop at 3NF — full BCNF compliance is checked mainly in academic exam settings, because true BCNF violations that survive 3NF are relatively rare and sometimes only fixable by giving up a functional dependency altogether (a trade-off most systems don't bother making).

Denormalization: Deliberately Breaking the Rules

Normalization isn't a moral obligation — it's a tool for eliminating redundancy and anomalies, and sometimes the cost of that tool (extra joins on every read) outweighs its benefit. Denormalization means intentionally reintroducing redundancy to speed up reads, usually after profiling shows joins are the bottleneck.

A common case: an e-commerce order history page that needs to display the customer's name next to every order, read thousands of times per second, while orders are written far less often.

-- Fully normalized: requires a join on every read
SELECT o.order_id, c.name, o.order_date
FROM orders o
JOIN customers c ON o.customer_id = c.customer_id;

-- Denormalized: customer_name duplicated onto orders for fast reads
CREATE TABLE orders_denormalized (
order_id INT PRIMARY KEY,
customer_id INT,
customer_name VARCHAR(100), -- duplicated from customers table
order_date DATE
);

This reintroduces the update anomaly 3NF was designed to prevent — if a customer changes their name, every historical order row needs updating (or you accept that historical orders show the name as of that order, which is sometimes exactly the desired behavior for an audit trail). Denormalization is a legitimate choice when: reads vastly outnumber writes, the duplicated data rarely changes, and the join cost is measurably hurting performance — not as a default starting point.

Indexing: Finding Rows Without Scanning Everything

An index is a separate data structure, stored alongside a table, that maps values in one or more columns to the physical location of the rows containing them — similar to a book's index mapping a keyword to page numbers instead of forcing you to read every page. Without an index, a query like WHERE last_name = 'Doe' forces the database to perform a full table scan, checking every single row.

CREATE TABLE employees (
employee_id INT PRIMARY KEY,
first_name VARCHAR(50),
last_name VARCHAR(50),
department_id INT
);

CREATE INDEX idx_last_name ON employees (last_name);

With this index in place, SELECT * FROM employees WHERE last_name = 'Doe' no longer scans the whole table — it looks up 'Doe' in the index structure and jumps straight to the matching rows.

B-Tree Indexes: The Default Workhorse

Most relational databases (PostgreSQL, MySQL/InnoDB, SQL Server) use a B-tree (specifically a B+tree) as the default index structure. It's a balanced, sorted tree where each node holds multiple keys and pointers, keeping the tree shallow even for millions of rows — a lookup, insert, or range scan all take O(log n) disk/page accesses.

The key reason B-trees dominate: because keys are kept in sorted order, a B-tree index supports both equality lookups (last_name = 'Doe') and range queries (order_date BETWEEN '2024-01-01' AND '2024-06-30', salary > 50000) efficiently, plus ORDER BY and BETWEEN can often be satisfied without a separate sort step.

Hash Indexes: Fast Equality, No Ranges

A hash index applies a hash function to the indexed column's value to compute a bucket location directly — giving O(1) average-case lookup for exact-match queries. PostgreSQL supports CREATE INDEX ... USING HASH; many key-value and in-memory stores use hashing as their primary index structure.

CREATE INDEX idx_session_token ON sessions USING HASH (session_token);

The trade-off: hash indexes cannot support range queries or sorting, because similar values don't hash to nearby buckets — session_token = 'abc123' works, but session_token > 'abc' gets no benefit at all. Use a hash index only when your workload is exclusively exact-match lookups on that column, such as a session-token or API-key cache; otherwise a B-tree's added flexibility is almost always worth it.

Clustered vs. Non-Clustered Indexes

A clustered index determines the physical order in which rows are stored on disk — there can be only one per table, because a table's rows can only be physically sorted one way. The primary key is usually the clustered index by default in engines like SQL Server and MySQL/InnoDB.

-- In InnoDB, the PRIMARY KEY is the clustered index by default
CREATE TABLE orders (
order_id INT PRIMARY KEY, -- rows physically stored in order_id order
order_date DATE,
total DECIMAL(10,2)
);

A non-clustered index is a separate structure that stores the indexed column's values plus a pointer (or the clustered key) back to the actual row, leaving the table's physical order untouched. You can have many non-clustered indexes on one table.

CREATE NONCLUSTERED INDEX idx_order_date ON orders (order_date);

The practical difference: reading via the clustered index is slightly faster because there's no extra pointer hop to the row — the index is the table's storage. Reading via a non-clustered index requires one extra lookup step (unless it's a covering index, see below).

Composite Indexes

A composite index spans multiple columns, and column order matters enormously — the index is only efficient for queries that filter on a prefix of the indexed columns, left to right.

CREATE INDEX idx_orders_date_customer ON orders (order_date, customer_id);

This index helps WHERE order_date = '2024-09-01' and WHERE order_date = '2024-09-01' AND customer_id = 42, but it does not meaningfully help a query that filters only on customer_id — that's like trying to use a phone book (sorted by last name, then first name) to look someone up by first name alone. If both query patterns are common, you may need two separate indexes, or a composite index with the columns in the other order.

When Indexes Help vs. Hurt

Indexes are not free. Every INSERT, UPDATE, or DELETE must also update every index on the affected columns, and each index consumes disk space.

  • Indexes help read-heavy workloads: dashboards, reporting queries, lookups by a specific column, JOIN conditions, and ORDER BY/WHERE clauses on the indexed columns.
  • Indexes hurt write-heavy workloads: a table with ten indexes that receives thousands of inserts per second pays the cost of updating all ten structures on every single write, which can dominate total write latency. Bulk-loading systems often drop indexes before a big load and rebuild them afterward for exactly this reason.
  • Indexing every column "just in case" is a common overcorrection — each unused index is pure write overhead and storage cost with no query ever benefiting from it.

Covering Indexes

A covering index includes every column a query needs, so the database can answer the query entirely from the index itself without touching the underlying table (an "index-only scan").

-- Query only needs customer_id and order_date
SELECT customer_id, order_date FROM orders WHERE order_date > '2024-01-01';

-- This index alone can answer it, with no lookup into the orders table
CREATE INDEX idx_covering_orders ON orders (order_date, customer_id);

Covering indexes are one of the highest-leverage tuning techniques available, precisely because they eliminate the extra "jump to the row" step that ordinary non-clustered indexes require — but they only pay off for queries whose exact column set you can predict in advance.

How Normalization and Indexing Trade Off Against Each Other

Normalization and indexing pull in related but distinct directions. Normalization reduces redundancy by splitting data across more tables, which necessarily means more JOINs to reassemble a full picture — and joins are exactly what indexes on foreign-key columns are built to make cheap. A well-normalized schema without indexes on its join columns will feel slow; the same schema with the right indexes performs well and keeps data consistent. Denormalization is the alternative lever: instead of indexing your way out of expensive joins, you avoid the joins altogether by duplicating data, accepting the redundancy risk in exchange for fewer moving parts on read. Neither choice is "correct" in isolation — the right schema design depends on your read/write ratio, consistency requirements, and how often the duplicated or joined data actually changes.

Key Terms

TermDefinition
Functional DependencyA relationship A → B meaning the value of A always determines the value of B.
AnomalyAn inconsistency (insertion, update, or deletion) caused by redundant, unnormalized data.
NormalizationThe process of organizing tables based on functional dependencies to eliminate redundancy and anomalies.
1NFRequires atomic column values and no repeating groups.
2NF1NF plus no partial dependency of non-key columns on part of a composite key.
3NF2NF plus no transitive dependency of one non-key column on another.
BCNFA stricter 3NF: every determinant in the table must be a candidate key.
DenormalizationIntentionally reintroducing redundancy to reduce joins and speed up reads.
IndexAn auxiliary data structure that speeds up data lookup at the cost of extra storage and write overhead.
B-tree IndexA balanced, sorted tree structure supporting fast equality and range lookups; the default index type in most RDBMSs.
Hash IndexAn index using a hash function for O(1) average equality lookups, but no range query support.
Clustered IndexAn index that determines the physical storage order of a table's rows; at most one per table.
Non-Clustered IndexA separate index structure pointing back to rows, without affecting physical storage order.
Composite IndexAn index built on multiple columns, efficient only for queries filtering on a left-to-right prefix of those columns.
Covering IndexAn index that contains every column a query needs, avoiding a lookup into the base table.

Common Mistakes

Misconception 1: "More normal forms are always better, so every schema should be pushed to BCNF or beyond." Why it's wrong: Each additional normal form typically means splitting a table further, which means more joins at query time. Beyond 3NF, the anomalies being fixed become increasingly rare in practice, while the join cost is very real and immediate. Correct understanding: Most production schemas target 3NF as a solid default and only push further, or denormalize backward, based on measured anomaly risk and query performance — not as a checklist to maximize.

Misconception 2: "Adding an index can only make queries faster, never slower." Why it's wrong: Indexes speed up reads that use them, but every index must be updated on every INSERT, UPDATE, or DELETE that touches its columns, and it consumes disk space. A table with many unused or rarely-used indexes can have noticeably slower writes for no read benefit. Correct understanding: Indexes are a read/write trade-off. Add them for columns actually used in WHERE, JOIN, or ORDER BY clauses on performance-critical queries — not defensively on every column.

Misconception 3: "Denormalization is just bad database design that beginners fall into." Why it's wrong: Denormalization done accidentally, without understanding the redundancy it introduces, is a mistake. But denormalization done deliberately — after normalizing first, then reintroducing specific redundancy to solve a measured read-latency problem — is a standard, respected technique used in high-traffic systems (caching layers, reporting tables, read replicas with flattened schemas). Correct understanding: The difference between "bad design" and "denormalization" is whether the redundancy was an informed trade-off made after understanding the normalized form, or an accident of not understanding functional dependencies in the first place.

Comparison and Connections

1NF vs. 2NF vs. 3NF vs. BCNF

Normal FormRequirementAnomaly It RemovesTypical Trigger
1NFAtomic values, no repeating groupsInability to query/index multi-valued cellsA column stores a comma-separated list
2NF1NF + no partial dependency on part of a composite keyRedundancy from data that only depends on part of the keyComposite primary key with a column depending on just one part
3NF2NF + no transitive dependency between non-key columnsRedundancy from one non-key fact depending on another non-key factA non-key column determines another non-key column
BCNF3NF + every determinant is a candidate keySubtler anomalies 3NF misses, from non-candidate-key determinantsAn attribute that isn't part of any candidate key still determines another attribute

Clustered vs. Non-Clustered Index

AspectClustered IndexNon-Clustered Index
Count per tableExactly one (or zero)Many
Physical row orderDetermines itDoes not affect it
StorageThe index is the table's dataSeparate structure with pointers back to rows
Lookup speedSlightly faster (no extra hop)One extra hop, unless it's a covering index
Typical usePrimary key or most range-queried columnAny other frequently filtered/joined column

Practice Questions

Recall

  1. What does it mean for a table to be in First Normal Form (1NF)? Answer guidance: Every column holds a single atomic value, and there are no repeating groups of columns — e.g., no comma-separated lists in a single cell.
  2. Name the three types of anomalies that unnormalized tables can produce. Answer guidance: Insertion anomalies (can't add a fact without an unrelated fact also being present), update anomalies (a fact must be changed in multiple places, risking inconsistency), and deletion anomalies (deleting one fact accidentally erases another).

Understanding

  1. Explain why a composite index on (order_date, customer_id) does not help a query that filters only on customer_id. Answer guidance: Composite indexes are usable only via a left-to-right prefix of their columns; without a condition on order_date, the database can't narrow down where in the index to look, similar to searching a phone book by first name alone.
  2. Why does moving from 2NF to 3NF specifically target transitive dependencies rather than partial dependencies? Answer guidance: Partial dependencies (2NF's concern) only exist when there's a composite key; transitive dependencies (3NF's concern) exist when one non-key column determines another non-key column, regardless of whether the key is composite.

Application

  1. A products table stores Product_ID, Product_Name, Category_ID, and Category_Name, where Category_ID → Category_Name. Identify the normalization violation and show the fix. Answer guidance: This is a transitive dependency (3NF violation) — Category_Name depends on Category_ID, a non-key column, not directly on Product_ID. Fix: split into a products table (Product_ID, Product_Name, Category_ID) and a categories table (Category_ID, Category_Name).
  2. A reporting dashboard runs SELECT customer_id, SUM(total) FROM orders WHERE order_date > '2024-01-01' GROUP BY customer_id thousands of times per hour, and the orders table receives only a few hundred writes per day. What index would you create, and why? Answer guidance: A composite (covering) index on (order_date, customer_id, total) — it lets the database answer the query using an index-only scan without touching the base table, and since writes are infrequent, the extra index-maintenance cost on writes is negligible compared to the read savings.

Analysis

  1. A social media table storing Post_ID, Author_ID, Author_Name, and Post_Text denormalizes Author_Name onto every post for fast read performance. Analyze the trade-off being made and when it stops being worth it. Answer guidance: The trade-off gains fast reads (no join needed to show the author's name on each post) at the cost of an update anomaly (a name change requires updating every post by that author). It stops being worth it if authors change their display name frequently relative to how often posts are read, or if strict consistency of displayed names matters more than read latency.
  2. Compare the cost of adding a new non-clustered index to a table that receives 10 reads per write versus a table that receives 10 writes per read. Which table benefits more, and why? Answer guidance: The 10-reads-per-write table benefits more — the index's per-write maintenance cost is amortized over far more reads that gain a speedup. The 10-writes-per-read table pays the index's maintenance cost on every write far more often than any query benefits from it, making the index more likely to be a net loss.

FAQ

Q: Do I always need to normalize all the way to 3NF? A: Not always, but 3NF is a reasonable default target for most transactional schemas because it eliminates the most damaging anomalies (update and deletion anomalies from transitive dependencies) while keeping join complexity manageable. Going further (BCNF, 4NF) is situational.

Q: Does adding a PRIMARY KEY automatically create an index? A: Yes, in virtually every major RDBMS, defining a primary key automatically creates a unique index on it (often the clustered index), because the database needs fast lookups to enforce uniqueness.

Q: Can a table have more than one clustered index? A: No. A clustered index determines the physical storage order of the table's rows, and a table's rows can only be physically ordered one way at a time. You can, however, have many non-clustered indexes.

Q: If indexes make reads faster, why not index every column? A: Every index adds storage overhead and must be updated on every write that touches its columns. Indexing columns that are rarely queried, or that change extremely often, adds write cost with little to no read benefit.

Q: Is denormalization the same as skipping normalization entirely? A: No. Denormalization is a deliberate step taken after understanding the normalized structure and its functional dependencies — you accept specific, known redundancy for a specific performance reason. Skipping normalization entirely means never identifying those dependencies in the first place, which leaves you blind to the anomalies you're risking.

Q: Why can't a hash index support range queries like > or BETWEEN? A: A hash function scatters similar values into unrelated buckets by design, so there's no way to know which buckets to examine for "everything greater than X" without checking all of them — defeating the purpose of the index. A B-tree, which keeps keys in sorted order, can walk directly to the starting point of a range.

Quick Revision

  • Functional dependency A → B means A's value always determines B's value — this is the basis for every normal form.
  • Unnormalized tables cause insertion, update, and deletion anomalies because the same fact is duplicated or entangled with unrelated facts.
  • 1NF: atomic values, no repeating groups.
  • 2NF: 1NF + no non-key column depends on only part of a composite key (no partial dependency).
  • 3NF: 2NF + no non-key column depends on another non-key column (no transitive dependency).
  • BCNF: 3NF + every determinant must be a candidate key (stricter, catches rarer edge cases).
  • Denormalization deliberately reintroduces redundancy to cut down on joins, when reads vastly outnumber writes on rarely-changing duplicated data.
  • An index is an auxiliary structure (usually a B-tree) that avoids full table scans on lookups.
  • B-trees support both equality and range queries; hash indexes support only fast equality lookups.
  • Clustered index = physical row order, one per table; non-clustered index = separate structure, many per table.
  • Composite indexes only help queries filtering on a left-to-right prefix of their columns.
  • Every index speeds up matching reads but slows down writes on that table — indexing is a trade-off, not a free upgrade.
  • A covering index contains every column a query needs, letting the database skip the base table entirely.

Prerequisites

  • Relational Database Model (tables, keys, foreign keys)
  • Basic SQL: CREATE TABLE, SELECT, JOIN

Related Topics

  • Entity-Relationship (ER) Modeling
  • Query Optimization and Execution Plans
  • Transactions and ACID Properties

Next Topics

  • SQL Query Optimization
  • NoSQL Data Modeling and Denormalization Patterns
  • Database Performance Tuning