Getting ChatGPT to Write Accurate SQL Migrations Without Locking Production Tables
You ask ChatGPT to write a migration that adds an index to a table with 50 million rows. It gives you a clean-looking CREATE INDEX statement. You run it on Friday afternoon and your production database grinds to a halt for eight minutes while every write queues up behind an exclusive table lock. That's the failure mode this article helps you avoid.
ChatGPT doesn't know your database engine, your table size, or your traffic patterns unless you tell it. Its default output is textbook-correct SQL β which often means it uses the simplest DDL syntax, not the production-safe one. With the right prompting strategy, you can get it to write migrations that are genuinely safe to run during business hours.
What you'll learn
- Why ChatGPT defaults to locking DDL and what it's actually missing from context
- Which DDL operations cause full table locks in PostgreSQL and MySQL
- Prompt templates for non-blocking index creation, safe column additions, and constraint changes
- How to ask ChatGPT to annotate locking risk directly in the migration output
- Common gotchas that slip through even well-prompted migrations
Prerequisites
You should be comfortable writing raw SQL and understand the basics of database migration tools (Flyway, Liquibase, Alembic, or similar). The examples here use PostgreSQL syntax, but the prompting principles apply to MySQL and MariaDB as well. Table sizes and traffic patterns in the examples are illustrative; test every migration against your own dataset in staging.
Why ChatGPT defaults to table-locking DDL
Large language models are trained on documentation, tutorials, and Stack Overflow answers. The vast majority of that content is written for correctness, not for production scale. When a PostgreSQL tutorial shows you how to add an index, it writes CREATE INDEX, not CREATE INDEX CONCURRENTLY, because the latter comes with caveats that would distract from the point being taught.
ChatGPT inherits this bias. It produces the canonical form of each DDL statement. That's not wrong β it's just incomplete for your use case. The model doesn't know whether your table has 500 rows or 500 million. It doesn't know your replication topology, your deployment window, or whether your app retries failed writes. You have to inject that context explicitly.
There's a second issue: ChatGPT tends to write migrations as a single transaction. Some safe migration techniques deliberately cannot run inside a transaction (notably CREATE INDEX CONCURRENTLY in PostgreSQL). If the model wraps everything in BEGIN; ... COMMIT;, you'll get an error the first time you run it β and a confused junior engineer who reverts to the locking version because at least it ran.
The locking risk by database engine
The specific operations that cause full table locks differ between engines. Knowing this helps you write better prompts.
| Operation | PostgreSQL | MySQL / InnoDB |
|---|---|---|
| Add a non-nullable column (no default) | Full rewrite pre-11, metadata-only in 11+ | Full table copy |
| Add an index | Exclusive lock (use CONCURRENTLY) | Online DDL available (ALGORITHM=INPLACE) |
| Add a CHECK constraint | Full scan + lock (use NOT VALID first) | Full scan + lock |
| Add a foreign key | ShareRowExclusive lock on both tables | Shared lock during validation |
| Change column type | Full rewrite | Full table copy |
| Rename a column | Metadata-only in 10+ | Full table copy pre-8.0, instant in 8.0+ |
When you prompt ChatGPT, include your engine version in the system context. The safe approach in PostgreSQL 15 can be very different from MySQL 5.7. A model that doesn't know which it's targeting will hedge toward the simplest syntax.
How to prompt ChatGPT for safe migrations
The single most effective change you can make is to front-load your constraints as a system-level instruction or as the opening paragraph of your prompt. Don't ask for a migration and then hope the model considers production safety. State it as a hard requirement before describing the change.
Here's a base prompt template to build from:
You are a database engineer writing SQL migrations for a PostgreSQL 15 production database.
Rules you must follow:
1. Never use DDL that takes an exclusive table lock on a table larger than ~1 million rows.
2. Use CREATE INDEX CONCURRENTLY for all new indexes.
3. Do not wrap operations that cannot run in a transaction (e.g., CREATE INDEX CONCURRENTLY) inside BEGIN/COMMIT.
4. When a safe migration requires multiple steps, list each step separately with a comment explaining the locking behavior.
5. For each migration step, add a SQL comment that rates the lock risk: LOW, MEDIUM, or HIGH.
Task: [describe your migration here]
Table name: [table]
Approximate row count: [N]
Deployment window: [zero-downtime required / maintenance window available]
Notice three things: you've given it a persona with operational responsibilities, you've given it explicit rules rather than vague hints, and you've supplied the context it needs to make risk decisions. This dramatically reduces hallucinated lock-safe claims.
For teams using database migration rollback scripts, you can extend this template to also request a rollback step for each forward migration β another area where ChatGPT often omits the safety detail unless asked.
Prompting for non-blocking index creation
This is the most common migration mistake. Here's what the default output looks like versus what you should get after prompting correctly.
Default (unsafe on large tables):
-- Add index on orders.customer_id
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
After applying the base prompt template:
-- Step 1: Create index without blocking writes
-- Lock risk: LOW (CONCURRENTLY avoids exclusive lock; takes longer but safe for production)
-- NOTE: This statement cannot run inside a transaction block.
CREATE INDEX CONCURRENTLY idx_orders_customer_id ON orders (customer_id);
-- Verify the index was created successfully before proceeding:
-- SELECT indexname, indisvalid FROM pg_indexes
-- JOIN pg_index ON pg_indexes.indexname::text = pg_indexes.indexname
-- WHERE tablename = 'orders';
The second version includes the concurrency flag, the transaction warning, and a verification query. You got all of that because the prompt made the safety requirements explicit. If ChatGPT still drops the CONCURRENTLY keyword, follow up with:
Your migration still blocks writes.
Rewrite it assuming the table contains
250 million rows and serves
2,000 writes per second.
Every operation must be zero-downtime.
The additional operational context usually pushes the model toward a much safer solution.
Safe Column Additions
Adding a nullable column is generally inexpensive.
Example:
ALTER TABLE users
ADD COLUMN last_seen_at TIMESTAMP;
The trouble begins when developers combine:
- NOT NULL
- DEFAULT
- Large tables
into a single migration.
Instead of asking:
Add a required column.
Prompt:
Generate a zero-downtime migration
for adding a required column
to a 100-million-row PostgreSQL table.
Avoid long table rewrites.
Explain every step.
A better migration becomes:
-- Step 1
ALTER TABLE users
ADD COLUMN last_seen_at TIMESTAMP;
-- Step 2
Backfill existing rows in batches.
-- Step 3
Add NOT NULL after verification.
The phased approach dramatically reduces locking.
Backfill Should Never Happen in One UPDATE
A common AI-generated migration:
UPDATE users
SET last_seen_at = NOW();
Looks innocent.
On a large table:
- Long-running transaction
- Massive WAL generation
- Replication lag
- Table bloat
Prompt instead:
Backfill existing rows
in batches of 5,000.
Assume production traffic
continues during migration.
The generated solution is much more likely to include pagination or batch updates.
Constraint Validation Can Be Deferred
Suppose you need:
CHECK (price >= 0)
Many developers generate:
ALTER TABLE products
ADD CONSTRAINT positive_price
CHECK (price >= 0);
On large PostgreSQL tables this immediately validates every row.
A safer pattern:
ALTER TABLE products
ADD CONSTRAINT positive_price
CHECK (price >= 0)
NOT VALID;
Later:
ALTER TABLE products
VALIDATE CONSTRAINT positive_price;
Prompt:
Prefer NOT VALID constraints
followed by separate validation
when supported.
This frequently reduces blocking significantly.
Foreign Keys Need Care Too
Another common migration:
ALTER TABLE orders
ADD CONSTRAINT fk_customer
FOREIGN KEY (customer_id)
REFERENCES customers(id);
Large tables may spend considerable time validating existing rows.
Prompt:
Generate a production-safe
foreign key migration.
Separate creation
from validation
where possible.
The resulting migration becomes much safer.
Changing Column Types
One of the highest-risk operations.
Example:
ALTER TABLE users
ALTER COLUMN age TYPE BIGINT;
Depending on:
- Engine
- Version
- Existing data
this may rewrite the entire table.
Prompt:
Assume changing the column type
requires a full table rewrite.
Design a zero-downtime alternative.
Possible output:
- New column
- Dual writes
- Background backfill
- Application switch
- Old column removal
This pattern is far safer than direct alteration.
Prompt for Lock Analysis
One prompt dramatically improves migration quality:
For every SQL statement,
explain:
- Lock type
- Lock duration
- Blocking risk
- Replication impact
The explanations often expose unsafe operations before deployment.
Ask for Rollback Plans
Many generated migrations have:
Forward migration
only.
Production requires rollback.
Prompt:
Generate:
Forward migration
Rollback migration
Verification queries
Explain rollback limitations.
Rollback scripts are especially valuable during staged deployments.
Verification Queries Matter
Never stop after DDL.
Prompt:
After every migration,
generate verification SQL
confirming:
- Constraint validity
- Index usability
- Row counts
- Expected metadata
Example:
SELECT *
FROM pg_indexes
WHERE tablename = 'orders';
or:
SELECT conname,
convalidated
FROM pg_constraint
WHERE conrelid = 'orders'::regclass;
Verification should be part of every migration.
PostgreSQL-Specific Prompt Improvements
Instead of:
Write a PostgreSQL migration.
Use:
PostgreSQL 15
200 million rows
Zero downtime
Streaming replication enabled
No maintenance window
Minimize blocking locks
Annotate every statement
with lock risk.
Providing operational constraints consistently produces safer SQL.
MySQL Prompt Improvements
Likewise:
MySQL 8.0
InnoDB
Large production table
Use online DDL
Prefer ALGORITHM=INPLACE
or ALGORITHM=INSTANT
when supported.
Avoid table copies.
The model cannot optimize for features it doesn't know are available.
Test the Migration Before Trusting It
Ask ChatGPT:
Generate tests for:
- Migration runtime
- Lock duration
- Replication lag
- Rollback
- Failure halfway through
- Interrupted deployment
These tests often reveal operational risks earlier than staging alone.
Common AI-Generated Migration Mistakes
Transaction Wrapping
Some operations cannot execute inside transactions.
Blocking Index Creation
Missing:
CONCURRENTLY
Large Single UPDATE
Creates long-running transactions.
Immediate Constraint Validation
Causes unnecessary blocking.
Missing Rollback
Forward-only migrations increase deployment risk.
No Verification Queries
Deployment succeeds without proving correctness.
No Version Awareness
Database version differences matter enormously.
A Production Prompt Template
A reusable prompt:
Act as a senior database reliability engineer.
Target:
PostgreSQL 15
Rules:
- Zero downtime
- Large production tables
- Streaming replication enabled
- Avoid exclusive locks
- Annotate lock risk
- Generate rollback
- Generate verification SQL
- Explain every operational trade-off
Task:
[Describe migration]
Table:
[Table Name]
Approximate rows:
[Count]
This prompt consistently produces much safer migrations than requesting SQL alone.
Review the Generated SQL Like a DBA
Before approving any migration, ask ChatGPT one final question:
Review this migration
as if you were
the primary database administrator.
Identify:
- Blocking operations
- Replication risks
- Lock escalation
- Long-running transactions
- Rollback concerns
Recommend safer alternatives.
Treat this as a mandatory second review.
Final Thoughts
SQL migrations that look perfectly correct in development can become major production incidents when they encounter large tables, continuous write traffic, replication, and strict availability requirements. ChatGPT doesn't intentionally generate unsafe migrationsβit simply defaults to the canonical SQL syntax because it has no knowledge of your database size, engine version, deployment window, or operational constraints unless you provide them.
The key is to shift your prompts from "write this migration" to "design a production-safe migration under these conditions." Specify your database engine and version, approximate row counts, zero-downtime requirements, acceptable lock levels, replication setup, and rollback expectations before requesting any SQL. Then require the model to explain locking behavior, generate verification queries, and review its own output for operational risk.
Used this way, ChatGPT becomes much more than a SQL generator. It becomes a valuable assistant for designing safer migration plansβwhile the final responsibility for testing, staging, and deployment remains exactly where it belongs: with the engineering team.
Frequently Asked Questions
How do I add an index to a large PostgreSQL table without locking it?
Use CREATE INDEX CONCURRENTLY instead of plain CREATE INDEX. This builds the index in the background without taking an exclusive table lock, allowing reads and writes to continue. Note that it cannot be run inside a transaction block.
Why does ChatGPT generate SQL migrations that lock production tables?
ChatGPT is trained on documentation and tutorials that favor syntactic simplicity over operational safety. It doesn't know your table sizes or traffic patterns unless you tell it, so it defaults to the simplest DDL form, which often takes exclusive locks. Adding explicit rules and context to your prompt reliably fixes this.
What is the expand-contract pattern for database migrations?
The expand-contract pattern splits a breaking schema change into multiple deployments: first add the new structure alongside the old (expand), update application code to use both, then remove the old structure once all traffic has migrated (contract). It avoids table locks and allows zero-downtime rollouts.
Can I add a NOT NULL column to a large table without downtime in PostgreSQL?
Yes, in PostgreSQL 11 and later you can add a NOT NULL column with a constant default in a metadata-only operation that doesn't rewrite the table. For non-constant defaults or older versions, the safe approach is to add a nullable column first, backfill it in batches, then add a CHECK NOT NULL constraint using NOT VALID before validating separately.
What happens if CREATE INDEX CONCURRENTLY fails partway through in PostgreSQL?
PostgreSQL leaves behind an invalid index that still occupies a name in the catalog. Subsequent attempts to create an index with the same name will fail with a duplicate error. You need to DROP the invalid index first using DROP INDEX CONCURRENTLY, then retry the creation.
π€ Share this article
Sign in to saveRelated Articles
Comments (0)
No comments yet. Be the first!