DEV Community

HARSHITH GADDAM
HARSHITH GADDAM

Posted on

SQL and Database Fundamentals: From Tables to Transactions and Indexes

SQL and Database Fundamentals: From Tables to Transactions and Indexes

When I started learning databases, I realized that writing SQL queries
is only one part of working with a database. To work with data properly,
we also need to understand how tables are designed, how they are
connected, how duplicate data can be avoided, and how database
operations are kept safe.

In this blog, I am explaining the database concepts I have learned, with
simple examples and SQL queries.

We will cover:

  • Tables, primary keys, and foreign keys
  • Relationships between tables
  • SQL joins: implicit joins vs. explicit joins, INNER/LEFT/RIGHT/FULL
  • Aggregation functions
  • Normalization: 1NF, 2NF, 3NF, BCNF, 4NF, and 5NF
  • Transactions: BEGIN, COMMIT, and ROLLBACK
  • Primary, unique, and composite indexes
  • Understanding EXPLAIN

Let's start with the basics.

1. Tables: How Databases Store Data

A relational database stores data in tables. A table contains rows and
columns.

For example, imagine we are building an online shopping application.

We can create a table to store customer information.

CREATE TABLE customers (
    customer_id INT PRIMARY KEY,
    name VARCHAR(100),
    email VARCHAR(150)
);
Enter fullscreen mode Exit fullscreen mode

Now, let's insert some data.

INSERT INTO customers (customer_id, name, email)
VALUES
    (1, 'Rahul', 'rahul@example.com'),
    (2, 'Priya', 'priya@example.com'),
    (3, 'Arun', 'arun@example.com');
Enter fullscreen mode Exit fullscreen mode

The table now looks like this:

customer_id name email


1 Rahul rahul@example.com
2 Priya priya@example.com
3 Arun arun@example.com

Here:

  • Table: customers
  • Columns: customer_id, name, and email
  • Rows: Individual customer records

We can retrieve the data using:

SELECT * FROM customers;
Enter fullscreen mode Exit fullscreen mode

The * means we want to retrieve all columns.

What I understood: Tables organize related information into rows and
columns, making it easier to store, retrieve, and manage data.

2. Primary Keys and Foreign Keys

When a database contains many tables, we need a way to identify
individual records and connect related records.

This is where primary keys and foreign keys become important.

Primary Key (PK)

A primary key uniquely identifies each row in a table.

For example:

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100),
    price NUMERIC(10, 2)
);
Enter fullscreen mode Exit fullscreen mode

Here, product_id is the primary key.

It ensures that: - Every product has a unique ID. - The ID cannot be
NULL. - Two products cannot have the same ID.

For example, we cannot insert two products with product_id = 101.

A table has only one primary key constraint, but that key can contain
multiple columns. We will see this later.

Foreign Key (FK)

A foreign key connects one table to another by referencing a primary key
or another eligible unique key.

Let's create an orders table.

CREATE TABLE orders (
    order_id INT PRIMARY KEY,
    customer_id INT,
    order_date DATE,
    FOREIGN KEY (customer_id)
        REFERENCES customers(customer_id)
);
Enter fullscreen mode Exit fullscreen mode

Here, customer_id is a foreign key.

Suppose we insert an order for customer ID 1. It works because
customer 1 exists in the customers table.

However, an order for customer ID 99 will fail if customer 99 does
not exist.

The foreign key protects the relationship between the tables.

Important difference:


Primary key Foreign key


Identifies a row uniquely Connects related records

Cannot contain NULL Can contain NULL unless
restricted

Must be unique Can contain duplicate values

One primary key constraint per A table can have multiple foreign
table keys


What I understood: A primary key identifies a record, while a
foreign key helps maintain valid relationships between records.

3. Relationships Between Tables

In real applications, data is divided into multiple tables because
different types of information have different purposes.

There are three common types of relationships.

3.1 One-to-One (1:1)

One record in the first table is associated with at most one record in
the second table, and vice versa.

Example: A user and their profile.

CREATE TABLE user_profiles (
    profile_id INT PRIMARY KEY,
    customer_id INT UNIQUE
        REFERENCES customers(customer_id),
    bio TEXT
);
Enter fullscreen mode Exit fullscreen mode

The UNIQUE constraint on customer_id prevents the same customer from
having multiple profile records.

For example:

  • Customer 1 → Profile 101
  • Customer 2 → Profile 102

A customer can have no profile or one profile. The UNIQUE constraint
ensures that a customer cannot be linked to multiple profiles through
this column.

3.2 One-to-Many (1:N)

One record in the first table can be associated with many records in the
second table.

Example: One customer can place many orders.

customers
---------
customer_id: 1
name: Rahul

orders
------
order_id: 101, customer_id: 1
order_id: 102, customer_id: 1
order_id: 103, customer_id: 1
Enter fullscreen mode Exit fullscreen mode

Customer Rahul has placed three orders.

We implement this relationship by placing the foreign key in the
orders table.

customer_id INT REFERENCES customers(customer_id)
Enter fullscreen mode Exit fullscreen mode

The foreign key can repeat because many orders can belong to the same
customer.

3.3 Many-to-Many (N:N)

Many records in one table can be associated with many records in another
table.

Example: Students and courses.

  • One student can enroll in many courses.
  • One course can have many students.

We use a junction table to represent this relationship.

CREATE TABLE students (
    student_id INT PRIMARY KEY,
    student_name VARCHAR(100)
);

CREATE TABLE courses (
    course_id INT PRIMARY KEY,
    course_name VARCHAR(100)
);

CREATE TABLE enrollments (
    student_id INT REFERENCES students(student_id),
    course_id INT REFERENCES courses(course_id),
    PRIMARY KEY (student_id, course_id)
);
Enter fullscreen mode Exit fullscreen mode

The enrollments table connects students and courses.

student_id course_id


1 101
1 102
2 101

Student 1 is enrolled in two courses, while course 101 has two students.

The composite primary key prevents the same student from being enrolled
in the same course more than once.

What I understood: A 1:1 relationship often uses a foreign key with
a unique constraint, a 1:N relationship uses a foreign key on the many
side, and an N:N relationship uses a junction table.

4. SQL Joins: Implicit and Explicit Joins

We often store related information in separate tables. A JOIN lets us
retrieve related data from multiple tables in one query.

In SQL, explicit and implicit joins are two different syntactical styles used to combine data from two or more tables based on a related column. Both produce the exact same query result and execution plan in modern database engines, but they differ in readability, maintenance, and standards.

Let's use our customers and orders tables.

4.1 Implicit JOIN

An implicit join uses a comma to list tables in the FROM clause. The
matching condition is written in the WHERE clause.

SELECT
    customers.name,
    orders.order_id,
    orders.order_date
FROM customers, orders
WHERE customers.customer_id = orders.customer_id;
Enter fullscreen mode Exit fullscreen mode

The condition customers.customer_id = orders.customer_id tells SQL
which customer matches each order.

This query returns matching customer-order rows, similar to an
INNER JOIN.

What can go wrong? If we forget the matching condition, the database
combines every customer with every order. This is called a Cartesian
product. For example, 3 customers and 10 orders can produce 30 rows,
even when most of those pairs are unrelated.

Implicit join syntax is older and can become difficult to read when a
query uses several tables and conditions.

4.2 Explicit JOIN

An explicit join uses the JOIN keyword and puts the matching condition
next to the tables being joined.

SELECT
    customers.name,
    orders.order_id,
    orders.order_date
FROM customers
INNER JOIN orders
    ON customers.customer_id = orders.customer_id;
Enter fullscreen mode Exit fullscreen mode

This query produces the same matching rows as the previous implicit
join.

Here: - INNER JOIN says that we want matching rows. - ON defines how
the two tables are related.

Why do we prefer explicit JOINs?

Both forms can express an inner join, but explicit joins are usually
better because:

  1. They are easier to read. The relationship between tables is written directly beside the JOIN.
  2. They reduce mistakes. Join conditions are kept separate from other filters, so it is easier to notice a missing condition.
  3. They support outer joins clearly. LEFT JOIN, RIGHT JOIN, and FULL OUTER JOIN are expressed naturally with explicit syntax.
  4. They are easier to maintain. When a query contains several tables, the join logic is easier to follow.

Explicit joins do not automatically make a query faster. The database
optimizer may produce the same execution plan for equivalent queries.
The main benefits are clarity and safer query writing.

4.3 INNER JOIN

An INNER JOIN returns only rows that match in both tables.

SELECT customers.name, orders.order_id
FROM customers
INNER JOIN orders
    ON customers.customer_id = orders.customer_id;
Enter fullscreen mode Exit fullscreen mode

If Rahul has two orders and Priya has one order, the result contains
three rows. If Arun has no orders, Arun does not appear.

4.4 LEFT JOIN

A LEFT JOIN returns every row from the left table and matching rows
from the right table. If there is no match, the right-side columns
contain NULL.

SELECT customers.name, orders.order_id
FROM customers
LEFT JOIN orders
    ON customers.customer_id = orders.customer_id;
Enter fullscreen mode Exit fullscreen mode

Now Arun appears even if he has no orders. His order_id is NULL.

This is useful when we want to find all customers, including those who
have never placed an order.

4.5 RIGHT JOIN

A RIGHT JOIN returns every row from the right table and matching rows
from the left table.

SELECT customers.name, orders.order_id
FROM customers
RIGHT JOIN orders
    ON customers.customer_id = orders.customer_id;
Enter fullscreen mode Exit fullscreen mode

Every order appears, along with its matching customer information. In
this example, the foreign key means every non-null customer ID in
orders must refer to an existing customer.

A RIGHT JOIN can often be rewritten as a LEFT JOIN by switching the
table order.

4.6 FULL OUTER JOIN

A FULL OUTER JOIN returns matching rows, unmatched rows from the left
table, and unmatched rows from the right table.

SELECT customers.name, orders.order_id
FROM customers
FULL OUTER JOIN orders
    ON customers.customer_id = orders.customer_id;
Enter fullscreen mode Exit fullscreen mode

This is useful when we want to see all records from both tables, even
when a matching record does not exist.

Quick comparison


Join What it returns


Implicit join Older comma-separated syntax;
matching conditions are usually in
WHERE

INNER JOIN Only matching rows

LEFT JOIN All left rows and matching right
rows

RIGHT JOIN All right rows and matching left
rows

FULL OUTER JOIN All rows from both sides


What I understood: Implicit and explicit syntax can produce the same
result for an inner join, but explicit joins make the relationship
clearer and are easier to maintain. The type of join determines what
happens to rows that have no match.

5. Aggregation Functions

Sometimes, retrieving individual rows is not enough. We may want to
calculate totals, averages, or counts.

SQL provides aggregate functions for this purpose.

The most common ones are:

  • COUNT() --- counts rows or non-null values, depending on the expression.
  • SUM() --- calculates a total.
  • AVG() --- calculates an average.
  • MIN() --- finds the smallest value.
  • MAX() --- finds the largest value.

Suppose we have these orders:

order_id customer_id total_amount


101 1 500
102 1 300
103 2 700

Count the orders

SELECT COUNT(*) AS total_orders
FROM orders;
Enter fullscreen mode Exit fullscreen mode

Result: 3

Calculate total sales

SELECT SUM(total_amount) AS total_sales
FROM orders;
Enter fullscreen mode Exit fullscreen mode

This requires a total_amount column in the orders table. If we add
that column and use the values above, the result is 1500.

Calculate average order value

SELECT AVG(total_amount) AS average_order_value
FROM orders;
Enter fullscreen mode Exit fullscreen mode

The result is 500.

GROUP BY

GROUP BY groups rows with the same value so that we can calculate a
separate result for each group.

For example, we want to know how much each customer has spent.

SELECT
    customer_id,
    SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id;
Enter fullscreen mode Exit fullscreen mode

Result:

customer_id total_spent


1 800
2 700

Instead of calculating one total for all orders, SQL calculates a
separate total for each customer.

HAVING

HAVING filters groups after aggregation.

SELECT
    customer_id,
    SUM(total_amount) AS total_spent
FROM orders
GROUP BY customer_id
HAVING SUM(total_amount) > 600;
Enter fullscreen mode Exit fullscreen mode

This returns only customers whose total spending is greater than 600.

What I understood: Aggregate functions calculate summary values,
GROUP BY creates groups, and HAVING filters those groups based on
aggregate results.

6. Database Normalization: 1NF, 2NF, 3NF, BCNF, 4NF, and 5NF

Normalization is the process of organizing data into tables to reduce
unnecessary duplication and avoid data problems.

Imagine storing customer information and all their orders in one table.
Rahul's name may be repeated for every order. If his name changes, we
must update several rows. If we update only some of them, the data
becomes inconsistent.

Normalization helps us place each fact in the right table. Let's go
through the normal forms one by one.

6.1 First Normal Form (1NF)

A table is in First Normal Form when each cell contains a single value
rather than a list of values, and rows can be identified individually.

Consider this table:

student_id student_name courses


1 Rahul Java, SQL
2 Priya Python, Java

The courses column contains multiple values in one cell.

We can instead store one student-course combination per row:

student_id student_name course


1 Rahul Java
1 Rahul SQL
2 Priya Python
2 Priya Java

Now each cell contains one value. In a real application, we would
usually separate students and courses into their own tables and connect
them with an enrollment table.

Main idea: Each cell should contain one value, not a list of
separate values.

6.2 Second Normal Form (2NF)

A table is in 2NF when: 1. It is already in 1NF. 2. Every non-key column
depends on the whole of every candidate key, not just part of a
composite key.

Consider this table, where the primary key is (student_id, course_id):

student_id course_id student_name course_name


1 101 Rahul SQL
1 102 Rahul Java
2 101 Priya SQL

student_name depends only on student_id, and course_name depends
only on course_id. Neither depends on the full two-column key. These
are partial dependencies.

We can split the data into three tables:

Students

student_id student_name


1 Rahul
2 Priya

Courses

course_id course_name


101 SQL
102 Java

Enrollments

student_id course_id


1 101
1 102
2 101

Now student details belong in students, course details belong in
courses, and enrollment records use the combination of both IDs.

Main idea: If a candidate key contains multiple columns, non-key
facts should not depend on only part of that key.

6.3 Third Normal Form (3NF)

A table is in 3NF when it is in 2NF and non-key attributes do not depend
transitively on a candidate key. More precisely, for every non-trivial
functional dependency X → A, either X is a superkey or A is a
prime attribute (an attribute that belongs to at least one candidate
key).

Consider this table:

employee_id employee_name department_id department_name


1 Rahul 10 Engineering
2 Priya 10 Engineering
3 Arun 20 Marketing

Assume employee_id is the key and each department ID determines one
department name.

  • employee_id determines department_id.
  • department_id determines department_name.

So, the department name depends on the employee ID indirectly through
department_id. This is a transitive dependency.

We can split the data into:

Employees

employee_id employee_name department_id


1 Rahul 10
2 Priya 10
3 Arun 20

Departments

department_id department_name


10 Engineering
20 Marketing

Now the department name is stored once in the departments table.

Main idea: Avoid storing facts about one entity repeatedly in a
table mainly about another entity.

6.4 Boyce-Codd Normal Form (BCNF)

BCNF is a stronger version of 3NF. A table is in BCNF when, for every
non-trivial functional dependency X → Y, X must be a superkey.

In simple words, every column or group of columns that determines
another fact must be able to identify a row uniquely
.

Consider a table with these columns:

Student, Course, Instructor

Assume: - A student can study a course with an instructor. - Each
instructor teaches only one course. - A student can have more than one
instructor.

The important dependency is:

Instructor → Course

The instructor determines the course, but Instructor alone does not
identify a student-instructor row because many students can have the
same instructor. So the determinant is not a superkey, and the table
violates BCNF.

We can separate the facts into tables such as:

InstructorCourses

instructor course


I1 SQL
I2 Java

StudentInstructors

student instructor


Rahul I1
Priya I1
Rahul I2

This stores which course an instructor teaches separately from which
instructor teaches a student.

Main idea: In BCNF, every determinant must be a superkey. BCNF
handles some dependency problems that 3NF can still allow.

6.5 Fourth Normal Form (4NF)

4NF deals with a different problem: independent multi-valued facts in
the same table.

Imagine a student can have several hobbies and speak several languages.
Assume hobbies and languages are independent of each other.

A single table might look like this:

student hobby language


Rahul Cricket English
Rahul Cricket Telugu
Rahul Music English
Rahul Music Telugu

Every hobby is repeated for every language. Every language is repeated
for every hobby. If Rahul adds a hobby, we may need to add a row for
each language.

We can separate these independent facts into two tables.

StudentHobbies

student hobby


Rahul Cricket
Rahul Music

StudentLanguages

student language


Rahul English
Rahul Telugu

A table is in 4NF when it is in BCNF and has no non-trivial multivalued
dependency unless its determinant is a superkey.

Main idea: Keep independent multi-valued facts in separate tables
instead of storing every possible combination.

6.6 Fifth Normal Form (5NF)

5NF deals with join dependencies. It helps when a table can be split
into smaller tables and reconstructed exactly by joining them, without
creating incorrect combinations.

Imagine a business records which suppliers provide which parts for which
projects. The table has three columns:

Supplier, Part, Project

In some systems, the business rules may mean that a valid
supplier-part-project combination can be determined entirely from three
pairwise relationships:

  • Which parts a supplier can provide
  • Which projects a supplier works on
  • Which parts a project uses

If---and only if---the business rules guarantee that these pairwise
relationships are enough to determine every valid three-way combination,
we can store them in three tables:

  • SupplierParts(Supplier, Part)
  • SupplierProjects(Supplier, Project)
  • ProjectParts(Project, Part)

Joining the three tables can reconstruct the original combinations
without adding invalid combinations.

This decomposition is not safe for every supplier-part-project dataset.
It is valid only when the required join dependency holds.

A table is in 5NF (also called Project-Join Normal Form) when every
non-trivial join dependency is implied by its candidate keys.

Main idea: 5NF handles cases where a table can be split into smaller
relationship tables and joined back together without losing information
or creating extra rows.

Quick comparison of normal forms


Normal form Main idea


1NF Each cell contains a single value

2NF No partial dependency on part of a
candidate key

3NF No disallowed transitive dependency
of non-key attributes

BCNF Every determinant in a non-trivial
functional dependency is a superkey

4NF No non-trivial multivalued
dependency unless its determinant
is a superkey

5NF No non-trivial join dependency
remains except those implied by
candidate keys


In practice, 1NF through 3NF are common starting points for relational
database design. BCNF, 4NF, and 5NF address more specific dependency
problems. The right design depends on the real business rules; splitting
tables without understanding those rules can cause incorrect results
when the tables are joined again.

What I understood: Normalization is about understanding which facts
depend on which keys or relationships, then organizing tables so that
each fact is stored in the right place with less unnecessary repetition.

7. Transactions: BEGIN, COMMIT, and ROLLBACK

A transaction is a group of database operations treated as one unit of
work.

Consider a bank transfer.

Rahul transfers ₹500 to Priya.

Two operations are required:

  1. Subtract ₹500 from Rahul's account.
  2. Add ₹500 to Priya's account.

What happens if the first operation succeeds but the second fails?

Money would be deducted from Rahul without being added to Priya.

A transaction helps prevent this partial update.

BEGIN

BEGIN starts a transaction.

COMMIT

COMMIT saves the transaction's changes permanently.

ROLLBACK

ROLLBACK cancels the uncommitted changes made by the current
transaction.

Let's implement the bank transfer.

Assume an accounts table with account_id and balance columns
already exists.

BEGIN;

UPDATE accounts
SET balance = balance - 500
WHERE account_id = 1;

UPDATE accounts
SET balance = balance + 500
WHERE account_id = 2;

COMMIT;
Enter fullscreen mode Exit fullscreen mode

If both operations succeed and the transaction commits, the transfer is
saved.

Now suppose we detect an error before committing.

BEGIN;

UPDATE accounts
SET balance = balance - 500
WHERE account_id = 1;

-- An error is detected before the transfer is completed.

ROLLBACK;
Enter fullscreen mode Exit fullscreen mode

The uncommitted balance change is undone.

In a real banking system, we would also validate the account IDs, check
that the sender has enough money, and handle failures correctly. A
database transaction alone does not automatically enforce all business
rules.

Important: ROLLBACK cannot normally undo a transaction after it
has been committed.

ACID properties

Transactions are commonly explained using ACID.

  • Atomicity: Either all required changes happen, or none do.
  • Consistency: The transaction preserves the database's defined rules and constraints.
  • Isolation: Concurrent transactions are controlled so they do not interfere in unacceptable ways.
  • Durability: Once committed, changes survive normal failures covered by the database's durability guarantees.

What I understood: Transactions protect related database operations
from being saved only partially. BEGIN starts the work, COMMIT saves
it, and ROLLBACK cancels uncommitted changes.

8. Indexes: Primary, Unique, and Composite

An index is a data structure that helps a database find rows without
scanning the entire table in many situations.

Think of a book's index. Instead of reading every page to find a topic,
you can use the index to locate the relevant pages.

Database indexes work in a similar way.

However, indexes also require storage and can make INSERT, UPDATE,
and DELETE operations more expensive because the indexes may need to
be updated.

8.1 Primary Key Index

When we create a primary key, PostgreSQL automatically creates a unique
B-tree index to enforce it, unless the constraint is implemented using
an already-existing suitable index.

CREATE TABLE products (
    product_id INT PRIMARY KEY,
    product_name VARCHAR(100)
);
Enter fullscreen mode Exit fullscreen mode

PostgreSQL uses the primary key's unique index to enforce uniqueness and
help find products by ID.

We do not need to create another index on product_id for the same
purpose.

8.2 Unique Index

A unique index prevents duplicate indexed key values.

CREATE UNIQUE INDEX idx_customers_email
ON customers(email);
Enter fullscreen mode Exit fullscreen mode

Now, two customers cannot have the same non-null email.

In PostgreSQL, a normal unique index allows multiple NULL values by
default because NULL values are treated as distinct for uniqueness.
Different options can change this behavior.

Difference: A primary key is a table constraint that requires
unique, non-null values. A unique index enforces uniqueness, but it does
not by itself make the column NOT NULL.

A table can have only one primary key constraint, but it can have
multiple unique constraints or unique indexes.

8.3 Composite Index

A composite index contains more than one column.

Suppose we frequently retrieve a customer's orders, sorted by date.

CREATE INDEX idx_orders_customer_date
ON orders(customer_id, order_date);
Enter fullscreen mode Exit fullscreen mode

This index can help queries such as:

SELECT *
FROM orders
WHERE customer_id = 1
ORDER BY order_date;
Enter fullscreen mode Exit fullscreen mode

The order of columns matters.

An index on (customer_id, order_date) is generally most useful when a
query filters or sorts using the leading column, customer_id.

It can also support searches on both columns, but it is not generally as
useful for filtering only by order_date.

For example, a query that filters only by order_date may benefit more
from an index that starts with order_date.

What I understood: Indexes can speed up data retrieval, but we
should create them based on actual query patterns rather than indexing
every column.

9. EXPLAIN: Understanding How SQL Queries Run

We can write a correct SQL query, but that does not mean it will always
run efficiently.

For example:

SELECT *
FROM orders
WHERE customer_id = 1;
Enter fullscreen mode Exit fullscreen mode

If the orders table contains millions of rows, PostgreSQL needs a way
to find the matching orders.

It may choose to scan the whole table or use an index.

How can we understand which approach PostgreSQL chooses?

We can use EXPLAIN.

EXPLAIN
SELECT *
FROM orders
WHERE customer_id = 1;
Enter fullscreen mode Exit fullscreen mode

PostgreSQL displays the query plan it expects to use.

Two common plan operations are:

Sequential Scan

Seq Scan on orders
Enter fullscreen mode Exit fullscreen mode

A sequential scan means PostgreSQL reads the table's rows to find
matching records.

This is not always bad. If the table is small or the query needs a large
part of the table, scanning it can be the most efficient option.

Index Scan

Index Scan using idx_orders_customer_date on orders
Enter fullscreen mode Exit fullscreen mode

An index scan means PostgreSQL uses an index to locate relevant rows.

This can be faster when the query needs only a small portion of a large
table.

The actual plan depends on the table size, data distribution, available
indexes, and the query itself.

EXPLAIN ANALYZE

EXPLAIN shows the planned operations and estimated costs.

EXPLAIN ANALYZE actually executes the query and reports the operations
performed along with execution measurements.

EXPLAIN ANALYZE
SELECT *
FROM orders
WHERE customer_id = 1;
Enter fullscreen mode Exit fullscreen mode

It helps us compare estimated and actual row counts and execution time.

Be careful: EXPLAIN ANALYZE executes the query. For a SELECT, it
runs the read query; for an UPDATE or DELETE, it performs the
change. Use caution with write queries, and use a transaction with
ROLLBACK when appropriate for controlled testing.

What should we look at?

When reading a query plan, pay attention to:

  • Scan type: Is PostgreSQL using a sequential scan or an index?
  • Estimated rows vs. actual rows: Are the estimates close to reality?
  • Execution time: How long did the query take in this run?
  • Buffers: With EXPLAIN (ANALYZE, BUFFERS), how much data was read from or found in memory?

Do not assume that every sequential scan should be replaced with an
index scan. The goal is to understand why PostgreSQL selected a
particular plan.

What I understood: EXPLAIN helps us understand the planned
execution of a query, while EXPLAIN ANALYZE lets us compare that plan
with actual execution results.

10. Putting Everything Together

Let's connect these concepts to a real application.

Imagine we are developing an online shopping website.

We need to:

  1. Store customer and order information in separate tables.
  2. Use primary keys to identify records.
  3. Use foreign keys to connect orders to customers.
  4. Use joins to retrieve customer details with their orders.
  5. Use aggregation functions to calculate total sales.
  6. Normalize tables to avoid unnecessary duplication.
  7. Use transactions to ensure that related operations succeed or fail together.
  8. Create indexes for frequently used search conditions.
  9. Use EXPLAIN ANALYZE to investigate slow queries.

Each concept solves a different problem, but all of them work together
to make a database reliable and efficient.

For example, when a customer places an order, the application might
validate the request, insert the order and its items inside a
transaction, and update inventory. Foreign keys can protect
relationships, constraints can prevent invalid data, and indexes can
help the application find the necessary records efficiently.

That is why learning database design and query execution is just as
important as learning SQL syntax.

Conclusion

Learning these database concepts helped me understand that databases are
not simply places where we store data. We must also think about how the
data is connected, how it can be retrieved, how duplication can be
avoided, and how changes can be performed safely.

Here are the key lessons I took away:

  • Tables and keys help us organize and identify data.
  • Relationships and joins help us connect information across tables.
  • Aggregations help us summarize data.
  • Normalization (1NF, 2NF, 3NF, BCNF, 4NF, and 5NF) helps reduce unnecessary duplication and dependency problems.
  • Transactions protect related operations.
  • Indexes can improve query performance.
  • EXPLAIN helps us understand how a database executes queries.

I am continuing to build my understanding of SQL and database design by
practicing these concepts with real examples.

I hope this article helps other beginners understand these topics more
easily.

What about you? Which database concept took you the longest to
understand? Share your experience in the comments!

Top comments (0)