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:
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!