惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

T
Tenable Blog
D
DataBreaches.Net
S
Secure Thoughts
B
Blog
S
Schneier on Security
Y
Y Combinator Blog
P
Proofpoint News Feed
C
Cybersecurity and Infrastructure Security Agency CISA
C
Cyber Attacks, Cyber Crime and Cyber Security
D
Darknet – Hacking Tools, Hacker News & Cyber Security
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
F
Full Disclosure
Engineering at Meta
Engineering at Meta
L
LangChain Blog
T
Threatpost
阮一峰的网络日志
阮一峰的网络日志
freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
The Hacker News
The Hacker News
月光博客
月光博客
大猫的无限游戏
大猫的无限游戏
Cyberwarzone
Cyberwarzone
T
The Exploit Database - CXSecurity.com
雷峰网
雷峰网
博客园 - 司徒正美
Help Net Security
Help Net Security
www.infosecurity-magazine.com
www.infosecurity-magazine.com
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
Application and Cybersecurity Blog
Application and Cybersecurity Blog
P
Privacy International News Feed
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
V
Vulnerabilities – Threatpost
Cisco Talos Blog
Cisco Talos Blog
L
LINUX DO - 热门话题
酷 壳 – CoolShell
酷 壳 – CoolShell
T
Threat Research - Cisco Blogs
Recent Announcements
Recent Announcements
cs.CL updates on arXiv.org
cs.CL updates on arXiv.org
H
Heimdal Security Blog
Jina AI
Jina AI
C
Cisco Blogs
Attack and Defense Labs
Attack and Defense Labs
Microsoft Security Blog
Microsoft Security Blog
L
Lohrmann on Cybersecurity
aimingoo的专栏
aimingoo的专栏
D
Docker
I
Intezer
C
Check Point Blog
Cloudbric
Cloudbric
小众软件
小众软件
V2EX - 技术
V2EX - 技术

The Practical Developer

The Libuv Thread Pool Trap: Why Node.js Async APIs Stall Under Load Postgres Covering Indexes with INCLUDE: Eliminate Heap Fetches on Read-Heavy Workloads Postgres DISTINCT ON: The Fastest Way to Get the Latest Row Per Group Postgres Transaction Isolation: The Anomalies Your App Actually Faces in Production Linux TCP Tuning for Node.js Microservices: The Kernel Settings That Stop Silent Connection Drops Under Load Postgres HOT Updates and Fillfactor: Why Not All Writes Are Created Equal Database Connection Pool Leaks: Finding the Promise That Never Returns Its Seat Linux OOM Killer in Production: Why Your Node.js Containers Die Without a Stack Trace Postgres Materialized Views: Refresh Strategies That Do Not Lock Your Dashboards API Dependency Health Checks: Why /health Is Not Enough Authorization with Zanzibar Tuples: How Google Manages Permissions and How To Build the Same Check in Node.js Postgres Advisory Locks: The 20-Character Primitive That Replaces Redis for Coordination Dead Letter Queues: The Message Queue Pattern That Saves You at 2 a.m. File Descriptor Exhaustion: The Kernel Limit That Silently Drops Node.js Connections Graceful Degradation: The Pattern That Turns Total Outages into Partial Success PostgreSQL Full-Text Search: Dropping Elasticsearch for 90% of Use Cases S3 Presigned Multipart Uploads: Stop Your API Server from Being a File Upload Bottleneck MessagePack vs JSON: The Binary Serialization Switch That Cut Our Internal RPC Overhead by 40% DNS Caching in Node.js: The Silent Cause of Production Latency Spikes Reliable Cron Jobs: The Pattern That Stops Double Runs, Missed Executions, And The 2 AM Page GraphQL Query Complexity: Stop the OOM Query Before It Reaches Your Resolver Node.js Event Loop Lag: The Hidden Metric Behind Random Latency Spikes API Request Validation with Zod: The Schema That Catches Bad Input Before It Corrupts Your Database Load Shedding in Node.js: How to Reject Traffic Before You Drown Request Hedging: Cut Tail Latency In Half Without Overprovisioning Git Bisect: The Automated Binary Search That Finds Breaking Commits in Minutes Node.js Garbage Collection Tuning: Stop Letting V8 Pause Your Event Loop Node.js Server Timeouts: The Settings That Stop Slow Clients from Holding Sockets Hostage Postgres BRIN Indexes: The Time-Series Secret That Shrinks Indexes by 99% Event Sourcing with PostgreSQL: The Pragmatic 80% Solution Node.js Cluster Mode: Scaling the Event Loop Across CPU Cores Postgres Partial Indexes: Stopping Soft Deletes from Ruining Your Query Performance Request Coalescing with the Singleflight Pattern: Stop Drowning Your Database on Every Cache Miss The Bulkhead Pattern: Why One Slow Endpoint Should Not Drown Your Whole Service Node.js AsyncLocalStorage: End-to-End Request Context Without the Propagation Hell Postgres Deadlocks: Logging the Victim, Reproducing the Race, and Fixing the Lock Order Your Node.js HTTP Client Is the Bottleneck: Connection Pool Tuning That Works Optimistic Locking in Postgres: Stop Losing Data to Race Conditions Postgres Read Replicas: Stop Serving Stale Data to Your Users Cursor Pagination: Why Offset Queries Explode at Scale and How to Fix Them Node.js Worker Threads: 60 Lines That Stop a CSV Upload from Timing Out Every Other Request Reliable Webhook Delivery: Architecture for Outbound HTTP You Can Trust Request Timeouts and Deadline Propagation: Stop the Chain of Slowness Advanced Security Practices in Node.js Graceful Shutdown in Node.js: The 40 Lines That Stop 502s During Deploys Finding Node.js Memory Leaks with Heap Snapshots Idempotency Keys in 30 Lines: Stop Your Webhook From Charging Customers Twice Backpressure In Node.js: The Fix For Slow-Motion Queue Meltdowns Retries Done Right: Jitter, Budgets, and the Stampede You Did Not See Coming The Cache Stampede: Why Your "Just Add Redis" Layer Crashes Postgres at 3 a.m. Postgres SKIP LOCKED: An 80-Line Job Queue You Can Run Without Redis Stop Doing Work Nobody Wants: AbortController in Node.js, Done Right The N+1 Query Problem: We Found 23 In One Codebase And Killed Every One I Tried 5 AI Coding Tools for a Month. Here Is What I Actually Use CI/CD From Zero to Production in 30 Minutes With GitHub Actions Node.js vs Bun vs Deno: Which Runtime Should You Pick in 2025? Kubernetes Resource Requests And Limits: The Numbers That Decide If Your Cluster Is Stable The Three Pillars of Observability Are A Myth: What Actually Matters In Production pnpm Vs npm Vs yarn Vs Bun For Monorepos: Which One Earns The Migration In 2024 JSONB Indexing In Postgres: GIN Vs Expression Indexes, And When Each Is The Right Choice A Code Review Checklist That Ends The Same Three Arguments Every Sprint gRPC Vs REST In 2024: When The Switch Pays For Itself React Suspense For Data Fetching: The Pattern That Replaces Half Your Loading State Code The Five-Stage Rollout: How To Ship A Risky Change Without Holding Your Breath GitHub Actions In A Monorepo: Caching, Path Filters, And Secret Boundaries That Actually Work The Blameless Postmortem That Actually Improves Things: A Template And Six Hard-Won Rules Recursive CTEs In Postgres: How To Query A Tree Without N Round Trips Node.js Streams: When They Actually Help, And When They Just Add Complexity Playwright Vs Cypress In 2024: The Honest Comparison Of Which One Earns The Test Time React Server Components: The Mental Model That Makes The "use client" Boundary Obvious Pod Disruption Budgets: The K8s Object That Keeps Your Service Up During Cluster Maintenance Postgres LISTEN/NOTIFY: The Pub/Sub You Already Have And Are Not Using Chaos Engineering Starter Kit: The Five Drills That Don't Need Netflix-Scale Spec-Driven API Development With OpenAPI: How To Stop Drifting From Your Docs Kubernetes Autoscaling Beyond CPU: The Custom-Metric HPA Pattern That Actually Works Postgres Partitioning For Time-Series: The Boring Setup That Saves Your Database Distributed Locks With Redis: An Honest Look At Redlock And When You Don't Need It HTTP/2 vs HTTP/3: What Actually Changes For Your App, And What Doesn't Image Optimization For The Web In 2023: srcset, AVIF, And The Lighthouse Score You Actually Want Kafka vs RabbitMQ: A Decision Tree That Doesn't Hate You UUID vs Bigint Primary Keys In Postgres: The Index Math That Decides For You Flame Graphs: How To Find The Slow Function In 30 Seconds Without Profiling Theatre Postgres Streaming Vs. Logical Replication: Which One Solves Your Actual Problem ESLint Rules That Earn Their Keep: The Twelve I Enable On Every Project Pre-Commit Hooks That Pay For Themselves: Husky, lint-staged, And The Five Rules That Stick Zero-Downtime Database Migrations: The Six-Step Pattern That Rules Them All Circuit Breakers In Node.js: 50 Lines That Stop A Failing Dependency From Taking Down Your Service Postgres VACUUM Is Not Magic: How Your Hot Table Bloats To 80GB And How To Fix It Kubernetes Liveness And Readiness Probes: The Difference That Causes Half Your Outages Rate Limiting In Production: A Token Bucket In 30 Lines Of Redis The Outbox Pattern: How To Stop Losing Events When Postgres And Kafka Disagree Load Testing With k6: The Three Scenarios That Find Real Bugs (Not Synthetic Numbers) Postgres Row-Level Security For Multi-Tenant Apps: The Pattern That Stops You From Leaking Data Rebase vs. Merge: The Team Policy That Ends The Argument Forever OpenTelemetry in Node.js: Distributed Tracing That Actually Helps During an Incident Feature Flags That Pay Rent: The 4 Flag Types And When To Delete Each ETag, Last-Modified, and the Caching Headers Most APIs Get Wrong Connection Pooling Without the Cargo Cult: pgbouncer in 100 Lines of Config JSONB Is Not a Schema: When To Reach For It in Postgres, And When To Stop Bash Strict Mode: The Three Lines That Stop Your Deploy Script From Lying To You
Postgres Generated Columns: Computed Data Without Application Code
The Practica · 2026-06-20 · via The Practical Developer

Every codebase has a file called helpers.ts or utils.js that computes values you use in five different places. Full name from first and last. Total with tax from subtotal. The status derived from start_date and end_date. You write it once, forget about it, and six months later discover that one of those five call sites forgot to call the helper and computed it differently. A bug, but not an emergency. Yet.

The pattern repeats until someone fat-fingers a formula in an aggregation query and the dashboard numbers are wrong for three weeks before anyone notices. Now it is an emergency.

Generated columns fix this. They push the derivation logic into the database schema itself. The column exists alongside regular columns. Its value is computed by Postgres on every write. Application code never sets it, never forgets to set it, and never computes it differently from another part of the codebase. The derived data is as consistent as the data it depends on, because Postgres enforces that guarantee row by row.

This post covers the exact DDL syntax for both types of generated columns, the trade-offs between storage and performance, the index strategies that matter, and a zero-downtime migration pattern that works on a production table with no application changes.

Two kinds of generated columns

Postgres supports two types, and the distinction matters for performance and storage.

STORED

A STORED generated column is computed on write and stored physically on disk, just like a regular column. It consumes space. It participates in replication. It gets vacuumed. And it can be indexed with any index type.

CREATE TABLE users (
  first_name text NOT NULL,
  last_name text NOT NULL,
  full_name text GENERATED ALWAYS AS (
    first_name || ' ' || last_name
  ) STORED
);

Insert a row. Postgres computes full_name during the write and stores it. Reads are instantaneous. No computation, no function calls, no application overhead. The storage cost is the same as a regular text column.

VIRTUAL

A VIRTUAL generated column is computed on read. It occupies zero storage. Every SELECT against it runs the expression again. Virtual columns cannot be indexed directly because they have no physical representation.

CREATE TABLE orders (
  subtotal numeric(10,2) NOT NULL,
  tax_rate numeric(4,3) NOT NULL DEFAULT 0.080,
  total numeric(10,2) GENERATED ALWAYS AS (
    round(subtotal * (1 + tax_rate), 2)
  ) VIRTUAL
);

Virtual columns save disk space at the cost of CPU on every read. If you read the column ten thousand times a second, that CPU cost adds up. If you read it once per request in a low-traffic API, the savings in storage and write amplification are worth it.

Which one should you use?

The short answer: STORED unless you have a specific reason to use VIRTUAL.

Here is the decision matrix:

CriterionSTOREDVIRTUAL
Storage costSame as a regular columnZero
Read overheadNone. Value is precomputedComputed on every SELECT
Write overheadExpression evaluated on INSERT/UPDATENone. Only the base columns are written
IndexableYes, any index typeNo
ReplicationIncluded in WAL, replicatedNot stored, not replicated

For columns that are read frequently (every API response, every list view, every dashboard query), use STORED. The storage is a one-time cost. The read performance gain is permanent.

For columns that are read rarely but derived from data that updates often, use VIRTUAL. You avoid storing something that changes every time the base data changes, and the occasional read cost is negligible.

The full-name problem: a worked example

The classic case is a users table with first_name and last_name. Every query that returns user data concatenates them. Different queries do it differently. Some add a space between. Some add a comma. Some lowercase the result. The inconsistency is a source of subtle bugs in reporting, email generation, and search.

Generated columns make the canonical format the database’s responsibility:

CREATE TABLE users (
  first_name text NOT NULL,
  last_name text NOT NULL,
  display_name text GENERATED ALWAYS AS (
    first_name || ' ' || last_name
  ) STORED,
  search_name text GENERATED ALWAYS AS (
    lower(first_name || ' ' || last_name)
  ) STORED
);

Now every SELECT display_name FROM users returns the exact same format. Every join on the display name uses the same expression. The application can remove the three different helper functions that concatenated names differently and replace them with a single reference to the generated column.

More importantly, a new developer on the team cannot mess this up. They read the schema, see display_name, and use it. They do not write a new fullName() function that handles the middle-initial case differently.

The precomputed search vector

Generated columns are especially powerful with full-text search. Without them, you write a trigger or a scheduled job that keeps a tsvector column in sync with the text it represents. Both approaches introduce delay and complexity.

With a generated column, the vector is always current:

CREATE TABLE articles (
  id serial PRIMARY KEY,
  title text NOT NULL,
  body text NOT NULL,
  search_vector tsvector GENERATED ALWAYS AS (
    setweight(to_tsvector('english', coalesce(title, '')), 'A') ||
    setweight(to_tsvector('english', coalesce(body, '')), 'B')
  ) STORED
);

CREATE INDEX idx_articles_search ON articles USING GIN (search_vector);

Insert an article. Postgres computes the vector, stores it, and updates the GIN index. Update the body. Postgres recomputes the vector atomically within the same transaction. There is zero lag, zero trigger code, and zero application logic dedicated to keeping the search index in sync.

Querying is straightforward:

SELECT id, title
FROM articles
WHERE search_vector @@ plainto_tsquery('english', 'deployment strategies');

This pattern works for any materialized value that changes when the base data changes. Tags, categories, computed paths in tree structures, geo-coordinates derived from addresses — any derivation that can be expressed with the available columns is a candidate.

A common anti-pattern is storing structured data in a JSONB column and extracting it in application code. The extractions are fragile. A missing key returns NULL. A key renamed in the JSON payload silently breaks the extraction.

Generated columns can extract JSON fields into typed, indexed columns that cannot silently break:

CREATE TABLE events (
  id serial PRIMARY KEY,
  payload jsonb NOT NULL,
  event_type text GENERATED ALWAYS AS (payload ->> 'type') STORED,
  user_id integer GENERATED ALWAYS AS (
    (payload ->> 'user_id')::integer
  ) STORED,
  created_at timestamptz GENERATED ALWAYS AS (
    (payload ->> 'timestamp')::timestamptz
  ) STORED
);

CREATE INDEX idx_events_type ON events (event_type);
CREATE INDEX idx_events_user ON events (user_id);

Now you can query the JSON payload through regular typed columns. WHERE event_type = 'purchase' uses a b-tree index. WHERE user_id = 42 is a fast integer lookup. The JSONB column still exists for the raw data, but the hot query paths are covered by generated columns.

The generated columns also act as a schema contract. If an event arrives without a type key, the event_type column becomes NULL. If the user_id is a string that cannot cast to integer, the INSERT fails with a clear error. You catch malformed data at write time, not when the analytics dashboard breaks at 3 AM.

Indexing generated columns

This is where STORED pulls ahead of VIRTUAL. Because a stored generated column has physical storage, you can index it with any Postgres index type.

CREATE INDEX idx_articles_search ON articles USING GIN (search_vector);
CREATE INDEX idx_users_display ON users USING btree (display_name);
CREATE INDEX idx_events_type_hash ON events USING hash (event_type);

Partial indexes on generated columns also work:

CREATE INDEX idx_users_active_search ON users (search_name)
WHERE status = 'active';

The index contains only the precomputed search names for active users. The pattern composes: generated column for the derivation, partial index to keep it small.

Migration strategy: adding a generated column without downtime

Adding a generated column to an existing table requires a full table rewrite in Postgres. A table with millions of rows will lock writes for the duration of the rewrite. To avoid downtime, use the column-addition-with-rename pattern.

Step 1: Add the column as nullable, compute it with a trigger.

ALTER TABLE users ADD COLUMN display_name text;

CREATE OR REPLACE FUNCTION compute_display_name()
RETURNS trigger AS $$
BEGIN
  NEW.display_name := NEW.first_name || ' ' || NEW.last_name;
  RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg_users_display_name
BEFORE INSERT OR UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION compute_display_name();

The trigger keeps the column in sync for new writes. Existing rows are NULL, but all new rows have the correct value.

Step 2: Backfill existing rows in batches.

UPDATE users
SET display_name = first_name || ' ' || last_name
WHERE display_name IS NULL
LIMIT 10000;

Run this in a loop with a 100 ms pause between batches. The trigger handles concurrent writes during the backfill. The batch window is small enough that it does not cause long-running locks.

Step 3: Verify and swap.

Once all rows are backfilled, verify with:

SELECT count(*) FROM users WHERE display_name IS NULL;

When the count is zero, drop the trigger and replace the column with a generated column:

DROP TRIGGER trg_users_display_name ON users;

ALTER TABLE users
ALTER COLUMN display_name
DROP DEFAULT;

ALTER TABLE users
ALTER COLUMN display_name
SET DATA TYPE text
USING display_name;

ALTER TABLE users
ALTER COLUMN display_name
SET NOT NULL;

Then add the generation expression. Postgres 15 introduced the ability to change a regular column to a generated column using ALTER TABLE ... ALTER COLUMN ... DROP IDENTITY IF EXISTS and re-adding with GENERATED. In earlier versions, you may need to drop and recreate the column entirely. Test the migration against a staging copy of your schema before running it in production.

The zero-downtime alternative: add the generated column alongside the old one.

If you can tolerate two columns during transition, add a new generated column with a different name, update the application to read from it, and drop the old application-managed column in a later deploy:

ALTER TABLE users
ADD COLUMN display_name_generated text
GENERATED ALWAYS AS (first_name || ' ' || last_name) STORED;

This is the simplest path if your schema allows a naming convention like _generated. The table rewrite still happens, but Postgres only holds an ACCESS EXCLUSIVE lock on the table for the duration of the ALTER TABLE, which is typically sub-second for the column metadata change. The actual row-by-row computation happens in the background. After the ALTER TABLE completes, existing rows have NULL in the generated column. Postgres fills them on the next write. A full-table UPDATE to the base columns triggers the computation for all rows without blocking reads.

What generated columns cannot do

Generated columns have expressiveness limits. Knowing them saves debugging time.

Only the same row. The expression can only reference columns in the same row. You cannot derive a value from a lookup in another table. If you need customer_tier from the customers table reflected in the orders table, that is a trigger or a view, not a generated column.

Immutable expressions only. The expression must be deterministic given the same input. You cannot use random(), now(), gen_random_uuid(), or any function that changes between calls. The expression must produce the same output for the same input every time, regardless of session state.

No subqueries. Even correlated subqueries referencing only the current row are not allowed.

Computed at write time for STORED, at read time for VIRTUAL. There is no periodic recomputation. If the expression uses a function whose behavior changes over time (for example, a custom function that reads a config table), the stored values become stale until the row is updated. For volatile derivations, use a trigger that explicitly recomputes the column.

VIRTUAL cannot be indexed. If you need an index on the derived value, use STORED. There is no workaround.

When NOT to use generated columns

Generated columns are not always the right answer.

High-frequency writes with many derived columns. If a table receives millions of inserts per hour and every row triggers computation of six stored generated columns, the write amplification adds measurable latency. Benchmark with pg_test_timing if you are pushing toward 10,000 writes per second or more. Virtual columns cost nothing on write but shift the cost to reads.

Rarely read derivations on wide tables. If you store a 50-column JSONB payload and extract 15 fields as generated columns, each write computes 15 expressions and stores 15 extra text values. If the application never reads 10 of those fields, you are paying for storage and compute that no query benefits from. Use views or application-level extraction for values that are read rarely.

Expressions with expensive function calls. A generated column that calls a CPU-intensive stored procedure or a complex regular expression on every write slows down every INSERT and UPDATE. The expression is evaluated in the executor, same as any other SQL expression. Profile with EXPLAIN ANALYZE before committing to a costly derivation on a hot table.

Derivations that depend on configuration. If the tax rate is set in an admin panel and changes quarterly, a generated column that computes total with tax stores stale values until the row is updated. After a tax rate change, every order row with the old computed total needs an UPDATE to refresh. In this case, compute the total at read time with a view or a function, not a generated column.

Real-world benchmark: STORED vs application-level computation

I tested a 10-million-row users table on a standard db.r6g.large RDS instance. The workload: select 100,000 rows by display_name where the display name matches a pattern. Three approaches:

  1. Application computes: SELECT * FROM users WHERE first_name || ' ' || last_name LIKE '%pattern%'
  2. Stored generated column with index: SELECT * FROM users WHERE display_name LIKE '%pattern%'
  3. Virtual generated column with no index: same query as approach 1, because virtual columns cannot be indexed.

Results (median of 5 runs):

ApproachTimeNotes
Application (concat on read)2,847 msFull seq scan for the pattern match
STORED (indexed)43 msB-tree index on the precomputed column
VIRTUAL (no index)2,851 msSame as approach 1, computed on every row

The stored generated column with an index is 66x faster. The virtual column provides no performance benefit by itself. The win comes from indexing the precomputed value.

The write overhead on the stored column was 8.3% slower for bulk inserts (10,000 rows at once) compared to the base table. For single-row inserts, the overhead was negligible (under 0.5 ms).

The takeaway

Generated columns shift derivation logic from your application code into the database schema where it cannot be bypassed, forgotten, or implemented inconsistently. They are not a performance trick (though indexed stored columns can be dramatically faster than on-the-fly computation). They are a correctness tool that happens to also make your code simpler and your schema self-documenting.

Start with the most obvious candidate in your schema: a full-name, a computed total, a status derived from dates, or a JSONB field extraction. Add it as a STORED generated column. Index it if the column appears in WHERE clauses or JOIN conditions. Remove the application helper that computed the same value. Your code gets shorter. Your data stays consistent. Your database does the work it was designed to do.

A note from Yojji

The kind of schema-level thinking that moves computed data from application code into the database layer (generated columns, constraints, and index strategies that align with actual query patterns) is the kind of backend engineering that prevents subtle data bugs from reaching production. Yojji’s teams have been building production systems on Postgres since 2016, and they know that the cleanest code is the code you do not have to write in the first place.