Performance Optimization
Learn the general principles behind a fast MySQL database, from indexing and data types to normalization tradeoffs and caching.
Introduction
A database that returns correct results but takes ten seconds to do it is not really working — users will not wait, and a slow database can bring an entire application to a crawl under real traffic. Performance is not something you chase only when something breaks; it is shaped by decisions you make from the very start of designing a database.
This lesson steps back from any single feature and looks at the general principles behind a fast MySQL database. Many of these ideas — indexing, normalization, avoiding SELECT * — were introduced in earlier lessons; here you will see them specifically through a performance lens, tying them together into one coherent picture.
- Why proper indexing is the single biggest performance lever you have.
- How choosing appropriately sized data types affects speed and storage.
- Why SELECT * costs more than selecting only the columns you need.
- The tradeoff between normalization and denormalization for read-heavy workloads.
- How application-level caching reduces load on the database.
- Why you should measure before optimizing, rather than guessing.
Indexing (Recap)
An index lets MySQL find matching rows without scanning the entire table, much like a book's index lets you jump straight to a page instead of reading cover to cover. Columns that regularly appear in WHERE clauses, JOIN conditions, or ORDER BY clauses are prime candidates for an index.
-- Speeds up lookups and joins on customer_id
CREATE INDEX idx_orders_customer_id ON orders (customer_id);As covered in the Indexes lesson, indexes are not free — they speed up reads but add a small cost to every INSERT, UPDATE, and DELETE, since the index itself must also be maintained. Index the columns your queries actually filter, join, or sort on, not every column in the table.
Choosing Appropriately Sized Data Types
Every column's data type determines how much storage it uses and how quickly MySQL can compare, sort, and index it. Smaller, more precise types generally mean faster queries and less disk I/O, because more rows fit into the same memory page.
| Situation | Prefer | Instead of |
|---|---|---|
| Small whole numbers (e.g. age, quantity) | TINYINT / SMALLINT | BIGINT for everything |
| A date without a time component | DATE | DATETIME or VARCHAR |
| Short, fixed-length codes (e.g. country code) | CHAR(2) | VARCHAR(255) |
| A yes/no flag | TINYINT(1) or BOOLEAN | VARCHAR('yes'/'no') |
A column defined as VARCHAR(255) when the data is always exactly 2 characters wastes space and gives MySQL less useful information to optimize with. Matching the type and size to the actual data keeps rows smaller, tables faster to scan, and indexes more compact.
Avoiding Unnecessary Columns in SELECT
SELECT * asks MySQL for every column in a table, even when your application only needs two or three of them. This wastes network bandwidth transferring data nobody uses, and it can prevent MySQL from using an efficient "covering index" (an index that already contains every column the query needs, avoiding a trip back to the full table).
Selecting Everything
Transfers every column, even ones the application never reads.
SELECT * FROM orders WHERE customer_id = 42;Selecting Only What Is Needed
Transfers only the columns the application actually uses.
SELECT order_id, order_date, total
FROM orders
WHERE customer_id = 42;Normalization vs Denormalization
Normalization (covered earlier in this course) organizes data to avoid duplication and keep it consistent, but it often means a query has to JOIN several tables together to reassemble a full picture. For a heavily read, reporting-style workload, those joins can add up.
Denormalization deliberately reintroduces some duplication — for example, storing a customer's name directly on an orders table instead of always joining to the customers table — to make the most common reads faster, at the cost of extra care needed to keep the duplicated data in sync.
| Approach | Best For | Tradeoff |
|---|---|---|
| Normalization | Write-heavy systems; data integrity matters most | More joins needed to read a full picture |
| Denormalization | Read-heavy systems; dashboards, reports, high-traffic pages | Risk of duplicated data drifting out of sync |
There is no universally "correct" choice — it depends on whether your workload reads far more often than it writes. Many real systems normalize their core data and selectively denormalize only the specific tables that are read the most.
Caching at the Application Level
Not every request needs to hit the database at all. Data that is expensive to compute but does not change every second — a homepage's "top products" list, for example — can be computed once and stored temporarily in an application-level cache (such as Redis or Memcached), then served instantly to every subsequent request until the cache expires or is invalidated.
Caching does not replace a well-designed database, but it can dramatically reduce how often expensive queries need to run in the first place, which helps every other query on the server too.
Measure Before You Optimize
It is tempting to guess which query is "probably slow" and start tweaking it. Resist that urge. Databases are often surprising — a query that looks complicated can be fast, while a query that looks simple can be scanning millions of rows behind the scenes.
MySQL gives you tools to measure exactly what is happening instead of guessing, most importantly the EXPLAIN statement, which you will learn in an upcoming lesson. EXPLAIN shows you precisely how MySQL plans to run a query — which indexes it will use and how many rows it expects to examine — so you can optimize based on evidence, not intuition.
The Query Optimization lesson covers writing queries that are efficient to begin with, and the lesson after that introduces EXPLAIN, the tool you will use to verify exactly how well your queries actually perform.
Common Mistakes
- Adding indexes to every column "just in case," which slows down writes without helping reads.
- Using oversized data types by default (e.g. BIGINT or VARCHAR(255) everywhere) without thinking about the real data.
- Habitually writing SELECT * even in application code that only uses a couple of columns.
- Denormalizing data before confirming normalization is actually the bottleneck.
- Changing queries or configuration based on a guess instead of measuring first.
Key Takeaways
- Proper indexing on columns used in WHERE, JOIN, and ORDER BY is the single biggest performance lever.
- Choosing appropriately sized data types keeps rows and indexes compact and fast.
- Selecting only the columns you need reduces wasted transfer and can enable covering indexes.
- Normalization favors data integrity; denormalization favors read speed — choose based on your workload.
- Application-level caching can avoid hitting the database at all for frequently-requested data.
- Always measure with tools like EXPLAIN before optimizing, rather than guessing.
Summary
Fast databases are usually not the result of one clever trick, but of consistently good habits: sensible indexes, right-sized data types, selective queries, a deliberate normalization strategy, and caching where it helps most.
In this lesson, you learned the general principles behind MySQL performance. Next, you will apply these ideas directly to writing more efficient queries, before learning to measure their actual performance with EXPLAIN.
- You understand the role indexing plays in performance.
- You can choose more appropriate data types for your columns.
- You know when normalization or denormalization suits a workload better.
- You are ready to optimize individual queries.