Using DDL to Manage Tables and Their Relationships: Complete Guide
DDL is the part of SQL used to create and change the structure of a database.
CREATE TABLE creates a new table, ALTER TABLE changes its structure
as requirements evolve, and DROP TABLE removes a table when it is no longer
needed. Constraints are an important part of this structure too — PRIMARY KEY
constraints identify rows uniquely, FOREIGN KEY constraints maintain relationships
between tables, and CHECK constraints prevent invalid values from being stored.
This guide covers DDL statements and database constraints with practical examples using the same
customer and order tables throughout the series, followed by twenty-one questions commonly asked
in SQL interviews.
🎯 Key Takeaways
CREATE TABLE defines structure in one statement
ALTER TABLE changes columns and constraints
Primary key is unique; foreign key links tables
CHECK enforces a rule on column values
DROP vs TRUNCATE — one removes structure, one keeps it
21 Interview QuestionsBeginner to AdvancedReal ExamplesUpdated Oct 7, 2026
KW
Kailas Warade
✓ Verified Author
Data Engineer · Oracle Certified · SQL & PL/SQL
Kailas works with SQL, PL/SQL, Python, and PySpark to build data pipelines.
He writes the SQL interview guides at Sankalan Data Tech, focused on
practical, real-world answers.
🏆 Oracle Certified💼 Data Engineer📝 SQL & PL/SQL Guides📍 Pune, India
DDL stands for Data Definition Language. It is the part of SQL used to define and change
the structure of a database — creating tables, adding or changing columns, defining
constraints, and removing database objects when they are no longer needed. In simple
terms, DML changes the data inside a table, while DDL changes the structure that holds
that data.
The difference between DDL and DML is important in day-to-day database work. DML
statements such as INSERT, UPDATE, and DELETE
change table data and are normally handled within transactions. DDL statements such as
CREATE, ALTER, DROP, and TRUNCATE
change database objects and have different transaction behavior depending on the
database system. For example, Oracle performs an implicit commit around DDL statements,
so you should treat DDL changes carefully and have appropriate backups or recovery
procedures in place.
Four DDL commands cover many of the schema changes you will make in SQL.
CREATE creates a new database object, ALTER changes an
existing object, DROP removes an object, and TRUNCATE removes
all rows from a table while keeping its structure. These commands are used when creating
schemas, adding columns, changing constraints, preparing database releases, and cleaning
up objects that are no longer required.
The Sample Schema We Use in Every Example
Across the series we have been using two tables — customers and
orders. Every DDL example in this guide either creates these tables from
scratch, changes them, or demonstrates constraints on them. If you have been following the
earlier pages, the data will look familiar.
Three things about this schema are deliberate and matter for DDL discussions:
Every order has a valid customer_id pointing at a real customer. That is what a foreign key constraint enforces.
Fiona Mart has a NULL credit limit, which is fine for a nullable column but would fail a NOT NULL constraint.
Region has only three distinct values, which is why CHECK constraints on a small set sometimes make sense.
CREATE TABLE — Bringing a Table Into Existence
A CREATE TABLE statement is a blueprint. You list every column, its data
type, and any constraints that column has to satisfy. Once the statement runs, the
structure exists — empty, waiting for data.
Creating the customers table
CREATE TABLE customers (
customer_id VARCHAR2(10) PRIMARY KEY,
customer_name VARCHAR2(100) NOT NULL,
region VARCHAR2(20),
credit_limit NUMBER(12,2)
);
Four columns. customer_id is the primary key — the identifier that makes
each customer row unique. customer_name is required — the database refuses
any insert without a name. region and credit_limit are
optional — they can be left NULL.
The data types used here are Oracle-flavoured. The equivalent in other databases:
VARCHAR2(n) → VARCHAR(n) in most other databases
NUMBER(p,s) → DECIMAL(p,s) or NUMERIC(p,s)
Those differences matter for a migration, but the concept of a typed column is universal.
Every database asks the same question when it sees a CREATE TABLE: what kind of values
can this column hold, and what has to be true about those values.
This is where DDL starts doing something more interesting than just declaring columns.
The CONSTRAINT fk_orders_customer block tells the database that
orders.customer_id must match a real customers.customer_id
value. From this point on, any insert into orders with a customer_id that
does not exist in customers fails immediately.
That constraint is the single most valuable thing in the whole schema. It is what prevents
a report from running against an order whose customer has been deleted. It is what makes
"join orders to customers" a reliable operation. Without it, you would have to trust every
application that touches the table — and trust is not a constraint you can rely on in
production.
The rules worth remembering
Every column must have a data type. You cannot leave it unspecified.
The primary key is required in most production tables. It is not strictly enforced by every database, but the practice is universal.
A NOT NULL column cannot hold missing values. Attempting to insert one fails.
Foreign keys can be declared inline with the column, or as a separate CONSTRAINT clause. The separate form is easier to name and reference later.
Primary Keys and Foreign Keys
Two constraints do most of the relationship work in any schema. Primary keys make rows
findable. Foreign keys make relationships real.
Primary key — the row's identity
ALTER TABLE customers
ADD CONSTRAINT pk_customers PRIMARY KEY (customer_id);
A primary key guarantees two things: no two rows share the same value, and the value
cannot be NULL. It is what makes a customer row uniquely identifiable.
The database backs that guarantee with a unique index — you do not have to create the
index yourself. When you declare a primary key, the index is created automatically and
the database uses it both for enforcement and for lookups.
A foreign key says: the value in this column must exist in the referenced column of
another table. If you try to insert an order with customer_id C999 and no
such customer exists, the database rejects the insert.
The same constraint also protects against deletes. If you try to delete a customer who
still has orders, the database refuses — unless you have specified
ON DELETE CASCADE, which says "when the parent is deleted, delete the child
rows too."
ALTER TABLE orders
DROP CONSTRAINT fk_orders_customer;
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
ON DELETE CASCADE;
With ON DELETE CASCADE, deleting a customer automatically deletes all their
orders. That is sometimes what you want (order history goes with the customer) and
sometimes exactly what you do not (you accidentally lose years of transaction history).
The safe default is the plain foreign key that refuses the delete — it makes you think
twice.
The one index you always create
The primary key gets an index automatically. The foreign key does not. On a table with
a heavy join load, every join that matches orders to customers scans the orders table
looking for the matching customer_id unless there is an index on that column.
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
It is the single most valuable index in a typical relational schema, and it is easy to
forget because the constraint works without it. The constraint works — the queries do
not.
ALTER TABLE — Reshaping a Table
Schemas evolve. New requirements mean new columns. Old features get retired and their
columns go with them. ALTER TABLE is the statement that reshapes an existing
table without recreating it from scratch — and without losing the data inside it.
What ALTER TABLE can do
Add a new column
Modify an existing column's type or constraints
Rename a column
Drop a column
Add, modify, or drop constraints
Rename the table itself (with a separate statement in some databases)
Every one of those operations preserves the data in the columns that are not touched. A
new column starts as NULL for existing rows, an altered type converts the existing values,
a dropped column takes its data with it. Nothing else moves.
Adding a column
ALTER TABLE customers
ADD contact_email VARCHAR2(100);
After that statement, every existing customer row has a new column called
contact_email with a value of NULL. New inserts can provide it or leave it
unset.
If you want the column to be NOT NULL from the start, you need to provide a default value
so existing rows can be filled in:
ALTER TABLE customers
ADD contact_email VARCHAR2(100) DEFAULT 'unknown@example.com' NOT NULL;
The DEFAULT clause supplies a value for existing rows. Every customer that
already exists gets the placeholder email. If you drop the DEFAULT later, new inserts
that omit the column fall back to NULL — which the NOT NULL constraint will then reject.
Rename and drop
-- Oracle and PostgreSQL
ALTER TABLE customers RENAME COLUMN contact_email TO email;
-- Drop a column
ALTER TABLE customers DROP COLUMN email;
-- Oracle with CASCADE for columns referenced by constraints
ALTER TABLE customers DROP COLUMN email CASCADE CONSTRAINTS;
The rename is cosmetic — the data stays, only the column's name changes. The drop removes
the column and every value in it. That second operation is irreversible, and there is no
undo once it commits.
Dropping a column that has a constraint on it fails unless you also drop the constraint
first, or use CASCADE CONSTRAINTS in Oracle. That is not a nuisance — it
is the database asking you to acknowledge what you are about to remove.
Adding, Modifying, Renaming, Dropping Columns
The four column operations that come up most often in day-to-day schema work. Each looks
small and matters more than it seems.
Adding a column
ALTER TABLE customers
ADD loyalty_tier VARCHAR2(20);
The new column starts empty for every existing row. On a table with a hundred million
rows, adding a column that is nullable is instant in most databases — they do not
physically rewrite the table. Adding a NOT NULL column with a default, on the other hand,
may rewrite the entire table.
Modifying a column's type
-- Oracle / MySQL
ALTER TABLE customers MODIFY credit_limit NUMBER(15,2);
-- SQL Server / PostgreSQL
ALTER TABLE customers ALTER COLUMN credit_limit DECIMAL(15,2);
Widening a numeric column (from NUMBER(12,2) to NUMBER(15,2))
usually succeeds — the existing values still fit. Narrowing one fails if any existing
value does not fit the new size. The database will not silently truncate your data.
Renaming a column
-- Oracle / PostgreSQL
ALTER TABLE customers RENAME COLUMN loyalty_tier TO tier_level;
-- MySQL
ALTER TABLE customers CHANGE COLUMN loyalty_tier tier_level VARCHAR2(20);
-- SQL Server
EXEC sp_rename 'customers.loyalty_tier', 'tier_level', 'COLUMN';
Oracle and PostgreSQL use a clean rename that only mentions the two names. MySQL's
CHANGE COLUMN requires the type to be repeated. SQL Server uses a stored
procedure rather than a DDL statement, which surprises people the first time they try
it.
Renaming a column is one of the riskiest schema changes in production. Every query that
references the old name, every view, every stored procedure, every report — all of them
break the moment the rename commits. Some teams avoid renaming altogether and use a
synonyms-style approach instead, keeping the old name as an alias until the codebase has
moved over.
Dropping a column
ALTER TABLE customers DROP COLUMN tier_level;
The column and every value in it are removed permanently. Same warning as the rename —
any code that depends on the column fails the moment this commits. It is worth a careful
search across the codebase before dropping anything, because the database will not warn
you about dependencies.
Managing Constraints
Constraints are rules the database enforces on data. They come in a small handful of
types, and most of them can be added or dropped after a table has been created.
The five common constraint types
Constraint
What it enforces
Example
PRIMARY KEY
Unique, non-NULL identifier
PRIMARY KEY (customer_id)
FOREIGN KEY
Reference to a primary key in another table
FOREIGN KEY (customer_id) REFERENCES customers
UNIQUE
No duplicates (allows NULLs in some databases)
UNIQUE (customer_name)
NOT NULL
Value cannot be NULL
customer_name NOT NULL
CHECK
A rule on allowed values
CHECK (credit_limit >= 0)
Adding a constraint to an existing table
ALTER TABLE customers
ADD CONSTRAINT uq_customer_name UNIQUE (customer_name);
That adds a UNIQUE constraint to a table that has already been created. Adding it fails
if the table currently violates the rule — if two customers already share the same name,
the database refuses to add the constraint until you fix the duplicates.
That is a good thing. It means every constraint added later is validated against existing
data, so you cannot accidentally add a rule that half your rows break.
Dropping a constraint
ALTER TABLE customers DROP CONSTRAINT uq_customer_name;
-- MySQL uses a slightly different form for foreign keys
ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;
Dropping a constraint removes the rule. It does not touch the data — every existing row
stays exactly as it is. What changes is that future inserts and updates are no longer
checked against that rule.
The reason to drop a constraint is usually to replace it with a different one. Since most
databases do not support directly modifying a constraint, the workflow is drop-then-add:
ALTER TABLE orders DROP CONSTRAINT fk_orders_customer;
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer_v2
FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
ON DELETE CASCADE;
Same constraint, different behaviour — the new version cascades deletes. Between the two
statements, the table briefly has no constraint, so run them in the same transaction if
your database supports transactional DDL (PostgreSQL does; Oracle and MySQL mostly do
not).
CHECK Constraints and Data Validation
A CHECK constraint enforces a rule on the values a column accepts. If an insert or update
would violate the rule, the database refuses the operation.
A simple CHECK constraint
ALTER TABLE customers
ADD CONSTRAINT chk_positive_credit
CHECK (credit_limit >= 0);
Every attempt to insert or update a customer with a negative credit limit now fails. The
rule is enforced by the database itself, so no application can accidentally violate it.
A CHECK constraint on a set of values
ALTER TABLE customers
ADD CONSTRAINT chk_valid_region
CHECK (region IN ('North', 'South', 'East', 'West'));
Region can only be one of four values. This is a lightweight way to enforce an enumerated
column without a lookup table — useful when the list is short and rarely changes, painful
when the list has to grow because you have to alter the constraint.
The NULL subtlety
CHECK constraints allow NULL. That surprises people the first time they run into it. If
you write CHECK (credit_limit >= 0), a customer with a NULL credit limit
is not checked against the rule at all — NULL is not a value, so it does not violate the
condition.
That is why CHECK and NOT NULL are separate constraints. If
you want to enforce both "not NULL" and "non-negative", you need both:
ALTER TABLE customers
ADD CONSTRAINT chk_credit_not_null
CHECK (credit_limit IS NOT NULL AND credit_limit >= 0);
That single CHECK does both. On our data, this would fail to apply because Fiona Mart
has a NULL credit limit — the database would refuse to add the constraint until that row
is fixed.
Three constraints on one table. Status must be one of four values, amount must be
positive, and customer_id must reference a real customer. Every insert is checked
against all three. That is the kind of guarantee that saves you from an entire class of
data-quality bugs downstream.
DROP and TRUNCATE — Removing Data
Two statements remove data. They differ in what they leave behind and how reversible
they are.
DROP TABLE — remove the table entirely
DROP TABLE orders;
The table is gone. Its structure, its data, its indexes, its constraints — all removed.
There is no undo. Any query that referenced the table fails immediately. Any foreign key
pointing at it is also gone unless the referencing table refused the drop.
Some databases refuse to drop a table that has foreign keys pointing at it. To force the
drop and cascade the removal to those constraints:
-- MySQL, PostgreSQL
DROP TABLE orders CASCADE;
-- Oracle
DROP TABLE orders CASCADE CONSTRAINTS;
The difference between the two is that CASCADE CONSTRAINTS drops the
referencing constraints but leaves the referencing table. The other tables become
independent of the dropped table — as if the constraint was never there.
TRUNCATE TABLE — empty a table but keep it
TRUNCATE TABLE orders;
Every row is removed. The table structure, columns, constraints, and indexes all remain.
The table is empty and ready for new data.
TRUNCATE is much faster than DELETE FROM orders on a large table because it
does not log individual row deletions. It also resets any auto-increment counter — the
next row inserted gets the starting value again. And it does not fire triggers, which is
either a feature or a trap depending on what your triggers do.
Which one to use
DROP — the table is no longer needed at all. Removes everything.
TRUNCATE — the table is needed but the data is not. Keeps the structure.
DELETE — the data is not needed but the table has to keep being used during the deletion, or you want to filter what gets removed.
The common mistake: using DELETE FROM table; on a huge table when the goal
is just to empty it. On tables with tens of millions of rows, TRUNCATE finishes in
seconds while DELETE takes minutes and fills the transaction log. Whenever you are about
to delete every row, TRUNCATE is the tool you probably want.
Defining One-to-Many and Many-to-Many Relationships
Relationships in a database are enforced by foreign keys. Two patterns cover almost every
real schema.
One customer has many orders, but each order belongs to exactly one customer. The foreign
key lives on the "many" side — in orders — because each order row needs to
point at a specific customer row.
On our data, Alice Corp has three orders and Bob Traders has two. Every order references
a customer that exists. That is exactly what the foreign key enforces.
A many-to-many relationship needs a junction table in the middle. The junction table has
two foreign keys — one pointing at each side — and its primary key is the combination of
both.
The composite primary key is what makes this work. It prevents the same customer-product
pair from appearing twice while still allowing a customer to have many products and a
product to have many customers. Without the composite primary key, the junction table
would accumulate duplicate relationships.
The table can carry extra columns too — the date a customer first bought a product, or
the quantity, or any other attribute that belongs to the pairing itself rather than to
either side alone:
first_seen and times_bought describe the relationship itself,
not the customer and not the product. That is what junction tables are for — modelling
facts that belong to a pairing.
Composite Primary Keys
A composite primary key uses more than one column to uniquely identify a row. Sometimes a
single column is not enough — the identity of a row depends on the combination of two or
more values.
Neither order_id nor product_id is unique on its own. An order
can contain many products, and a product can appear in many orders. But the pair is
unique — a given order cannot list the same product twice.
The composite primary key enforces that rule. Every insert checks the pair against the
index built on (order_id, product_id). If the combination already exists,
the insert fails.
How the enforcement works
Under the hood, the database builds a unique index on both columns together. Every query
or insert that references the primary key uses that index. The columns themselves cannot
be NULL — a composite primary key implies NOT NULL for every column in the key.
That is one thing people miss. A composite primary key on
(order_id, product_id) means neither column can be NULL. If you want to
allow NULL in one of them, you cannot use it as part of a primary key.
When to use one
Junction tables. The customer-product table from the previous section. The pairing is the identity.
Order line items. Order plus product. Different orders with the same product do not clash.
Versioned records. Document id plus version number identifies a specific revision.
Historical snapshots. Account id plus snapshot date.
The alternative to a composite primary key is a synthetic surrogate key — a single
auto-generated id column that has no business meaning. Both approaches are valid. The
composite version carries business meaning in the key itself; the surrogate version keeps
keys small and simple but loses that meaning. Different teams pick differently; the
important thing is to pick one and stay consistent across the schema.
Data Integrity with Constraints and Triggers
Constraints and triggers both enforce rules, but they work at different levels. Constraints
are declarative — you state the rule, and the database figures out how to check it.
Triggers are procedural — you write a block of code, and the database runs it in response
to an event.
Constraints — the declarative layer
CREATE TABLE customers (
customer_id VARCHAR2(10) PRIMARY KEY,
customer_name VARCHAR2(100) NOT NULL,
region VARCHAR2(20),
credit_limit NUMBER(12,2),
CONSTRAINT chk_region
CHECK (region IN ('North', 'South', 'East', 'West')),
CONSTRAINT chk_limit_positive
CHECK (credit_limit IS NULL OR credit_limit >= 0)
);
Constraints are the first line of defence. They are simple, fast, and understood by every
tool that touches the database. A reporting tool that tries to insert a bad value is
rejected the same way the application would be. There is no way to bypass the constraint.
Triggers — the procedural layer
CREATE TRIGGER trg_customer_audit
BEFORE INSERT OR UPDATE ON customers
FOR EACH ROW
BEGIN
:NEW.customer_name := UPPER(TRIM(:NEW.customer_name));
END;
That trigger runs automatically every time a row is inserted or updated. It trims
whitespace from the customer name and uppercases it before the row is written. The
application does not need to do that — the database handles it consistently for every
path in.
Triggers can do things constraints cannot. They can call other code. They can modify the
row being inserted (BEFORE triggers) or log changes to another table
(AFTER triggers). They can conditionally raise errors. Any time the rule is
more complex than a simple check, a trigger is the tool.
Which one to reach for
Constraints — for anything declarative: NOT NULL, UNIQUE, CHECK, foreign keys, primary keys.
Triggers — for procedural logic: automatic value transformations, audit logging, cross-table validation, complex business rules.
The general preference is constraints when they can express the rule. They are faster,
easier to reason about, and visible in the schema itself. Triggers carry hidden logic —
if you look at the table definition, you do not see them, so it is easy for a new
developer to be surprised by a side effect they did not expect.
When you use a trigger, document it. In the table's comments, in the migration scripts,
somewhere that a person reading the schema will notice. A trigger that nobody knows about
is a bug waiting to happen.
21 Interview Questions and Answers
These are the DDL questions that come up most often. Every answer stands on its own —
read them in any order, or work through them in sequence to build up the full picture.
/
🔍
No matching questions found
Try a keyword like CREATE, ALTER, or constraint.
DDL statements define and change the structure of database objects — tables, views,
indexes, sequences, and other schema elements. Four commands cover almost every
case: CREATE brings a new object into existence, ALTER
changes an existing one, DROP removes it permanently, and
TRUNCATE empties a table while keeping its structure.
-- Create a new table
CREATE TABLE customers (
customer_id VARCHAR2(10) PRIMARY KEY,
customer_name VARCHAR2(100) NOT NULL,
region VARCHAR2(20),
credit_limit NUMBER(12,2)
);
-- Change it — add a column
ALTER TABLE customers ADD loyalty_tier VARCHAR2(20);
-- Remove the column
ALTER TABLE customers DROP COLUMN loyalty_tier;
-- Drop the whole table
DROP TABLE customers;
The distinction from DML matters. DML changes data inside a table — insert, update,
delete — and those changes can be rolled back until you commit. DDL changes the
structure and is auto-committed by default. A DROP TABLE is
irreversible. The only recovery is a backup.
You write a CREATE TABLE statement that lists the table name, every
column with its data type, and any constraints on those columns. Once it runs, the
table structure exists and waits for data.
CREATE TABLE customers (
customer_id VARCHAR2(10) PRIMARY KEY,
customer_name VARCHAR2(100) NOT NULL,
region VARCHAR2(20),
credit_limit NUMBER(12,2)
);
Four columns with different rules. The PRIMARY KEY on
customer_id makes each row uniquely identifiable. NOT NULL
on customer_name means every row must have a name. The other two
columns are optional — they can be left NULL.
The equivalent structure in other databases uses slightly different type names.
VARCHAR2 becomes VARCHAR in MySQL and SQL Server.
NUMBER(12,2) becomes DECIMAL(12,2) or
NUMERIC(12,2). The concept of a typed, constrained column is
universal.
Once the table exists, you can add indexes for common queries, foreign keys
pointing at other tables, and additional constraints. But CREATE TABLE
itself only has to be run once — the structure persists until someone drops or
alters it.
A primary key uniquely identifies each row in its table. A foreign key points at
a primary key in another table, linking the two. Together they form the backbone
of relational integrity.
The customers table has customer_id as its primary key.
The orders table has its own primary key, order_id, plus
a foreign key on customer_id that points at the customers table.
The foreign key enforces a rule: every value in orders.customer_id
must exist in customers.customer_id. Attempting to insert an order
for a customer that does not exist fails immediately. That is what stops orphaned
orders from accumulating in the database.
The same constraint protects deletes. Trying to delete a customer who still has
orders fails unless you have specified ON DELETE CASCADE, which says
the child rows should go with the parent. The safe default — plain foreign key —
refuses the delete and forces you to think about it.
ALTER TABLE changes the structure of an existing table without
recreating it. You can add columns, modify them, rename them, drop them, and add
or remove constraints — all without losing the data that is already there.
-- Add a column
ALTER TABLE customers ADD contact_email VARCHAR2(100);
-- Modify a column's type
ALTER TABLE customers MODIFY credit_limit NUMBER(15,2);
-- Rename a column
ALTER TABLE customers RENAME COLUMN contact_email TO email;
-- Drop a column
ALTER TABLE customers DROP COLUMN email;
-- Add a constraint
ALTER TABLE customers ADD CONSTRAINT uq_customer_name UNIQUE (customer_name);
Each operation preserves the data that is not directly affected. Adding a column
leaves existing rows untouched — the new column starts as NULL. Renaming a column
does not change any values, only the name. Modifying a type converts the existing
values to the new type if they are compatible.
The syntax differs slightly between databases. Oracle and MySQL use
MODIFY for changing types. SQL Server and PostgreSQL use
ALTER COLUMN. SQL Server uses a stored procedure
(sp_rename) for renaming columns, not a DDL statement. Once you know
the pattern, translating is straightforward.
You use ALTER TABLE with the ADD clause, name the new
column, and give it a data type.
ALTER TABLE customers
ADD contact_email VARCHAR2(100);
Every existing customer row gets a new column called contact_email
with a value of NULL. New inserts can provide a value or leave it unset.
If the column should be required, you need a default so existing rows can be filled
in:
ALTER TABLE customers
ADD contact_email VARCHAR2(100)
DEFAULT 'unknown@example.com'
NOT NULL;
The DEFAULT supplies a value for every existing row. The
NOT NULL then ensures future inserts also supply something — either
an explicit value or the default.
One production consideration: adding a nullable column is usually instant because
the database just updates its metadata. Adding a NOT NULL column with a default
used to require a full table rewrite on older databases, though modern versions
of most engines have optimised this. On a table with hundreds of millions of
rows, it is worth checking the behaviour of your specific database before running
the change during peak hours.
ALTER TABLE ... DROP COLUMN removes a column and every value in it,
permanently.
ALTER TABLE customers DROP COLUMN contact_email;
The column is gone. Any code — queries, views, stored procedures, application
logic — that references the old column name fails from that point on. The database
does not track those references for you, so it is worth searching the codebase
before dropping.
If a constraint or index depends on the column, the drop fails unless you also
remove the dependent objects. Oracle allows a shortcut:
ALTER TABLE customers DROP COLUMN contact_email CASCADE CONSTRAINTS;
That drops the column and any constraints that referenced it, in one statement.
The alternative is to drop the constraint first, then the column.
Unlike adding a column, dropping one is not easily reversible. Most databases do
not keep a copy of the data. If you think you might need the column back, take a
backup or copy the values to another table first. That safety net costs a few
minutes; recovering a dropped column without one can cost much more.
Renaming a column is a DDL operation, but the syntax differs meaningfully between
databases.
-- Oracle, PostgreSQL
ALTER TABLE customers RENAME COLUMN old_name TO new_name;
-- MySQL
ALTER TABLE customers
CHANGE COLUMN old_name new_name VARCHAR2(100);
-- SQL Server
EXEC sp_rename 'customers.old_name', 'new_name', 'COLUMN';
Oracle and PostgreSQL use a clean rename — only the two names appear. MySQL's
CHANGE COLUMN requires the data type to be repeated, which is a
legacy of how MySQL handles column modification generally. SQL Server uses a
stored procedure rather than a DDL statement, which surprises people the first
time they try it.
Renaming a column is one of the riskier schema changes in production. Every query
that references the old name breaks the moment the rename commits — views, stored
procedures, application code, reporting tools. Renaming a column is often more
trouble than it is worth; the alternative is to add a new column with the desired
name, migrate data into it, and drop the old one after everything has moved over.
If you do rename, deploy the schema change and the code change together. A window
where one is updated and the other is not is exactly when the bugs happen.
You use ALTER TABLE with MODIFY (Oracle, MySQL) or
ALTER COLUMN (SQL Server, PostgreSQL).
-- Oracle, MySQL
ALTER TABLE customers MODIFY credit_limit NUMBER(15,2);
-- SQL Server, PostgreSQL
ALTER TABLE customers ALTER COLUMN credit_limit DECIMAL(15,2);
That changes the column's type from NUMBER(12,2) to
NUMBER(15,2) — widening it to allow larger values. The database
checks every existing row to make sure the new type can hold its current value.
If any row is incompatible, the change fails and nothing is modified.
Widening a numeric column usually succeeds because the values already fit. The
reverse — narrowing the column — fails whenever an existing value would be
truncated. That is a safety feature. The database refuses to silently lose data.
Changing a type between categories is harder. Converting a text column to a number
works only if every existing value can be parsed as a number. If any row contains
"abc", the change fails. Changing a date column to text is easier — every date
can be represented as text.
In production, changing a column's type on a large table is often the slowest
schema change you can make. Some databases rewrite the entire table to accommodate
the new type. Plan these changes for maintenance windows, or use online-schema
tools when the table cannot be locked.
DROP TABLE removes a table from the database permanently — structure,
data, indexes, constraints, all of it.
DROP TABLE orders;
After that statement, the table is gone. Any query that referenced it fails
immediately. The storage it occupied is freed. There is no undo.
If the table has foreign keys pointing at it from other tables, some databases
refuse the drop unless you specify a cascade option:
-- MySQL, PostgreSQL
DROP TABLE orders CASCADE;
-- Oracle
DROP TABLE orders CASCADE CONSTRAINTS;
Both forms remove the foreign key constraints that point at the dropped table.
The tables that owned those constraints are left intact — they simply no longer
reference the dropped table.
The safe practice before dropping anything in production: confirm that no other
table has a foreign key pointing at it (or accept that the constraints will go
too), confirm that no code references it, and take a backup. A drop that nobody
has thought through is one of the few DDL mistakes that cannot be fixed by
dropping and recreating — the data is gone.
CASCADE tells the database to remove any objects that depend on the
table being dropped — foreign keys from other tables, views that reference it,
and sometimes indexes.
-- MySQL, PostgreSQL
DROP TABLE orders CASCADE;
-- Oracle
DROP TABLE orders CASCADE CONSTRAINTS;
Without the cascade option, the drop fails if any foreign key references the
table. The database is protecting referential integrity — it does not want to
leave a foreign key in another table pointing at a table that no longer exists.
With the cascade option, the drop succeeds and the referencing constraints are
also removed. The other tables stay, but they are now independent of the dropped
table — as if the constraint had never existed.
The wording differs between databases. MySQL and PostgreSQL use plain
CASCADE, which removes dependent objects. Oracle uses
CASCADE CONSTRAINTS, which removes only the constraints — it does
not remove views or stored procedures that reference the table, unlike the other
databases. SQL Server requires you to drop the constraints separately before
dropping the table.
Reach for cascade when you know what depends on the table and you want them gone
too. Avoid it when you are unsure — running the plain DROP TABLE
first and reading the error message is a safer way to find out what depends on
it.
Renaming a table is a DDL operation, and like renaming a column, the syntax varies
between databases.
-- Oracle, PostgreSQL
RENAME employees TO staff;
-- MySQL
RENAME TABLE employees TO staff;
-- SQL Server
EXEC sp_rename 'employees', 'staff';
Oracle and PostgreSQL use RENAME old TO new directly.
MySQL wraps it in RENAME TABLE. SQL Server uses a stored procedure,
sp_rename, that takes the current name, the new name, and (optionally)
the type of object being renamed.
The table and its data are untouched by the rename — only the name changes. Indexes,
constraints, foreign keys, and triggers all stay attached to the table. Anything
that references the table by its old name now fails until the reference is updated.
Foreign keys pointing at the renamed table keep working, because the foreign key
references the table by its internal identifier, not by the name. Views that were
defined as SELECT * FROM employees will need to be updated if they
reference the old name explicitly — some databases resolve the view definition at
creation time, some at execution time.
You add constraints with ADD CONSTRAINT, drop them with
DROP CONSTRAINT, and — for anything else — you drop and re-add.
-- Add a unique constraint
ALTER TABLE customers
ADD CONSTRAINT uq_customer_name UNIQUE (customer_name);
-- Drop it
ALTER TABLE customers
DROP CONSTRAINT uq_customer_name;
-- Replace it with a different definition
ALTER TABLE customers
ADD CONSTRAINT uq_customer_name_v2 UNIQUE (customer_name, region);
Every constraint you add is validated against existing data. If the table already
violates the rule — duplicate customer names, say — the ADD fails and nothing
changes. Fix the data, then add the constraint.
Modifying a constraint is not usually a direct operation. Most databases do not
support ALTER CONSTRAINT. The pattern is drop-then-add. On databases
that support transactional DDL (PostgreSQL), both statements run inside a single
transaction so there is no window where the constraint is missing. On Oracle and
MySQL, the drop takes effect immediately, so a brief window exists where the
constraint is not enforced.
That window matters on production systems. Between drop and add, inserts can go
through that would have been rejected. If the constraint is doing real work, plan
the change for low-traffic periods or wrap it in a way the application can handle.
ALTER TABLE with ADD CONSTRAINT and FOREIGN KEY
creates the link between an existing column and the primary key of another table.
After that statement, orders.customer_id can only hold values that
exist in customers.customer_id. Attempting to insert an order for a
customer that does not exist fails.
The database checks every existing row when you add the constraint. If the table
already contains orphaned references — orders pointing at customers that do not
exist — the ADD fails. You have to fix the data first, which is usually a good
audit in itself.
For cascading behaviour when the parent row is deleted, add an option:
ALTER TABLE orders
ADD CONSTRAINT fk_orders_customer
FOREIGN KEY (customer_id) REFERENCES customers (customer_id)
ON DELETE CASCADE;
That says: when a customer is deleted, also delete their orders. Useful when the
child rows are genuinely dependent on the parent. Dangerous when the child rows
have value of their own — you are one DELETE away from losing a lot of history.
Not every database supports every cascade option. Oracle, PostgreSQL, MySQL, and
SQL Server all support ON DELETE CASCADE. Support for
ON UPDATE CASCADE varies. Check the docs for the specifics of your
database before relying on them.
You drop it with ALTER TABLE ... DROP CONSTRAINT, naming the
constraint.
-- Oracle, PostgreSQL, SQL Server
ALTER TABLE orders DROP CONSTRAINT fk_orders_customer;
-- MySQL
ALTER TABLE orders DROP FOREIGN KEY fk_orders_customer;
MySQL uses DROP FOREIGN KEY specifically for foreign key constraints.
Other constraint types use DROP CONSTRAINT. That is a small syntax
detail that trips people up when moving SQL between databases.
Once the constraint is dropped, the two tables are no longer linked. You can insert
orders with customer_id values that do not exist in the customers table. You can
delete a customer without the database complaining about the orders that referenced
them.
Dropping a foreign key is common during schema migrations. You might drop the
constraint temporarily to bulk-load data, then re-add it once the load is complete.
On a table with millions of rows, validating a foreign key on every insert is slow;
dropping it, loading, and re-adding is sometimes the fastest path.
The risk: during the window when the constraint is dropped, invalid data can get
in. Re-adding the constraint after the load will fail if the loaded data has
orphaned rows. The cleaner approach is to validate the load before re-adding, or
to use a bulk-load path that bypasses the constraint temporarily without dropping
it.
CREATE INDEX builds an index on one or more columns to speed up
lookups, joins, and sorts that reference those columns.
CREATE INDEX idx_customer_name
ON customers (customer_name);
After that statement, any query that filters on customer_name can use
the index instead of scanning the customers table.
For multiple columns:
CREATE INDEX idx_region_limit
ON customers (region, credit_limit);
That index supports queries that filter on region alone, or on
region and credit_limit together. It does not help
queries that filter only on credit_limit, because the index is sorted
by region first.
A unique index adds a constraint:
CREATE UNIQUE INDEX idx_customer_name_unique
ON customers (customer_name);
That prevents duplicate customer names, in addition to speeding up lookups. It is
equivalent to declaring a UNIQUE constraint, and in fact most databases implement
unique constraints by creating a unique index behind the scenes.
Indexes speed up reads and slow down writes. Every insert, update, or delete has
to maintain every index on the table. On high-write tables, the balance matters —
index what queries actually need, and drop anything that is never used.
DROP INDEX removes an index and frees the storage it occupied. The
table and its data are unaffected — only the index structure is removed.
-- Oracle, PostgreSQL, MySQL
DROP INDEX idx_customer_name;
-- SQL Server
DROP INDEX customers.idx_customer_name;
Most databases only need the index name. SQL Server requires the table name as
well, because index names are scoped to the table rather than the schema. That
is why the SQL Server syntax writes customers.idx_customer_name —
the index belongs to that table.
Once the index is dropped, any query that was using it falls back to a full table
scan. On small tables that is invisible. On large ones, the query that was running
in milliseconds now takes seconds. That is why dropping an index is not a
gratuitous cleanup operation — it is a real trade-off.
The reasons to drop an index:
It is never used (the database's usage statistics can confirm this).
It is made redundant by a composite index that covers the same queries.
The write overhead on a high-insert table outweighs the read benefit.
The column it indexed has been dropped (some databases drop the index automatically, some do not).
Before dropping in production, run the queries that used the index and confirm
they still perform acceptably without it. Once it is dropped, restoring it means
rebuilding the index — which on a large table can take a long time and is not
instantaneous.
Constraints and triggers both enforce rules, but they work at different levels.
Constraints are declarative — you state the rule and the database enforces it.
Triggers are procedural — you write code and the database runs it on specific
events.
Constraints — the declarative layer
CREATE TABLE customers (
customer_id VARCHAR2(10) PRIMARY KEY,
customer_name VARCHAR2(100) NOT NULL,
region VARCHAR2(20),
credit_limit NUMBER(12,2),
CONSTRAINT chk_region
CHECK (region IN ('North', 'South', 'East', 'West')),
CONSTRAINT chk_limit_non_negative
CHECK (credit_limit IS NULL OR credit_limit >= 0)
);
Five constraint types cover most declarative rules: PRIMARY KEY, FOREIGN KEY,
UNIQUE, NOT NULL, and CHECK. They are fast, portable, and visible in the schema.
Any tool that looks at the table sees them.
Triggers — the procedural layer
CREATE TRIGGER trg_customer_normalize
BEFORE INSERT OR UPDATE ON customers
FOR EACH ROW
BEGIN
:NEW.customer_name := UPPER(TRIM(:NEW.customer_name));
END;
That trigger trims whitespace and uppercases the customer name before the row is
written. The transformation happens automatically, for every insert and update,
regardless of which application or script did the write.
Triggers can do things constraints cannot — call other code, log changes to
another table, conditionally raise errors, modify the row being written. Any rule
that requires logic rather than a simple check is a trigger's territory.
When to use which
Prefer constraints when they can express the rule. They are faster, clearer, and
less surprising. A constraint is a rule the schema declares — anyone reading the
table definition sees it. A trigger is hidden logic — a developer looking at the
schema will not know it exists until they trace through the database's behaviour.
Use triggers for the cases that need them: complex validation, cross-table rules,
audit trails, denormalization maintenance. When you use one, document it in the
table's comments or a schema document. A trigger nobody knows about is a
debugging nightmare waiting to happen.
A CHECK constraint enforces a boolean rule on a column's values. Every insert or
update is checked against the rule; anything that violates it is rejected.
Two rules are enforced: credit limit must be non-negative, and region must be one
of the four listed values. Any attempt to insert a customer with a negative limit
or an unrecognised region fails immediately.
You can also add a CHECK to a table that already exists:
ALTER TABLE customers
ADD CONSTRAINT chk_status
CHECK (region IS NOT NULL OR credit_limit IS NULL);
That rule says: if region is missing, credit_limit must also be missing. Adding it
to an existing table validates every current row — if any row violates the rule,
the ADD fails and you have to fix the data first.
The NULL subtlety
CHECK constraints allow NULL. The expression is only evaluated when the value is
something other than NULL. On our data, chk_positive_limit would not
reject Fiona Mart's NULL credit limit — it just does not check it.
To enforce both "must have a value" and "the value must satisfy a rule", combine
them:
CHECK (credit_limit IS NOT NULL AND credit_limit >= 0)
That single CHECK rejects both NULL and negative values. On our data, adding this
constraint would fail because Fiona Mart's row would violate it.
CHECK constraints are best for rules that are stable and simple. A short list of
valid values, a range of allowed numbers, a required format. Rules that change
frequently belong in a lookup table with a foreign key, not in a CHECK that has
to be altered every time the list grows.
A composite primary key uses two or more columns together to uniquely identify each
row. Neither column is unique on its own, but the combination is.
The primary key is on the pair (order_id, product_id). A single order
can contain many products, and a single product can appear in many orders, but no
order can list the same product twice.
Adding it to an existing table works the same way:
ALTER TABLE order_items
ADD CONSTRAINT pk_order_items PRIMARY KEY (order_id, product_id);
If the table currently has duplicate pairs, the ADD fails and you have to
deduplicate first.
The NOT NULL implication
Every column in a composite primary key is implicitly NOT NULL. Attempting to
insert a row with a NULL in any of the key columns fails, because NULL cannot be
part of a unique identifier.
INSERT INTO order_items (order_id, product_id, quantity)
VALUES (NULL, 'P001', 2); -- fails: order_id cannot be NULL
That is a feature, not a bug — a row whose identity depends on a missing value is
not identifiable at all.
Composite primary keys vs surrogate keys
The alternative to a composite primary key is a surrogate key — a single
auto-generated id column with no business meaning. Both work. The composite
version carries meaning in the key itself (an order-item pair is meaningful);
the surrogate version keeps keys small and consistent but loses that meaning.
Composite keys are the natural choice for junction tables and versioned records.
Surrogate keys are common in the base entities — customers, products, orders —
because they stay stable even when business definitions change. Most well-designed
schemas use both: surrogate primary keys on the primary entities, composite keys
on the junction tables between them.
TRUNCATE TABLE removes every row from a table while keeping the
structure — columns, constraints, indexes, triggers all stay in place. The table
is empty and ready to receive new data.
TRUNCATE TABLE orders;
After that statement, orders has zero rows but the same structure it
had before. Any query against it returns no rows. New inserts work exactly as
before.
How it differs from DELETE
DELETE FROM orders; with no WHERE clause also removes every row. The
difference is in how the database does it:
Speed. TRUNCATE is dramatically faster on large tables because it does not log each row deletion.
Auto-increment. TRUNCATE resets any identity or sequence counter tied to the table. DELETE does not.
Triggers. TRUNCATE does not fire row-level triggers. DELETE does.
Rollback. DELETE can be rolled back before commit. TRUNCATE is auto-committed on Oracle and often cannot be rolled back, though PostgreSQL, MySQL, and SQL Server allow it inside a transaction.
When TRUNCATE is the right choice
Whenever you want to empty a table entirely. Reloading a staging table, resetting a
test environment, clearing a cache table — all classic TRUNCATE scenarios. The
speed advantage is dramatic on large tables; a table that takes minutes to DELETE
every row truncates in seconds.
When it is not
When you need to keep some rows. TRUNCATE cannot be filtered — it removes
everything. Any conditional cleanup uses DELETE ... WHERE.
When the table has foreign keys pointing at it. Most databases refuse to truncate a
table that other tables reference, because removing all rows would violate the
referenced constraint. You have to drop the constraints first or use DELETE.
On Oracle, TRUNCATE is a DDL statement and cannot be rolled back. That makes it one
of those operations that either works exactly as intended or destroys data — there
is no middle state. Before running it, run a
SELECT COUNT(*) against the table to confirm you are about to empty
the right one.
Relationships between tables are enforced by foreign keys. Two patterns cover
almost every real schema.
One customer, many orders. Each order belongs to exactly one customer. The foreign
key lives on the "many" side — orders — because each order row points
at a specific customer.
On our data, Alice Corp has three orders and Bob Traders has two. Every order
references a customer that exists in the parent table. That is the constraint at
work.
Many customers, many products. A customer can buy many products, and a product can
be bought by many customers. The relationship lives in a junction table with a
foreign key pointing at each side.
The composite primary key on (customer_id, product_id) prevents
duplicate pairs. The same customer can appear many times (once per product), and
the same product can appear many times (once per customer). Only the pair is
unique.
The junction table can carry extra columns too, describing the pairing itself
rather than either side alone:
first_seen and times_bought are attributes of the
relationship, not of either entity. That is what junction tables are for —
modelling facts that only exist at the intersection of two things.
Every major database supports both patterns with the same syntax. The design
decision is about which side needs the foreign key (the "many" side in one-to-many)
and whether you need a junction table at all (yes for many-to-many).
Mistakes I Keep Seeing in Interviews
These come up repeatedly. None of them are exotic, and every one of them is easy to avoid
once you know it is coming.
Mistake one: forgetting that DDL is auto-committed. Candidates assume a
DROP TABLE or ALTER TABLE can be rolled back. On Oracle and
most databases it cannot. Once the statement runs, the change is permanent.
Mistake two: not indexing foreign key columns. The foreign key constraint
works without an index, so nothing complains. But every join or check that touches the
column scans the whole table without one. Index every foreign key column, every time.
Mistake three: using DROP TABLE when the intent was to empty
the table. TRUNCATE removes every row and keeps the table; DROP
removes the table itself. If you wanted to reload data, TRUNCATE is almost always what
you meant.
Mistake four: assuming a CHECK constraint will reject NULL. It will not.
CHECK evaluates the rule only when the value is present. To also reject NULL, add a
NOT NULL or include IS NOT NULL in the CHECK condition.
Mistake five: adding a foreign key to a table with orphaned rows and
being surprised when the ALTER fails. The database checks every existing row. Fix the
data first, then add the constraint.
Mistake six: renaming a column or table without updating every query,
view, and stored procedure that references the old name. Renames break more code than any
other DDL operation, and the database does not warn you.
Follow-Up Questions Interviewers Ask
Getting the first answer right is only half the interview. What usually decides the
outcome is what happens next. Here is what to prepare for on the DDL topics.
After "What is DDL?" — expect
"What is the difference between DDL and DML?"
"Can you roll back a DROP TABLE?"
"What is TRUNCATE and how does it differ from DELETE?"
"Which DDL statements are irreversible?"
After "How do you create a table?" — expect
"What is the difference between VARCHAR and CHAR?"
"What does NOT NULL do?"
"Can you change a column's type after the table is created?"
"What happens to existing rows when you add a NOT NULL column?"
After "What is a foreign key?" — expect
"What is the difference between a primary key and a foreign key?"
"What is ON DELETE CASCADE?"
"Why should every foreign key column be indexed?"
"What happens if you try to insert a row with a foreign key that does not exist?"
After "What is a CHECK constraint?" — expect
"Does a CHECK constraint reject NULL?"
"How do you add a CHECK constraint to an existing table?"
"What happens if existing data violates the constraint you are adding?"
"What is the difference between CHECK and NOT NULL?"
After "How do you model many-to-many?" — expect
"What is a junction table?"
"How do you prevent duplicates in a junction table?"
"Can a junction table have extra columns?"
"How would you query a many-to-many relationship with joins?"
For every question above, prepare a second-level answer. The candidates who pass are not
the ones who memorised the first answer — they are the ones who kept going for another
thirty seconds and said something useful about why it matters.
Quick Quiz
Three questions to check whether the topics above have landed.
1. What is the difference between DROP TABLE and TRUNCATE TABLE?