DEV Community

Cover image for What Actually Happens When You Run a Database Query?
Tanu Priya
Tanu Priya

Posted on

What Actually Happens When You Run a Database Query?

You write something that looks almost too simple:

SELECT *
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

The database returns a row, and from the application's perspective, the job is done.

But the database didn't simply "look through the table" and return the answer.

Behind that one query, the database may parse SQL, check permissions, build an execution plan, choose an index, read pages from memory or disk, filter rows, perform joins or sorting, and finally return the result to your application.

Understanding this process makes databases much easier to reason about.

It also explains why some queries take milliseconds while others take seconds.

Let's follow a query from the moment your application sends it until the result comes back.


1. The Application Sends a Query

Imagine your backend executes:

const result = await db.query(
  "SELECT * FROM users WHERE id = 42"
);
Enter fullscreen mode Exit fullscreen mode

The application sends the query to the database server.

The simplified flow is:

Application
     ↓
Database Connection
     ↓
SQL Query
     ↓
Database Server
Enter fullscreen mode Exit fullscreen mode

The database receives the SQL statement as text or through a database protocol.

At this point, the database still has to figure out what the query means and how it should execute it.


2. The Database Receives the Request

The database server receives the query through an existing or newly established connection.

In a production application, there is often a connection pool between your application and database.

Instead of creating a completely new database connection for every query, the application can reuse existing connections.

Conceptually:

Application
     ↓
Connection Pool
     ↓
Database
Enter fullscreen mode Exit fullscreen mode

This matters because creating and maintaining connections has overhead.

A connection pool allows multiple requests to efficiently share a controlled number of database connections.


3. The Database Parses the SQL

Now the database needs to understand the query.

Consider:

SELECT name
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

The database parses the SQL syntax and builds an internal representation of the statement.

It needs to understand things such as:

SELECT → What data do we want?
FROM   → Which table?
WHERE  → Which conditions?
Enter fullscreen mode Exit fullscreen mode

Conceptually:

SQL Query
   ↓
Parser
   ↓
Internal Representation
Enter fullscreen mode Exit fullscreen mode

If the SQL syntax is invalid, execution stops here.

For example:

SELEC name FROM users;
Enter fullscreen mode Exit fullscreen mode

would result in a syntax error.

The database cannot execute something it cannot understand.


4. The Database Checks Permissions

The database also needs to determine whether the requesting user is allowed to perform the operation.

For example, a database user might have permission to:

SELECT
Enter fullscreen mode Exit fullscreen mode

but not:

DELETE
Enter fullscreen mode Exit fullscreen mode

If the application attempts an operation it isn't authorized to perform, the database can reject the query.

So the flow is more like:

Query
  ↓
Parse
  ↓
Check Permissions
  ↓
Continue
Enter fullscreen mode Exit fullscreen mode

This is one reason database credentials and permissions should be carefully configured.

Your application should generally have only the database privileges it actually needs.


5. The Query Optimizer Enters the Picture

This is where things become interesting.

Suppose you run:

SELECT *
FROM users
WHERE email = 'alex@example.com';
Enter fullscreen mode Exit fullscreen mode

The database has multiple ways to execute this query.

It could scan every row:

User 1
User 2
User 3
User 4
...
User 1,000,000
Enter fullscreen mode Exit fullscreen mode

Or, if there is an index on email, it could use the index to find the relevant row much more efficiently.

The database needs to decide:

What is the most efficient way to execute this query?

That's the job of the query optimizer.


6. What Is a Query Execution Plan?

The optimizer creates an execution plan.

The plan describes how the database intends to execute the query.

For example:

Query
 ↓
Use index on email
 ↓
Find matching row
 ↓
Fetch row from table
 ↓
Return result
Enter fullscreen mode Exit fullscreen mode

Another query might produce:

Query
 ↓
Scan table
 ↓
Filter rows
 ↓
Sort results
 ↓
Return result
Enter fullscreen mode Exit fullscreen mode

The database chooses a plan based on information such as:

  • available indexes
  • estimated number of rows
  • filtering conditions
  • table statistics
  • join relationships
  • sorting requirements
  • database configuration

The exact optimizer behavior depends on the database engine.


7. Full Table Scan

Let's say you run:

SELECT *
FROM users
WHERE age = 25;
Enter fullscreen mode Exit fullscreen mode

If there is no useful index, the database may need to inspect many rows.

Conceptually:

Users Table

Row 1 → age 21
Row 2 → age 32
Row 3 → age 25 ✓
Row 4 → age 41
Row 5 → age 25 ✓
...
Enter fullscreen mode Exit fullscreen mode

The database checks rows to determine which ones satisfy:

age = 25
Enter fullscreen mode Exit fullscreen mode

This is often called a table scan or sequential scan, depending on the database.

For a small table, this may be completely fine.

For a table containing hundreds of millions of rows, scanning a huge amount of data can become expensive.


8. Indexes Change the Game

Now suppose you create:

CREATE INDEX idx_users_email
ON users(email);
Enter fullscreen mode Exit fullscreen mode

Then you execute:

SELECT *
FROM users
WHERE email = 'alex@example.com';
Enter fullscreen mode Exit fullscreen mode

The database may be able to use the index instead of scanning the entire table.

Conceptually:

Without Index

Query
 ↓
Scan many rows
 ↓
Find matching row
Enter fullscreen mode Exit fullscreen mode

With an appropriate index:

Query
 ↓
Index
 ↓
Locate matching value
 ↓
Fetch row
Enter fullscreen mode Exit fullscreen mode

This can dramatically reduce the amount of data the database needs to inspect.

But indexes aren't magic.

They also consume storage and add work to inserts, updates, and deletes.


9. Why Not Create an Index on Everything?

It might sound logical to create an index for every column.

But that's usually a bad idea.

Suppose a table has:

id
name
email
age
city
phone
created_at
status
Enter fullscreen mode Exit fullscreen mode

You could theoretically create indexes on all of them.

But indexes have costs.

When you insert a new row:

INSERT
  ↓
Update Table
  ↓
Update Relevant Indexes
Enter fullscreen mode Exit fullscreen mode

More indexes can mean more maintenance work.

They also consume storage.

So indexing is a trade-off:

Faster Reads
     ↕
More Storage + Write Overhead
Enter fullscreen mode Exit fullscreen mode

Good database design means choosing indexes based on actual query patterns.


10. The Database Reads Data Pages

A database doesn't usually think about data exactly like your application does.

It manages data in units such as pages or blocks, depending on the database system.

Conceptually:

Database
 ├── Page 1
 ├── Page 2
 ├── Page 3
 ├── Page 4
 └── ...
Enter fullscreen mode Exit fullscreen mode

Rows are stored within these structures.

When the database needs data, it needs to access the relevant pages.

And this leads to one of the most important performance concepts:

Memory is much faster than storage.


11. Database Memory and Buffer Cache

Databases try to avoid reading from disk unnecessarily.

Frequently accessed data can remain in memory through mechanisms such as a database buffer pool or cache.

Conceptually:

Query
  ↓
Memory?
  |
  ├── Yes → Use cached page
  |
  └── No
       ↓
      Storage
       ↓
     Load page
       ↓
      Memory
Enter fullscreen mode Exit fullscreen mode

If the required data is already in memory, the database can avoid a slower storage read.

This is one reason repeated queries can behave very differently from a first query, depending on the workload and cache state.


12. The Database Doesn't Always Read the Entire Row

Suppose your query is:

SELECT name
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

You only asked for:

name
Enter fullscreen mode Exit fullscreen mode

The database doesn't necessarily need to return every column to the application.

This is one reason it's often better to avoid:

SELECT *
Enter fullscreen mode Exit fullscreen mode

when you only need a few columns.

Instead:

SELECT name, email
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

This makes the requested data explicit and can reduce the amount of data that needs to be processed or transferred.


13. What Happens With Multiple Conditions?

Consider:

SELECT *
FROM products
WHERE category = 'laptops'
AND price < 50000;
Enter fullscreen mode Exit fullscreen mode

Now the database needs to evaluate multiple conditions.

It may use:

Index
   ↓
Find candidate rows
   ↓
Check category
   ↓
Check price
   ↓
Return matching rows
Enter fullscreen mode Exit fullscreen mode

The optimizer determines the actual execution strategy.

This is why two queries that look similar to a developer can have very different performance characteristics.


14. Joins Make Queries More Interesting

Suppose you have:

users
orders
Enter fullscreen mode Exit fullscreen mode

and want to find orders belonging to a particular user.

You might write:

SELECT users.name, orders.total
FROM users
JOIN orders
ON users.id = orders.user_id
WHERE users.id = 42;
Enter fullscreen mode Exit fullscreen mode

Now the database has to combine information from two tables.

Conceptually:

Users
   ↓
Match user_id
   ↓
Orders
   ↓
Filter
   ↓
Result
Enter fullscreen mode Exit fullscreen mode

The database has different algorithms for performing joins, and the optimizer chooses a strategy based on the available information and estimated cost.

This is one of the strengths of relational databases.


15. Sorting Also Costs Work

Consider:

SELECT *
FROM products
ORDER BY price DESC;
Enter fullscreen mode Exit fullscreen mode

The database needs to produce the rows in price order.

Depending on the query and available indexes, it may need to perform a sort.

Conceptually:

Rows
 ↓
Read
 ↓
Sort
 ↓
Return
Enter fullscreen mode Exit fullscreen mode

Sorting a small number of rows is usually cheap.

Sorting millions of rows can be much more expensive.

This is why queries involving large datasets should be designed carefully.


16. LIMIT Can Reduce Unnecessary Work

Suppose you only need the latest 20 posts.

Instead of:

SELECT *
FROM posts
ORDER BY created_at DESC;
Enter fullscreen mode Exit fullscreen mode

you might use:

SELECT *
FROM posts
ORDER BY created_at DESC
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

Now the database knows that the application only needs a limited number of results.

With a suitable index and execution plan, this can be significantly more efficient than retrieving a huge result set.

This is especially important for APIs that power feeds, search results, dashboards, and admin panels.


17. Aggregations Require Computation

Consider:

SELECT COUNT(*)
FROM orders
WHERE user_id = 42;
Enter fullscreen mode Exit fullscreen mode

The database needs to calculate the result.

Other examples include:

SUM()
AVG()
MIN()
MAX()
COUNT()
Enter fullscreen mode Exit fullscreen mode

For example:

SELECT
    category,
    COUNT(*)
FROM products
GROUP BY category;
Enter fullscreen mode Exit fullscreen mode

The database now needs to:

Read data
   ↓
Group rows
   ↓
Calculate counts
   ↓
Build result
Enter fullscreen mode Exit fullscreen mode

Aggregation queries can become expensive when they operate on large datasets.


18. Transactions Change How Queries Are Executed

Now consider:

BEGIN;

UPDATE accounts
SET balance = balance - 1000
WHERE id = 1;

UPDATE accounts
SET balance = balance + 1000
WHERE id = 2;

COMMIT;
Enter fullscreen mode Exit fullscreen mode

These queries are part of one transaction.

The database needs to ensure that the transaction follows its consistency and durability rules.

If something goes wrong before the transaction commits:

BEGIN
 ↓
UPDATE
 ↓
ERROR
 ↓
ROLLBACK
Enter fullscreen mode Exit fullscreen mode

The database can undo the transaction according to its transactional model.

Transactions are essential for operations where multiple changes need to behave as one logical unit.


19. Concurrency Makes Databases Harder

Imagine two users try to purchase the last available product at exactly the same time.

Both requests might execute:

Check inventory
     ↓
Inventory = 1
     ↓
Purchase
Enter fullscreen mode Exit fullscreen mode

If the system isn't designed correctly, both requests might believe the product is available.

Databases provide mechanisms such as:

  • locks
  • isolation levels
  • MVCC
  • transactions

to manage concurrent operations.

The exact behavior depends on the database engine and configuration.

This is where database design becomes much more than simply writing SQL.


20. MVCC and Concurrent Reads

Many modern relational databases use some form of Multi-Version Concurrency Control (MVCC).

The basic idea is that readers and writers can often work concurrently without every read blocking every write.

Conceptually:

Transaction A
     ↓
Reads version 1

Transaction B
     ↓
Creates version 2
Enter fullscreen mode Exit fullscreen mode

Depending on the database's isolation model, Transaction A can continue seeing a consistent view while Transaction B modifies newer data.

The exact implementation differs between databases, but the larger idea is important:

Databases need sophisticated mechanisms to handle many operations happening at the same time.


21. The Database Produces the Result

Once the database has completed the execution plan, it produces the result.

For example:

Query
 ↓
Parse
 ↓
Optimize
 ↓
Execute
 ↓
Read Data
 ↓
Filter / Join / Sort
 ↓
Result
Enter fullscreen mode Exit fullscreen mode

The result might be:

id | name  | email
-------------------------
42 | Alex  | alex@mail.com
Enter fullscreen mode Exit fullscreen mode

The database then sends the result back through the database connection.


22. The Backend Receives the Result

Your backend receives the database result:

const result = await db.query(
    "SELECT name, email FROM users WHERE id = 42"
);
Enter fullscreen mode Exit fullscreen mode

The application can then transform the database representation into an API response.

For example:

{
  "id": 42,
  "name": "Alex",
  "email": "alex@example.com"
}
Enter fullscreen mode Exit fullscreen mode

The client doesn't need to know how the database produced the result.

The backend acts as the boundary between the API and the data layer.


23. The Entire Journey

Let's put everything together.

Suppose your application executes:

SELECT name
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

A simplified journey is:

Application
     ↓
Connection Pool
     ↓
Database Server
     ↓
Parse SQL
     ↓
Check Permissions
     ↓
Query Optimizer
     ↓
Execution Plan
     ↓
Index / Table Scan
     ↓
Memory / Storage
     ↓
Filter Rows
     ↓
Build Result
     ↓
Send Result
     ↓
Application
Enter fullscreen mode Exit fullscreen mode

One SQL statement can therefore trigger a surprisingly large amount of work.


24. Why Some Queries Are Slow

When someone says:

"The database is slow."

that doesn't necessarily mean the database itself is the problem.

The issue could be:

Missing Index
       ↓
Large Table Scan
       ↓
Too Many Rows
       ↓
Expensive Join
       ↓
Large Sort
       ↓
Lock Contention
       ↓
Slow Storage
       ↓
Connection Pool Exhaustion
Enter fullscreen mode Exit fullscreen mode

Or the query might simply be asking the database to do an enormous amount of work.

This is why database performance tuning should start with understanding what the database is actually doing.


25. EXPLAIN Shows the Database's Plan

Most major relational databases provide tools for inspecting query execution plans.

For example, PostgreSQL supports:

EXPLAIN
SELECT *
FROM users
WHERE email = 'alex@example.com';
Enter fullscreen mode Exit fullscreen mode

You can also use:

EXPLAIN ANALYZE
SELECT *
FROM users
WHERE email = 'alex@example.com';
Enter fullscreen mode Exit fullscreen mode

The output can help you understand things such as:

Which index was used?
Was a table scan performed?
How many rows were estimated?
How many rows were actually processed?
How much time did each operation take?
Enter fullscreen mode Exit fullscreen mode

This is one of the most useful tools for understanding slow queries.

Instead of guessing:

"Maybe I need an index."

you can inspect what the database is actually doing.


26. Query Optimization Is About Reducing Work

A useful mental model is:

A fast query usually isn't doing less work by accident. It has been designed so the database can avoid unnecessary work.

For example:

Bad Pattern
Query
 ↓
Scan 10 million rows
 ↓
Filter
 ↓
Sort
 ↓
Return 20 rows
Enter fullscreen mode Exit fullscreen mode

A better execution strategy might be:

Query
 ↓
Use Index
 ↓
Find relevant rows
 ↓
Return 20 rows
Enter fullscreen mode Exit fullscreen mode

The goal isn't simply:

"Make the database faster."

It's:

"Make the database do less unnecessary work."


27. Why Database Indexes Are So Important

Suppose your API frequently executes:

SELECT *
FROM users
WHERE email = ?;
Enter fullscreen mode Exit fullscreen mode

If email is frequently used for lookups, an index may make sense.

Without an index:

Query
 ↓
Potentially inspect many rows
Enter fullscreen mode Exit fullscreen mode

With an appropriate index:

Query
 ↓
Index lookup
 ↓
Relevant row(s)
Enter fullscreen mode Exit fullscreen mode

But indexes should be based on actual workload.

An index that helps one query may not help another.

Database optimization is therefore closely connected to understanding how your application accesses its data.


28. The Database Is Not Just Storage

A common beginner mental model is:

Backend
   ↓
Database
   ↓
Save data
Enter fullscreen mode Exit fullscreen mode

A database is much more than a place to store rows.

It is responsible for things such as:

Parsing
Query Planning
Indexing
Data Retrieval
Transactions
Concurrency
Consistency
Durability
Caching
Recovery
Enter fullscreen mode Exit fullscreen mode

That's why databases are such complex pieces of software.

Your SQL query may be only a few lines long, but the database engine underneath it is doing significant work.


29. What Happens in a Production Application?

A real application may look more like:

                 Client
                    ↓
                Backend
                    ↓
              Connection Pool
                    ↓
                Database
                    ↓
        ┌───────────┼───────────┐
        ↓           ↓           ↓
      Cache       Indexes      Storage
                    ↓
                 Query
                    ↓
               Execution
                    ↓
                Response
Enter fullscreen mode Exit fullscreen mode

And there may be additional infrastructure such as:

Read Replicas
Connection Proxies
Monitoring
Backups
Replication
Sharding
Enter fullscreen mode Exit fullscreen mode

This is where database queries connect directly to system design.

A query that takes 5 milliseconds on a small dataset may behave very differently when the database has billions of rows and thousands of concurrent requests.


30. A Query Is a Request for Work

The most useful way to think about a database query is not:

"I'm asking the database for some data."

Instead think:

"I'm asking the database to perform a specific computation over stored data."

For:

SELECT name
FROM users
WHERE age > 18
ORDER BY name
LIMIT 20;
Enter fullscreen mode Exit fullscreen mode

the database has to:

Find users
   ↓
Filter age > 18
   ↓
Sort by name
   ↓
Take 20
   ↓
Return result
Enter fullscreen mode Exit fullscreen mode

The SQL statement describes what you want.

The database decides how to produce it.

That separation is one of the most important concepts in database systems.


A Simple Mental Model

Whenever you execute:

SELECT *
FROM users
WHERE id = 42;
Enter fullscreen mode Exit fullscreen mode

think:

Application
     ↓
Connection
     ↓
Database
     ↓
Parse Query
     ↓
Check Permissions
     ↓
Build Execution Plan
     ↓
Choose Index / Scan
     ↓
Read Memory / Storage
     ↓
Filter / Join / Sort if needed
     ↓
Build Result
     ↓
Return Result
     ↓
Application
Enter fullscreen mode Exit fullscreen mode

And when a query becomes slow, don't immediately blame the database.

Ask:

What is the database actually doing?
Enter fullscreen mode Exit fullscreen mode

Is it scanning millions of rows?

Is it missing an appropriate index?

Is it sorting a huge dataset?

Is a join producing far more rows than expected?

Is the database waiting on locks?

Is the application exhausting its connection pool?

Is the query simply doing too much work?

Tools such as execution plans can help answer these questions.

The biggest lesson is simple:

SQL describes what you want. The database figures out how to get it.

Once you understand that process, indexes, query optimization, transactions, joins, caching, connection pooling, and database scaling stop feeling like isolated concepts.

They become different ways of controlling how much work the database has to perform to answer your application's questions.

Top comments (1)

Collapse
 
raknaos profile image
Raknaos

The step that finally clicked for me was the buffer pool miss. People reason about query cost as if everything is CPU work, but the difference between an index hit served from memory and the same plan taking a disk read path is often two orders of magnitude -- and the plan looks identical in EXPLAIN output.

One thing worth expanding: the execution-plan step is where the optimizer's assumptions get baked in. A stale statistics set can pick a perfectly reasonable plan for a distribution that no longer exists, which is why "it was fast yesterday" debugging is so misleading. Have you run into cases where you traced the slow query all the way through this pipeline and the answer turned out to be at the statistics layer rather than any of the stages you listed?