<< Home

Chapter 7: Altering and Dropping Structures

Once a database is in use, structures evolve. You add columns, rename objects, create or remove indexes, and carefully roll out changes so applications continue to run. This chapter covers the core statements for structural change; safe patterns for index management; and practical guidance for schema migrations.

⚠️ Structural changes can lock tables or block concurrent queries. Plan changes for low-traffic windows; prefer transactional changes where supported; and always test on a copy first.

ALTER TABLE, RENAME, and DROP TABLE

ALTER TABLE changes an existing table without recreating it. Typical operations include adding or dropping columns, changing data types, setting or removing constraints, and renaming columns or the table itself.

Common ALTER TABLE patterns

-- Add a nullable column with a default (ANSI-style)
ALTER TABLE accounts
  ADD COLUMN last_login TIMESTAMP DEFAULT CURRENT_TIMESTAMP;

-- Backfill and then enforce NOT NULL (safer two-step)
UPDATE accounts SET last_login = CURRENT_TIMESTAMP WHERE last_login IS NULL;
ALTER TABLE accounts
  ALTER COLUMN last_login SET NOT NULL;           -- PostgreSQL
-- MySQL
ALTER TABLE accounts
  MODIFY last_login TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP;
-- SQL Server
ALTER TABLE accounts
  ALTER COLUMN last_login DATETIME2 NOT NULL;

-- Rename a column
-- PostgreSQL / SQLite
ALTER TABLE accounts RENAME COLUMN last_login TO last_seen;
-- MySQL 8+
ALTER TABLE accounts RENAME COLUMN last_login TO last_seen;
-- SQL Server
EXEC sp_rename 'dbo.accounts.last_login', 'last_seen', 'COLUMN';

-- Change a column type (with USING when needed)
-- PostgreSQL
ALTER TABLE orders
  ALTER COLUMN total TYPE NUMERIC(12,2)
  USING total::NUMERIC(12,2);

-- Add a CHECK constraint
ALTER TABLE accounts
  ADD CONSTRAINT chk_age CHECK (age >= 13);

-- Drop a constraint (name varies by engine)
-- PostgreSQL
ALTER TABLE accounts DROP CONSTRAINT chk_age;
-- MySQL (checks are named automatically; use SHOW CREATE TABLE to find the name)
-- SQL Server
ALTER TABLE accounts DROP CONSTRAINT chk_age;
💡 When tightening constraints, use a phased approach: add column as nullable; backfill; validate; then set NOT NULL. For massive tables, do the backfill in batches (for example by primary key ranges).

Renaming tables and columns with RENAME

Most engines provide a simple rename that preserves data and constraints.

-- Rename a table
-- PostgreSQL / SQLite
ALTER TABLE session RENAME TO session_archive;

-- MySQL
RENAME TABLE session TO session_archive;

-- SQL Server
EXEC sp_rename 'dbo.session', 'session_archive';
⚠️ Renames can break dependent objects (views, stored procedures, foreign keys) and application code. Prefer compatibility layers (views or synonyms) during a transition window.

Dropping tables with DROP TABLE

Dropping a table removes its data and metadata. Use conditional clauses to avoid errors if the table may not exist; and use dependency options carefully.

-- Safer: avoid errors if missing
DROP TABLE IF EXISTS temp_import;

-- Handle dependencies
-- PostgreSQL: CASCADE drops dependent objects; RESTRICT fails if any exist
DROP TABLE report CASCADE;
DROP TABLE report RESTRICT;

-- MySQL: CASCADE/RESTRICT are parsed but not enforced for tables; check FKs
DROP TABLE report;

-- SQL Server
DROP TABLE IF EXISTS dbo.report;
⚠️ DROP TABLE is irreversible without a backup. Consider ALTER TABLE ... RENAME TO ..._archive for a deprecation period, or export data before removal.

Quick reference: common operations and dialect notes

Operation PostgreSQL MySQL SQL Server SQLite
Add column ALTER TABLE ... ADD COLUMN ... ALTER TABLE ... ADD COLUMN ... ALTER TABLE ... ADD ... ALTER TABLE ... ADD COLUMN ...
Rename column ALTER TABLE ... RENAME COLUMN a TO b ALTER TABLE ... RENAME COLUMN a TO b (8+) sp_rename 'tbl.a','b','COLUMN' ALTER TABLE ... RENAME COLUMN a TO b (3.25+)
Change type ALTER COLUMN ... TYPE ... USING ... MODIFY COLUMN ... ALTER COLUMN ... Often requires create-copy-swap
Drop constraint DROP CONSTRAINT name Drop by altering column/index; FK by name DROP CONSTRAINT name Limited; often recreate table
Rename table ALTER TABLE ... RENAME TO ... RENAME TABLE ... TO ... sp_rename 'schema.tbl','new' ALTER TABLE ... RENAME TO ...
Drop table DROP TABLE [IF EXISTS] ... [CASCADE|RESTRICT] DROP TABLE [IF EXISTS] ... DROP TABLE IF EXISTS ... DROP TABLE IF EXISTS ...
💡 SQLite supports many alter operations today, but complex changes (for example dropping a column in older versions) may still require the copy-table pattern: create a new table; copy data; drop the old table; rename the new table.

Managing indexes

Indexes accelerate reads by organizing data for fast lookups. Design them for the queries you run; balance read speed against write overhead and storage.

Creating and dropping indexes

-- Basic non-unique index
CREATE INDEX idx_users_email ON users (email);

-- Unique index (enforces no duplicates)
CREATE UNIQUE INDEX uq_users_email ON users (email);

-- Composite (multi-column) index
CREATE INDEX idx_orders_customer_date ON orders (customer_id, order_date);

-- Functional / expression index
-- PostgreSQL
CREATE INDEX idx_users_email_lower ON users ((lower(email)));
-- SQLite
CREATE INDEX idx_users_email_lower ON users (lower(email));
-- MySQL (use generated column prior to functional indexes)
ALTER TABLE users ADD COLUMN email_lower VARCHAR(255) GENERATED ALWAYS AS (lower(email)) STORED;
CREATE INDEX idx_users_email_lower ON users (email_lower);

-- Include (covering) columns
-- PostgreSQL 11+
CREATE INDEX idx_orders_cover ON orders (customer_id, order_date) INCLUDE (status);
-- SQL Server
CREATE INDEX idx_orders_cover ON dbo.orders (customer_id, order_date) INCLUDE (status);

-- Partial / filtered index
-- PostgreSQL
CREATE INDEX idx_orders_open ON orders (customer_id) WHERE status = 'OPEN';
-- SQL Server
CREATE INDEX idx_orders_open ON dbo.orders (customer_id) WHERE status = 'OPEN';
-- SQLite supports partial indexes with WHERE
CREATE INDEX idx_orders_open ON orders (customer_id) WHERE status = 'OPEN';

-- Drop indexes (syntax differs)
-- PostgreSQL / SQLite
DROP INDEX IF EXISTS idx_users_email;
-- MySQL
DROP INDEX idx_users_email ON users;
-- SQL Server
DROP INDEX idx_users_email ON dbo.users;
⚠️ Indexes speed up reads but slow down writes and increase storage. Measure with your real workload; remove dead indexes that do not help any queries.

Choosing columns and order

💡 Start from the slow queries: examine execution plans, add the smallest effective index, and re-check the plan. Avoid speculative indexing.

Rebuilding and concurrent indexing

-- Build an index without blocking writes (engine-specific)
-- PostgreSQL
CREATE INDEX CONCURRENTLY idx_events_created_at ON events (created_at);

-- Rebuild or reorganize to defragment
-- SQL Server
ALTER INDEX ALL ON dbo.orders REORGANIZE;    -- light
ALTER INDEX ALL ON dbo.orders REBUILD;       -- heavy
⚠️ Concurrent index builds reduce lock time but can run longer and use more temporary space. Read your engine’s guidance for maintenance windows.

Schema migrations

Schema migrations are controlled, versioned changes to database structures applied in sequence. A reliable process reduces risk and keeps environments aligned.

Versioning and tracking

-- A simple migrations ledger
CREATE TABLE schema_migrations (
  version VARCHAR(50) PRIMARY KEY,
  applied_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

-- Mark a migration as applied
INSERT INTO schema_migrations (version) VALUES ('2025-11-02_1200_add_last_seen');

Each migration is an ordered script with an id (for example 2025-11-02_1200_add_last_seen.sql) that can be applied once. Tools like Flyway and Liquibase manage this automatically; the principle is the same.

Designing safe, incremental changes

💡 For hot systems, use “expand and contract”: expand the schema to support new code; deploy new code; backfill; switch reads/writes; contract old columns or tables only after a cooling period.

Example migration set

-- 001_expand_add_column.sql
ALTER TABLE accounts
  ADD COLUMN last_seen TIMESTAMP;

-- 002_backfill_last_seen.sql  (run in batches in application code or a job)
UPDATE accounts
SET last_seen = GREATEST(last_login, created_at)
WHERE last_seen IS NULL
  AND id BETWEEN :min_id AND :max_id;

-- 003_enforce_not_null.sql
-- PostgreSQL
ALTER TABLE accounts ALTER COLUMN last_seen SET NOT NULL;
-- MySQL
ALTER TABLE accounts MODIFY last_seen TIMESTAMP NOT NULL;
-- SQL Server
ALTER TABLE accounts ALTER COLUMN last_seen DATETIME2 NOT NULL;

-- 004_add_index.sql
-- PostgreSQL (concurrent)
CREATE INDEX CONCURRENTLY idx_accounts_last_seen ON accounts (last_seen);
-- MySQL
CREATE INDEX idx_accounts_last_seen ON accounts (last_seen);
-- SQL Server
CREATE INDEX idx_accounts_last_seen ON dbo.accounts (last_seen);

-- 005_contract_drop_old.sql
-- After monitoring period
ALTER TABLE accounts DROP COLUMN last_login;
⚠️ Test migrations against production-like data volumes. Small tables hide lock times and long-running backfills. Use staged rollouts and measure actual impact.

Blue-green and compatibility layers

For renames that risk breaking code, introduce a compatibility layer:

-- Keep old name available during transition
-- PostgreSQL view
CREATE VIEW session AS SELECT * FROM session_archive;

-- SQL Server synonym
CREATE SYNONYM dbo.session FOR dbo.session_archive;

-- MySQL view
CREATE VIEW session AS SELECT * FROM session_archive;
💡 Maintain the compatibility layer until all consumers have switched; then remove it in a later migration with a clear version tag.

Structure of a migration file

-- <version>__<summary>.sql
-- 2025-11-02__rename_session_to_session_archive.sql

BEGIN;  -- or implicit transaction if the engine handles it

-- … DDL / DML statements here

COMMIT;

Where the engine does not support transactional DDL (or only for some statements), split the migration into safe chunks and ensure idempotency checks are present (for example IF EXISTS, IF NOT EXISTS, or guards in application code).

⚠️ Some statements are not fully transactional in certain engines (for example MySQL implicit commit for ALTER TABLE in some modes). Read engine notes and plan accordingly.

With careful use of ALTER TABLE, disciplined index management, and versioned migrations, you can evolve a schema smoothly while keeping systems available.