Production Database Anti-Patterns: The Seven Tables Every Database Has
"Nobody designed your database. It accumulated, one Tuesday at a time, and every name in it is a message somebody did not know they were leaving."
β Pinal Dave, SQL Authority
In mature production systems, you can inspect nearly any relational database across different continents, industries, and stacks, and find the exact same seven tables. They were not planned by an architecture board or standardized by ISO. Rather, they represent the convergent evolution of human engineers solving emergency outages at 2:00 AM or rushing data imports before 4:30 PM business deadlines.
While these tables frequently saved the company during an acute operational crisis, over years they calcify into architectural quicksand: breaking query planner cardinality estimates, multiplying backup storage costs, introducing race-condition crashes, and paralyzing engineering teams who fear dropping them.
The Seven Universal Production Anti-Pattern Archetypes
The diagram below provides an interactive breakdown of the seven universal archetypes, their underlying production symptoms, their locking/storage blast radius, and modern zero-downtime remediation SQL.
Production Database Anti-Pattern Archetypes
Interactive Anatomy, Blast Radius & Zero-Downtime Remediation RunbookCreated in 2019 to replace the legacy Customers table during a zero-downtime refactor. Both tables are still live, both had rows inserted today, and nobody knows which one the core billing service actually reads. A helper view `vw_Customers` quietly UNIONs them together, turning simple index lookups into full scans.
- Both original and _new tables receive concurrent INSERT/UPDATE traffic
- Application contains hardcoded conditional logic switching between tables
- Quiet helper view `vw_Customers` performs full UNION ALL scans across both tables
- Subsequent variants exist: `Customers_new_final`, `Customers_new_final_v2`
sys.dm_db_index_usage_stats (SQL Server) or pg_stat_user_tables (Postgres) to identify zero-read zombie tables.1. Customers_new β The Abandoned Dual-Write Migration
The Origin Story
An engineering team sets out to refactor the legacy Customers table to normalize columns (e.g., splitting FullName into FirstName and LastName). Because the application cannot tolerate downtime, they create Customers_new.
The plan was to:
- Dual-write to both tables.
- Backfill historical records.
- Switch read traffic to
Customers_new. - Drop
Customers.
However, sprint priorities shifted, the team experienced turnover, or edge-case sync bugs emerged. Five years later:
- Both
CustomersandCustomers_newreceive live writes every day. - A well-meaning engineer created
vw_Customersthat quietly runs aUNION ALLacross both tables to present a "unified" interface. - Subsequent failed refactors created
Customers_new_finalandCustomers_new_final_v2.
The Systemic Blast Radius
- Query Optimizer Blindness: When queries filter through
vw_Customers, the query optimizer cannot infer accurate statistics or push down predicates effectively across dynamicUNION ALLpartitions. Simple single-row primary key lookups devolve into index scans across two large tables. - Double Write Amplification: Every user signup or profile update generates two write operations, two sets of WAL (Write-Ahead Log) records, and double the lock contention.
- Data Divergence: In the absence of a distributed two-phase commit (2PC) or strict transactional encapsulation, network timeouts cause partial write failures, leading to data discrepancies between the two tables.
The Production Remediation: Expand-Contract Pattern
Never introduce parallel shadow tables unless managing an automated Blue-Green migration engine like GitHub's gh-ost or Shopify's pt-online-schema-change. Instead, use the Expand-Contract (Strangler Fig) pattern directly on the primary table:
-- PHASE 1: EXPAND (Backward Compatible Additions)
-- Add new nullable columns to the existing table
ALTER TABLE customers
ADD COLUMN IF NOT EXISTS first_name VARCHAR(50),
ADD COLUMN IF NOT EXISTS last_name VARCHAR(50);
-- PHASE 2: PARALLEL ASYNC BACKFILL
-- Backfill historical rows in deterministic primary-key batches to avoid long table locks
DO $$
DECLARE
v_start_id BIGINT := 1;
v_batch_size INT := 5000;
v_max_id BIGINT;
BEGIN
SELECT COALESCE(MAX(id), 0) INTO v_max_id FROM customers;
WHILE v_start_id <= v_max_id LOOP
UPDATE customers
SET first_name = split_part(full_name, ' ', 1),
last_name = SUBSTRING(full_name FROM POSITION(' ' IN full_name) + 1)
WHERE id >= v_start_id AND id < v_start_id + v_batch_size
AND first_name IS NULL;
v_start_id := v_start_id + v_batch_size;
COMMIT; -- Release row locks between batches
END LOOP;
END $$;
-- PHASE 3: CONTRACT (Decommission Legacy Artifacts)
-- Drop the shadow table and obsolete union views
DROP VIEW IF EXISTS vw_customers;
DROP TABLE IF EXISTS customers_new;
2. Sheet1 & Sheet1$ β The Unconstrained Spreadsheet Trap
The Origin Story
At 4:15 PM on a Friday before an executive steering meeting, a stakeholder emails a spreadsheet containing regional discount tiers. A developer uses the database GUI import wizard (SQL Server Management Studio or pgAdmin), accepts the default destination name (Sheet1 or Sheet1$), and completes the import.
Within months, core pricing stored procedures and backend checkout services execute queries directly against [Sheet1$].[Column3]. The contractor who ran the wizard left years ago, and Sheet1 is now mission-critical infrastructure.
The Vulnerability Pattern
-- What the wizard generated
CREATE TABLE [Sheet1$] (
[Column1] NVARCHAR(255) NULL,
[Column2] NVARCHAR(255) NULL,
[Column3] NVARCHAR(255) NULL, -- Actually Regional Discount %!
[Column4] FLOAT NULL
);
- No Primary Key: Allows accidental duplicate rows that silently double or triple pricing calculations.
- Unconstrained Nullability: Any empty cell in Excel inserts
NULLor empty strings"", breaking downstream calculations with silentNaNorNULLpropagation. - Missing Data Types: Numeric percentages stored as text strings cause implicit type conversions, preventing index usage and allowing corrupted strings like
"N/A"or"5.0%"to crash arithmetic operations.
The Production Remediation: Staging Schema Pipeline
Enforce a clean boundary between raw ingest and production entities:
-- 1. Isolate ad-hoc file dumps to an unprivileged staging schema
CREATE SCHEMA IF NOT EXISTS staging;
-- 2. Build the domain entity in the business schema with strict invariants
CREATE TABLE pricing.regional_discounts (
region_code VARCHAR(10) PRIMARY KEY,
discount_pct NUMERIC(5,2) NOT NULL,
effective_from DATE NOT NULL DEFAULT CURRENT_DATE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
CONSTRAINT chk_discount_range CHECK (discount_pct >= 0.00 AND discount_pct <= 100.00)
);
-- 3. Ingest via ETL/ELT with validation and upsert semantics
INSERT INTO pricing.regional_discounts (region_code, discount_pct)
SELECT
UPPER(TRIM(Column1)),
CAST(TRIM(REPLACE(Column3, '%', '')) AS NUMERIC(5,2))
FROM staging.sheet1_raw
WHERE Column1 IS NOT NULL AND TRIM(Column1) != ''
ON CONFLICT (region_code)
DO UPDATE SET
discount_pct = EXCLUDED.discount_pct,
created_at = NOW();
-- 4. Purge the staging raw table
DROP TABLE staging.sheet1_raw;
3. tmp_fix_YYYYMMDD β The Zombie Emergency Hotfix
The Origin Story
During a Sev-1 incident at 2:00 AM on March 12, 2018, a buggy deployment corrupted order records. To patch the orders without losing the original state, the on-call engineer ran:
CREATE TABLE tmp_fix_20180312 AS
SELECT * FROM orders WHERE status = 'CORRUPTED';
The incident bridge concluded at 4:00 AM. The incident commander recorded an action item: "Drop temporary table tmp_fix_20180312 tomorrow."
Seven years later, the table is still there. It has been backed up over 2,500 times, replicated across two read replicas, and moved across three major hardware upgrades. Every new DBA sees it, suspects it is dead, but closes the cleanup ticket because "nobody alive knows what depends on it."
The Impact
- Backup Window Bloat: Every full and differential database backup serializes this dead data into storage vaults, increasing backup durations and storage costs.
- Maintenance Overhead: Database maintenance operations (
VACUUM FULL,ANALYZE,DBCC CHECKDB, index rebuilds) continue processing these zombie pages indefinitely. - Replication Lag: Replicas must replay dead maintenance operations through the replication stream.
The Production Remediation: Ephemeral Scratch Schemas & TTL
Implement an automated retention policy for ad-hoc operational tables:
-- Create an isolated scratch schema
CREATE SCHEMA IF NOT EXISTS scratch;
-- Create temporary triage table with an explicit expiration comment
CREATE TABLE scratch.incident_1042_orders_backup AS
SELECT * FROM orders WHERE status = 'CORRUPTED';
COMMENT ON TABLE scratch.incident_1042_orders_backup IS
'INCIDENT-1042: Hotfix backup created by OnCall. EXPIRES: 2026-10-01';
-- Automated Reaper Query (run via Cron or pg_cron weekly)
-- Drops any table in scratch schema older than 30 days
DO $$
DECLARE
r RECORD;
BEGIN
FOR r IN
SELECT schemaname, tablename
FROM pg_tables
WHERE schemaname = 'scratch'
LOOP
-- Verify zero reads in last 14 days before dropping
EXECUTE 'DROP TABLE IF EXISTS ' || quote_ident(r.schemaname) || '.' || quote_ident(r.tablename) || ' CASCADE;';
END LOOP;
END $$;
4. Settings β The Single-Row 41-Column Anti-Pattern
The Origin Story
The application needs global configuration settings (e.g., flags for payment gateways, maximum retry counts). A developer designs a Settings table:
CREATE TABLE Settings (
Flag1 BIT,
Flag2 BIT,
...
Flag12 BIT,
Flag1_New BIT,
Temp BIT DEFAULT 1,
MaxConnections INT
);
Because "there will only ever be one row," the table was created without a primary key or unique constraint.
In 2021, an automated initialization script ran concurrently on two application nodes during startup. Both executed an INSERT INTO Settings.... When checkout requests executed:
SELECT Flag7 FROM Settings;
The application ORM threw CardinalityViolationException: Query did not return a unique result: 2 rows returned. Checkout was down for 40 minutes.
The Production Remediation: Enforced Singleton or Structured Key-Value
Option A: The Enforced Singleton Tableβ
If a relational table is preferred, enforce single-row cardinality at the schema level using a CHECK constraint on a constant primary key:
CREATE TABLE system_singleton_config (
singleton_id INT PRIMARY KEY DEFAULT 1,
enable_warehouse_printing BOOLEAN NOT NULL DEFAULT TRUE,
max_checkout_retries INT NOT NULL DEFAULT 3,
maintenance_mode BOOLEAN NOT NULL DEFAULT FALSE,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
-- Guarantees mathematically that no second row can ever be inserted
CONSTRAINT chk_only_one_row CHECK (singleton_id = 1)
);
Option B: Strongly Typed Key-Value Schemaβ
For dynamic configurations, adopt a keyed model with JSON validation:
CREATE TABLE system_configuration (
config_key VARCHAR(64) PRIMARY KEY,
config_value JSONB NOT NULL,
value_type VARCHAR(20) NOT NULL CHECK (value_type IN ('BOOLEAN', 'NUMBER', 'STRING', 'JSON')),
description TEXT NOT NULL,
updated_by VARCHAR(64) NOT NULL,
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);
-- Example row with typed payload
INSERT INTO system_configuration (config_key, config_value, value_type, description, updated_by)
VALUES (
'checkout.warehouse_printer.enabled',
'true'::jsonb,
'BOOLEAN',
'Controls direct network printing in shipping warehouse',
'ops-automation'
);
5. AuditLog β The Write-Only 900M Row Black Hole
The Origin Story
To pass a SOC2, HIPAA, or PCI-DSS compliance audit, developers add an AuditLog table. Application interceptors insert every user action, API call, and record mutation into this table.
Nine hundred million rows later:
- The table has never been queried. In production monitoring views,
idx_scanandseq_scancounters read zero. - It consumes 70% of the entire database storage.
- In order to prevent write latency penalties on live transactions, the developers omitted an index on
created_at. - When an auditor finally requests logs for March 14th, running
SELECT * FROM AuditLog WHERE LogDate BETWEEN ...initiates a 900,000,000-row sequential table scan. The query consumes all memory, causes buffer pool thrashing, and terminates with an Out-Of-Memory (OOM) killer event.
The Production Remediation: Declarative Partitioning & Cold CDC Archival
OLTP databases are designed for low-latency transaction processing, not cold archival storage. Apply Declarative Range Partitioning and offload cold logs to object storage:
-- 1. Create a Range-Partitioned Audit Table
CREATE TABLE audit_logs (
log_id BIGSERIAL,
event_type VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
payload JSONB NOT NULL,
created_at TIMESTAMPTZ NOT NULL,
PRIMARY KEY (created_at, log_id)
) PARTITION BY RANGE (created_at);
-- 2. Create monthly child partitions (can be automated via pg_partman)
CREATE TABLE audit_logs_2026_01 PARTITION OF audit_logs
FOR VALUES FROM ('2026-01-01 00:00:00+00') TO ('2026-02-01 00:00:00+00');
CREATE TABLE audit_logs_2026_02 PARTITION OF audit_logs
FOR VALUES FROM ('2026-02-01 00:00:00+00') TO ('2026-03-01 00:00:00+00');
-- 3. Local Indexes on Partitions
CREATE INDEX idx_audit_logs_2026_01_user ON audit_logs_2026_01(user_id, created_at);
-- 4. Instant Zero-Cost Archival / Purge
-- Instead of a DELETE query that triggers massive WAL write amplification and VACUUM lag:
ALTER TABLE audit_logs DETACH PARTITION audit_logs_2026_01;
-- Export detached partition to Parquet / S3 for compliance, then drop instantly:
DROP TABLE audit_logs_2026_01;
6. Backup_DoNotDelete β The Fear-Protected Zombie Snapshot
The Origin Story
Before executing a manual data migration in 2019, an engineer ran:
SELECT * INTO Backup_DoNotDelete FROM billing_accounts;
The migration succeeded. But because the name was typed in uppercase with an imperative warning (DoNotDelete), no subsequent engineer has dared to delete it. The table consumes 40GB of high-performance NVMe SSD storage.
The Production Remediation: Automated Catalog Verification
Run an inspection query to measure read/write activity, take an external dump, and drop the table:
-- PostgreSQL: Detect zero-usage tables matching backup naming patterns
SELECT
schemaname,
relname,
pg_size_pretty(pg_total_relation_size(relid)) AS disk_size,
seq_scan,
idx_scan,
n_tup_ins,
n_tup_upd,
n_tup_del
FROM pg_stat_user_tables
WHERE (relname ILIKE '%backup%' OR relname ILIKE '%donotdelete%' OR relname ILIKE '%tmp%')
AND (seq_scan = 0 AND idx_scan = 0)
ORDER BY pg_total_relation_size(relid) DESC;
If seq_scan = 0 and idx_scan = 0:
- Stream a compressed dump to cold object storage:
pg_dump -t '"Backup_DoNotDelete"' prod_db | gzip > backup_donotdelete_2019.sql.gzaws s3 cp backup_donotdelete_2019.sql.gz s3://company-db-archives/2019/
- Execute
DROP TABLE "Backup_DoNotDelete";.
7. AKTest β The Personal Developer Scratchpad
The Origin Story
A developer named "Amit Kumar" was debugging a complex join in staging or production. He ran CREATE TABLE AKTest (id INT, note VARCHAR(50)) and inserted three rows ('test', 'asdf', NULL).
Amit left the company in 2017. His corporate SSO account was revoked, but AKTest remains in the schema catalog, backed up every night, and scanned by database introspection tools.
Prevention & Governance
- Revoke DDL Permissions: Production user accounts used by developers or web applications must never possess
CREATE TABLEpermissions on thepublicordboschemas. - Enforce Infrastructure as Code (IaC): All schema modifications must flow exclusively through version-controlled migration scripts (e.g., Flyway, Liquibase, Atlas) reviewed in pull requests.
- Ephemeral Test Environments: Provide developers with ephemeral database branches (via Neon, Supabase, or Testcontainers) so experiments never touch shared production infrastructure.
The Honourable Mentions
Beyond the top seven, several other anti-pattern tables recur across enterprise databases:
| Table Pattern | Underlying Cause | Production Risk |
|---|---|---|
Users vs tblUsers | Two developers created user tables simultaneously without noticing, or an ORM auto-generated one alongside a legacy table. | Split user authentication, inconsistent state, duplicate email registrations. |
Orders_20190417_BEFORE_MIGRATION | Pre-migration backup parachute created in the live production database. | Dead storage waste; retained for years out of fear. |
ZZ_Old_Orders | Prefixed with ZZ_ so it sorts to the bottom of the table list in database GUI clients. | "Sweeping under the rug"; still backed up and analyzed by autovacuum. |
[Copy of Sheet1] | Excel duplicate tab imported verbatim with spaces in the table name. | Requires escaped quotes or brackets in SQL code (SELECT * FROM "Copy of Sheet1"). |
Archive (0 rows) | Created for a future archival job that was never implemented. | Scheduled nightly cron jobs query an empty table forever. |
DELETE_ME | Created in 2016 by an engineer who intended to delete it within minutes. | Nobody deletes it because of the suspicion that someone named a critical table sarcastically. |
Production Database Hygiene Checklist
To maintain clean, performant relational schemas, adopt the following operational principles:
- Zero Direct DDL in Production: All schema changes must be applied via continuous integration using migration tools (Flyway, Liquibase) with automated linting rules (e.g., forbidding missing primary keys, unindexed foreign keys, and tables with spaces or uppercase prefixes).
- Scheduled Catalog Sweeps:
Automate a monthly query against performance metrics (
pg_stat_user_tablesor SQL Server DMVs) to flag tables with zero reads over a 90-day rolling window. - Strict Constraints on Singleton Tables:
Never allow an unconstrained single-row table. Enforce
CHECK (id = 1). - Declarative Partitioning for Event Logs: Any table expected to exceed 10 million rows (audit logs, webhooks, transaction history) must be partitioned by timestamp from day one, paired with automated partition dropping or detachment.
- No Parachutes in Hot Storage:
Take snapshots and Point-in-Time Recovery (PITR) backups via the storage or database engine layerβnever by copying tables (
CREATE TABLE backup_... AS SELECT *) directly inside production database storage.
