Денормализација на базата на податоци: Забрзување на читањата без жртвување на запишувањата
Normalization is one of the best defaults in database design. It reduces duplication, makes updates predictable, and gives every fact a clear home. But a perfectly normalized schema can become an expensive way to answer simple questions when the application reaches production scale.
Denormalization is the deliberate decision to store some derived or repeated data so common reads require less joining, aggregation, or computation. Done carelessly, it creates stale data and difficult writes. Done intentionally, it can make a system faster while keeping correctness under control.
Start with the read path, not the schema diagram
The usual motivation is easy to recognize: a page or API endpoint needs information spread across several tables, and its query is becoming a hotspot. A product listing may need product details, category names, review averages, inventory status, and seller information. A dashboard may need totals calculated across a large event history.
A normalized design can represent these facts cleanly. That does not automatically mean every request should reconstruct them from scratch.
Before adding a denormalized column or table, inspect the actual read path. Ask what query is slow, how often it runs, what filters and sort orders matter, and whether the issue is really a missing index, an inefficient query, excessive data transfer, or an N+1 query in the application layer.
Denormalization should follow measurement and a clear access pattern. It is not a substitute for understanding the query plan.
What denormalization looks like in practice
Denormalization is broader than copying a name into another table. It includes several useful patterns.
- Cached aggregates: storing values such as
comment_count,average_rating, oropen_ticket_counton a parent record. - Read models: maintaining a table designed for a particular screen, API response, or reporting query.
- Duplicated display fields: saving a stable snapshot such as the shipping address or item price attached to an order.
- Precomputed search or sorting fields: storing a value that avoids calculating the same expression for every query.
Some duplication is not merely a performance optimization. An order line should normally preserve the purchased item name and unit price even if the catalog changes later. In that case, the copied values express business history. Treating them as a live cache of the product table would be a modeling mistake.
Make the source of truth explicit
The central question is not whether data is duplicated. It is which copy is authoritative and how the others stay correct.
Suppose a posts record stores a comment_count. The comments table remains the source of truth for individual comments. The count is a derived value maintained for fast reads. That distinction should appear in naming, documentation, and code ownership.
A useful rule is simple: every denormalized field needs a declared maintenance strategy. Choose one deliberately:
- Update it in the same database transaction as the source write.
- Update it asynchronously through a reliable event or job workflow.
- Rebuild it periodically when temporary staleness is acceptable.
- Store it as an immutable snapshot that should never be synchronized.
If the answer is “the application will probably remember,” the design is unfinished.
Use transactions for values that must be immediately correct
For counters and summaries that must reflect a successful write immediately, update the normalized row and its derived value in one transaction. In PHP, that often means keeping the write logic in a small service boundary rather than scattering updates across controllers and jobs.
$pdo->beginTransaction();
try {
$statement = $pdo->prepare(
'INSERT INTO comments (post_id, body) VALUES (:post_id, :body)'
);
$statement->execute([
'post_id' => $postId,
'body' => $body,
]);
$statement = $pdo->prepare(
'UPDATE posts
SET comment_count = comment_count + 1
WHERE id = :id'
);
$statement->execute(['id' => $postId]);
$pdo->commit();
} catch (Throwable $exception) {
$pdo->rollBack();
throw $exception;
}
The increment happens in SQL rather than by reading a count into PHP and writing back a replacement. That matters under concurrency: an in-database increment avoids the classic lost-update pattern.
Transactions do not solve every consistency problem, especially when a change crosses services or databases. They do provide a strong and understandable boundary when the related data lives in one database.
Accept eventual consistency only when the product can accept it
Asynchronous updates can protect the request path when recalculation is costly or a read model combines data from multiple domains. They also introduce operational obligations. Jobs can fail, run twice, arrive out of order, or be delayed.
That means consumers must be designed for a lagging projection. A newly submitted review may not affect a product’s displayed average immediately. A reporting dashboard may show a recent refresh time. The acceptable delay should be a product decision, not an accidental side effect of queueing work.
Make projection handlers idempotent where possible. Record enough information to detect already-applied work, and provide a safe way to rebuild a projection from canonical data. A denormalized model without a repair path eventually turns a minor incident into a manual data investigation.
Keep writes boring and observable
Denormalization moves some cost from reads to writes. That is often the right trade, but only if the write path remains understandable. Avoid allowing every caller to update both the source table and its cached representation independently.
Instead, centralize the operation that changes the underlying fact. For example, a method that publishes a comment can own the comment insert and count update. A worker that processes a catalog change can own updates to its read model. This makes code review more effective because consistency behavior has a visible home.
Also add checks that detect drift. A scheduled reconciliation can compare stored counts with counts calculated from the base table. The check does not need to run on every request; its job is to reveal broken assumptions early and support repair.
Index the denormalized path too
A copied field helps only if the database can use it efficiently. If an API lists products by average_rating and filters on availability, index the combination that matches the query’s filtering and ordering needs. Validate the resulting query plan with representative data rather than assuming an index will be selected.
Likewise, do not create a wide read model that returns every possible field to every client. Denormalization reduces database work, but moving unnecessary payloads across an API can simply relocate the bottleneck.
Know when normalization is still the better choice
Do not denormalize merely because joins exist. Relational databases are designed to join related data, and a well-indexed join over appropriately sized result sets is often exactly the right solution.
Keep the normalized design when writes are frequent, reads are varied, freshness is non-negotiable, or the duplicated value has no stable, well-understood owner. A premature cached aggregate can impose more complexity than the original query ever did.
The strongest database designs are not ideologically normalized or denormalized. They preserve a reliable core of canonical facts, then add carefully maintained shortcuts for proven read needs. Treat each shortcut as a small piece of infrastructure: define its owner, consistency contract, failure behavior, and repair process. When those answers are clear, faster reads do not have to come at the expense of trustworthy writes.