No Such Table: Fix Django OperationalError Fast
Run showmigrations, apply migrate, verify the DB file.
20+ years shipping production Python across data and backend systems. Drawn from code that ran under real load.
- ✓Basic Django: models, migrations, and manage.py
- ✓Running scripts with python3 and reading tracebacks
- ✓How databases store tables (SQLite basics)
- Run showmigrations to see what's applied, then migrate so schema ships before code ever queries it
- A model without a migration file is invisible — generate with makemigrations, review, commit, then migrate
- Confirm the app reads the database you migrated; dev sqlite vs staging Postgres splits cause phantom tables
- Keep queries lazy: module-level evaluation runs before fresh databases exist, so wrap it in functions
Think of models as architectural blueprints and the database as the built house. A blueprint for a new pantry doesn't put shelves on the wall — builders (migrations) must construct it first. No-such-table means someone tried to store jam in a pantry that was drawn but never built. Either the builders never came, they built the wrong house, or someone reached for the shelf before construction started.
Your view, shell command, or test crashes with django.db.utils.OperationalError: no such table: myapp_order. The model exists. The migration might even exist. But the database your process is reading has no such table, and Django won't invent one at query time. This error is a schema-reality mismatch, and its causes split neatly into five buckets you'll learn to check in order.
The impact ranges from annoying to severe. Locally it's a 2-minute migrate. In CI it's a red suite blocking every merge. In production it's checkout 500s because the deploy ran code before schema, or tests passing against a stale kept database while fresh installs crash. One variant — queries evaluated at import time — breaks every fresh environment while working perfectly on the developer's seasoned laptop.
This guide covers all five: unapplied migrations, missing migration files, wrong-database settings, --keepdb test staleness, and import-time queries. You'll get the triage order that names the cause in two minutes, the exact commands for each fix, and deploy patterns that keep schema ahead of code forever.
Migrations Unapplied: Schema Ships Before Code
The most common cause is also the simplest: the migration exists but was never applied to the database being read. You added a model, a teammate's branch added a table, or a fresh environment was provisioned — and nobody ran migrate against this particular database. Django models describe desired schema; only the migrator creates it. Querying before that step raises OperationalError naming the absent table, which is the database telling the truth about its contents.
Confirm with showmigrations: applied migrations carry [X], pending ones [ ]. If your app's latest migration shows [ ], the fix is python manage.py migrate — nothing more exotic. Verify afterward at the database level (dbshell plus .tables on SQLite, or the information-schema query on Postgres) rather than trusting the command's summary line. The table must exist where the app reads, not just where you assume it does.
The production lesson is ordering: migrate before traffic, every deploy, as a blocking step. Release-phase hooks, init containers, and entrypoint scripts all exist for this; pick the one your platform supports and make failed migrates block rollout. Code that queries a table must never reach a dyno whose database lacks it — the 47-minute outage in this article's incident came from deploying code beside schema instead of after it.
Missing Migration File: The Invisible Model
A model without a migration file is invisible to the migrator. migrate can run a hundred times and create nothing, because it only executes operations recorded in files under myapp/migrations/. This happens when a developer adds a model and forgets makemigrations, when the file is generated but never committed, or when a squash-and-rebuild cycle drops an operation. The symptom is distinctive: migrate reports nothing to apply, yet the table is missing.
Diagnose with the check command: makemigrations --check --dry-run exits nonzero when model state diverges from migration files, printing what it would generate. If it proposes a CreateModel for your missing table, you've found the gap. Generate the file with makemigrations myapp, read the operations (auto-detection occasionally misreads renames as drop-plus-create — fix those by hand), commit the file alongside the model change, then migrate.
Gate this in CI so it can't recur: the check command fails the build when files and models diverge, forcing the file into the same pull request as the model. Migration files are code — they get reviewed, tested, and deployed like code. Teams that enforce the same-commit rule stop seeing phantom-table failures because the schema change and its construction script can never separate.
Wrong DB in Settings: The Table Exists Elsewhere
Sometimes the table exists — just not in the database your process reads. Development points at db.sqlite3 while a second settings file points at prod.sqlite3; staging uses Postgres while a debug override uses SQLite; two dynos mount different volumes. You migrate database A with full success, the app reads database B with full confidence, and the error persists through every retry because both sides are behaving correctly against different files.
The tell is identity: print DATABASES['default'] from the exact settings module the failing process loads, then compare NAME (or HOST/PORT for server databases) against where you ran migrate. SQLite makes this concrete — list *.sqlite3 files with sizes and timestamps; a 0-byte file created 5 minutes ago next to a 40MB file from last month means the app is reading a fresh empty database while you migrated the old one.
Standardize ruthlessly. One canonical DATABASES per environment, committed or templated, with the database NAME echoed into deploy logs at boot. Run migrate against that same NAME in the same job that deploys code. When the logs name the file or host on every boot, wrong-database mysteries collapse to a one-line diff instead of a two-hour hunt across volumes and settings files.
--keepdb Tests: Stale Schema, Green Runserver
Django's test runner builds a fresh database by applying every migration, then destroys it — unless --keepdb tells it to reuse last run's database. Reuse is fast and usually fine, until the migration set changes: new tables from new migrations don't exist in the kept schema, and tests fail with no-such-table while runserver (against your dev database) works perfectly. The failure looks like broken tests but is really a stale cache.
Confirm by forcing one clean rebuild: run the suite once without --keepdb (or delete the test_ database) and watch the failures vanish. The fix for local work is periodic recreation; the policy for CI is never passing --keepdb at all, so the pipeline always validates the full migration chain from zero. Fast suites that skip schema creation prove nothing about fresh installs — the exact environments new customers and new teammates hit.
Keep migrations quick so full rebuilds stay painless: squash long chains, avoid data migrations that loop millions of rows in tests (gate them behind settings flags), and keep test settings on SQLite or a disposable Postgres schema. When creating from scratch takes seconds, nobody reaches for --keepdb as a crutch, and the staleness class disappears with it.
Querying at Import Time: SQL Before Schema Exists
The sneakiest variant works on every seasoned machine and fails on every fresh one. A QuerySet evaluated at import time — a module-level FEATURED = Product.objects.filter(...), a class attribute, a decorator argument, a default parameter — executes its SQL during app loading, before any migrate could create the table. The developer's laptop has the table from months ago, so imports succeed; a new container, a rebuilt database, or a test runner hits no-such-table during check or collection, long before any test runs.
Find these with a targeted grep: QuerySet calls (objects.all/filter/get) at module or class scope, outside any function body. The fix is laziness — move the query into a method, a property, or so it executes per request against a ready database. Startup data needs belong behind get_queryset()AppConfig.ready() with a connection guard, or behind post_migrate signals that run after schema exists by definition.
Prove the fix on an empty database: build fresh (migrate --run-syncdb on a blank file), then run check and collect tests with zero queries at import. If collection passes against nothing, the import-time dependency is gone. This is the one no-such-table variant that code review must catch, because no amount of migrating fixes a query that runs before migrations exist.
ready() gates that run after schema exists.Code Beat Schema to Production: 1,180 Checkouts Failed
no such table: shop_coupon on every failure — and 1,180 checkout attempts failed over 47 minutes. The rollback was delayed because reverting code without reverting schema would have broken the already-migrated dyno.coupon table via migration 0041, but the pipeline deployed code to all 4 production dynos concurrently with the migrate step instead of before it. Every checkout POST in the 47-minute window hit a dyno whose code queried shop_coupon before migration 0041 had run there, raising OperationalError: no such table: shop_coupon. The release-phase hook that once ordered this correctly had been disabled 2 weeks prior. The first dyno to finish migrating recovered, but the router kept sending traffic to the 3 unmigrated ones, so the outage persisted until an engineer manually sequenced scale-down, migrate, and scale-up.python manage.py migrate (applying 0041 in 8 seconds), then scale back to 4 dynos and replay the 1,180 failed checkouts from the idempotency log. The release-phase migrate hook was restored and made blocking, a /health/ check now asserts the table exists before the router sends traffic, and CI fails the build when migration files diverge from models. Error rate fell from 100% to 0.01% within 4 minutes of the migrate.- Schema ships before traffic: run migrate as a blocking release step, never beside or after code rollout.
- Health checks must assert schema readiness (table exists), not just process liveness.
- Stage the deploy topology, not just the code — single-node staging hides multi-node migration races.
python manage.py showmigrations myapp (applied ones carry [X]) and python manage.py makemigrations --check --dry-run (nonzero exit means model state diverged from files). Apply with python manage.py migrate, then confirm the table: run python manage.py dbshell and type .tables for sqlite or \dt for Postgres, or use python manage.py inspectdb | grep -i <table>.ls myapp/migrations/ and python manage.py makemigrations myapp --dry-run --verbosity 3 to see what Django would generate. If it proposes CreateModel for your table, the file was never made or never committed — check git status --short myapp/migrations/ and git log --oneline -5 -- myapp/migrations/. Generate, review, commit, migrate.python -c "import os, django; os.environ.setdefault('DJANGO_SETTINGS_MODULE','mysite.settings'); django.setup(); from django.conf import settings; print(settings.DATABASES['default'])" and ls -la .sqlite3 2>/dev/null; grep -rn "DATABASES" mysite/settings.py | head. Migrate the NAME the app reads, not the one you assume — then re-run showmigrations against that same NAME.python manage.py test myapp --keepdb -v 2 2>&1 | tail -5 to see reuse messaging, then force recreation once with python manage.py test myapp --noinput (or delete the test_ database). In CI, never pass --keepdb; always build from zero so the suite validates the full migration chain.grep -rn "objects\.\(all\|filter\|get\)\|\.all()" myapp/ --include='*.py' | grep -v "def \|return\|test" | head -20. Any hit at class or module scope (defaults, attributes, decorator args) runs at import. Wrap it in a function or get_queryset(), then prove laziness: a fresh migrate --run-syncdb on an empty DB followed by python manage.py check should pass with zero queries.| File | Command / Code | Purpose |
|---|---|---|
| django_no_such_table_repro.py | con = sqlite3.connect(":memory:") | Migrations Unapplied |
| django_missing_migration_file.py | app = "myapp" | Missing Migration File |
| django_wrong_db.py | print("candidate sqlite files in cwd:") | Wrong DB in Settings |
| con = sqlite3.connect(":memory:") | configuration |
Key takeaways
ready() gates.Common mistakes to avoid
6 patternsDeploying code concurrently with migrations
Adding a model without makemigrations
Maintaining two settings files with different databases
Always testing with --keepdb
Module-level QuerySets in app code
get_queryset(); gate startup data behind ready().Health checks that only test TCP liveness
Interview Questions on This Topic
What does OperationalError: no such table mean?
Frequently Asked Questions
20+ years shipping production Python across data and backend systems. Drawn from code that ran under real load.
That's Python Libraries. Mark it forged?
4 min read · try the examples if you haven't