Managing Indexes, Synonyms and Sequences: Complete Guide
Indexes, sequences, and synonyms are three database objects that solve very different problems.
An INDEX helps the database find rows faster, although it also adds some overhead to
INSERT, UPDATE, and DELETE operations. A
SEQUENCE generates numeric values that are commonly used for unique IDs, while a
SYNONYM provides an alternate name for a database object and can make SQL easier to
write. These objects do not change the data itself, but they can have a big impact on database
performance, application design, and how easily SQL can access database objects. This guide
explains indexes, sequences, and synonyms with practical examples using the same customer and
order tables throughout the series, followed by twenty-five questions commonly asked in SQL
interviews.
🎯 Key Takeaways
Indexes speed up reads, slow down writes
Composite indexes need the right column order
Sequences generate unique numbers safely
Synonyms alias objects across schemas
EXPLAIN shows whether an index is being used
25 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
Indexes, sequences, and synonyms are database objects that often become important when
you start working with real applications and larger databases. A query may become slow
as a table grows, an application may need a reliable way to generate unique IDs, or a
SQL statement may need to reference an object in another schema. Indexes, sequences, and
synonyms address these different requirements.
An index is a data structure maintained by the database to help find
rows without scanning the entire table. A sequence generates numeric
values that are commonly used for primary keys and other unique identifiers, and it can
safely generate values for multiple sessions. A synonym provides an
alternate name for a database object, so you can reference customers instead
of writing hr.customers, when the appropriate synonym and privileges exist.
Each object has its own trade-offs. Indexes can improve query performance but add storage
and can increase the work required for INSERT, UPDATE, and
DELETE operations. Sequence values are generated independently of the
transaction, so gaps can occur when a transaction is rolled back. Synonyms can make SQL
easier to maintain, but changes to the underlying object or schema still need to be
managed. This guide covers indexes, sequences, and synonyms with practical examples
using the same customer and order tables throughout the series.
The Sample Data We Use in Every Example
Same customers and orders tables as the rest of the series. Every index, sequence and
synonym below refers back to these tables, and you can run the examples side by side with
the other pages in this tutorial set.
Two things in this data matter for the topics in this guide. Fiona Mart has a NULL
credit limit, which affects how indexes handle NULL values. And the region column has
only three distinct values, which matters when you think about whether an index on that
column is worth creating.
Indexes — The Fundamentals
An index is a structure the database maintains alongside a table so it can find specific
rows without scanning every one. The mental model that helps: it is like the index at the
back of a book. Without one, you would flip through every page looking for a topic. With
one, you go straight to the page number.
What an index costs you
Nothing in a database is free. An index makes reads faster and writes slower, because
every time a row is inserted, updated, or deleted, the index has to be maintained too.
On a table with three indexes, one insert becomes four writes — the row plus three index
updates.
That is why a table with a heavy write load should not carry more indexes than it needs.
A table with a heavy read load should carry indexes on every column that appears in a
frequent WHERE clause. The decision is entirely about which side you are on.
Creating an index on a single column
CREATE INDEX idx_customer_name
ON customers (customer_name);
That creates an index on customers.customer_name. Any query that filters or
sorts on that column can now use the index instead of scanning the table.
SELECT customer_id, customer_name
FROM customers
WHERE customer_name = 'Alice Corp';
Without the index, the database reads every row of the customers table to find the one
that matches. With it, the database looks up "Alice Corp" in the index and reads only the
row it points to. On a table with millions of rows, that difference is measured in
seconds versus milliseconds.
Dropping an index
DROP INDEX idx_customer_name;
Removing an index frees its storage and removes the maintenance cost from writes. It also
removes the speed-up for any queries that were using it. Dropping is not just cleanup —
it is a real trade-off, and the right call depends on whether the index was earning its
keep.
Simple, Unique, and Composite Indexes
Three kinds of indexes cover most of the ground. They differ in what they index and what
they enforce.
Simple index — speed without constraints
CREATE INDEX idx_customer_region
ON customers (region);
A simple index just speeds up lookups. Duplicates are allowed — the index stores every
region value, and multiple rows can share the same one. Speed is the only thing on offer.
Unique index — speed plus a guarantee
CREATE UNIQUE INDEX idx_customer_name_unique
ON customers (customer_name);
A unique index does everything a simple one does, plus it prevents duplicate values.
Attempting to insert two customers with the same name fails with an error. That makes it
the standard way to enforce business rules like "no two customers can share an id" — the
database stops the duplicate at the source rather than relying on application-level
checks.
Composite index — multiple columns in one
CREATE INDEX idx_region_limit
ON customers (region, credit_limit);
That index covers both region and credit_limit. It speeds up
queries that filter on both, or filter on region alone. It does not speed up queries that
filter only on credit_limit, because the index is sorted by region first.
That is the rule that catches people out: a composite index is only useful when the query
filters on the leftmost columns. The index (region, credit_limit) helps:
-- Uses the index
SELECT * FROM customers WHERE region = 'North';
SELECT * FROM customers WHERE region = 'North' AND credit_limit > 50000;
But it does not help this one:
-- Cannot use the index efficiently
SELECT * FROM customers WHERE credit_limit > 50000;
Order of columns matters. If you have a query that filters on credit_limit alone, you
need a separate index, or a composite index with credit_limit as the leading column.
Side-by-side comparison
Type
Speeds up
Enforces
Best for
Simple
WHERE, JOIN, ORDER BY on one column
Nothing
Frequently queried single columns
Unique
Same as simple
No duplicates
Natural keys (email, username, code)
Composite
Queries filtering on multiple columns
Optionally unique
Multi-column WHERE and ORDER BY
Managing Indexes Day to Day
Indexes need to be curated. Adding them for every column is a common beginner mistake and
one of the biggest causes of slow writes on mature systems. Managing them well means
adding only what pays for itself and removing what does not.
The lifecycle of an index
Create it for a column or combination of columns that a frequent query filters on.
Test it by running EXPLAIN against the query and confirming the plan now uses the index.
Monitor it. Most databases have views that show which indexes have been used and how often.
Rebuild it periodically if the data has churned a lot and the index has fragmented.
Drop it once nothing uses it, freeing storage and reducing write overhead.
Finding an index's definition
-- Oracle
SELECT index_name, table_name, uniqueness
FROM user_indexes
WHERE table_name = 'CUSTOMERS';
SELECT index_name, column_name
FROM user_ind_columns
WHERE table_name = 'CUSTOMERS';
-- MySQL
SHOW INDEX FROM customers;
-- PostgreSQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'customers';
Every database has its own way of reporting indexes, but the information is the same:
what the index is called, what table it belongs to, and which columns it covers.
Over-indexing is a real cost
Three indexes on a table means every INSERT does four writes. Eight indexes means nine.
On a high-write table that multiplies into serious slowdown. The habit worth building:
whenever you add an index, ask which existing index it might make redundant. A composite
index (region, credit_limit) makes a simple index on region unnecessary, because the
composite one already handles those queries.
Improving Query Performance with Indexes
Indexes are the single biggest performance lever most applications have. Here is where
they do the most good.
WHERE clauses
CREATE INDEX idx_region ON customers (region);
SELECT customer_name, credit_limit
FROM customers
WHERE region = 'North';
Without the index, the database reads every customer row and checks the region. With it,
only the North-region rows are touched. On a small table this is invisible. On a large
one it is a different order of magnitude.
JOIN conditions
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
SELECT c.customer_name, o.order_id
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id;
The index on orders.customer_id lets the database look up each customer's
orders directly instead of scanning the orders table for each match. Every join column
on the "many" side of a one-to-many relationship should have an index. It is the single
most impactful index you can create in a typical relational schema.
ORDER BY
CREATE INDEX idx_order_amount ON orders (order_amount);
SELECT order_id, order_amount
FROM orders
ORDER BY order_amount DESC;
When the column you sort by has an index, the database reads the rows in sorted order and
skips the sort step entirely. On large result sets, that saves real time and memory.
Covering indexes
CREATE INDEX idx_covering ON orders (customer_id, order_id, order_amount);
SELECT order_id, order_amount
FROM orders
WHERE customer_id = 'C001';
A covering index contains every column the query needs. The database reads the query
answer straight from the index and never touches the table at all. It is the fastest
form of lookup you can get, and it is worth the extra columns when a query is called
often enough.
The order to make decisions in: find the slow query, look at which
columns it filters and joins on, check whether those columns are indexed, and add an
index if they are not. Every other consideration — composite, unique, covering — is a
refinement of that basic first step.
Reading EXPLAIN to See What Is Happening
Guessing about indexes is a waste of time. The database will tell you exactly what it is
doing if you ask. That is what EXPLAIN is for.
The basic query
EXPLAIN
SELECT customer_name
FROM customers
WHERE customer_id = 'C001';
The output shows how the database plans to execute the query. Different databases format
the output differently, but the essential content is the same: which tables are involved,
which indexes are used, and how many rows the plan expects each step to produce.
What to look for in the plan
The single most important thing to watch for is the word "scan." When the plan says
"sequential scan" or "full table scan" on a large table, the database is reading every
row to find the ones you want — which is exactly what an index is supposed to prevent.
If the same query used an index, the plan would say "index scan" or "index seek."
Testing whether an index makes a difference
-- Before the index
EXPLAIN SELECT * FROM customers WHERE region = 'North';
-- Output likely says: Seq Scan on customers
-- Create the index
CREATE INDEX idx_region ON customers (region);
-- After the index
EXPLAIN SELECT * FROM customers WHERE region = 'North';
-- Output now says: Index Scan using idx_region on customers
That before-and-after is how you prove an index is doing its job. Run EXPLAIN, note the
plan, create the index, run EXPLAIN again, and confirm the plan changed.
When EXPLAIN surprises you
Sometimes you create an index and the optimizer ignores it. That usually means one of
three things: the table is small enough that a scan is cheaper than an index lookup, the
query filters on a function of the column (like WHERE UPPER(region) = 'NORTH',
which cannot use a plain index on region), or the statistics are stale and the optimizer
is misjudging the size of the table. Fixing any of those is a small change — but only
after you have seen the plan and identified the real cause.
Fragmentation and Index Maintenance
Indexes are not static. As rows are inserted, updated, and deleted, the physical layout
of the index drifts away from ideal. Pages fill unevenly. Reads require more I/O than
they should. This is called fragmentation, and it is why indexes need periodic
maintenance.
How to check fragmentation
-- SQL Server
SELECT *
FROM sys.dm_db_index_physical_stats(NULL, NULL, NULL, NULL, 'DETAILED');
That view reports the fragmentation percentage for every index in the database. Oracle,
MySQL, and PostgreSQL have equivalent statistics, though the exact query differs. Any
index above roughly 30% fragmentation is a candidate for a rebuild.
Rebuilding an index
ALTER INDEX idx_customer_name REBUILD;
A rebuild reconstructs the index from scratch. It drops the fragmented structure and
builds a compact one, which is faster to read. The cost is that a rebuild is resource-
intensive, so it usually runs during a maintenance window.
Reorganising an index
ALTER INDEX idx_customer_name REORGANIZE;
A reorganise is the lighter-weight alternative. It reorders the existing pages without
rebuilding from scratch. It is less disruptive than a rebuild but also does less to
reduce fragmentation. The usual rule: moderate fragmentation (5–30%) gets a reorganise,
heavy fragmentation (30%+) gets a rebuild.
Fill factor — leaving room for growth
CREATE INDEX idx_customer_name
ON customers (customer_name)
WITH (FILLFACTOR = 80);
A fill factor of 80 tells the database to fill each index page only 80% full at build
time, leaving 20% free for future inserts. That reduces the number of page splits as the
index grows, which slows down fragmentation. The trade-off is that the index takes more
space — the same reasoning applies at a smaller scale to leaving blank space in a filing
cabinet so new folders have a home.
Where you actually see this matter is on high-churn tables — orders, log entries,
events — where the data changes constantly. A table that is only ever read does not
fragment, so its indexes can be built with a fill factor of 100 and left alone.
Index Hints and When to Use Them
Usually the optimizer does a good job of picking which index to use. Sometimes it does
not. Index hints let you override its choice for a specific query.
Oracle syntax
SELECT /*+ INDEX(customers idx_region) */
customer_name, credit_limit
FROM customers
WHERE region = 'North';
The /*+ INDEX(table_name index_name) */ comment is a hint to Oracle's
optimizer: use the idx_region index on the customers table.
If the hint is wrong — say the index does not exist — the optimizer silently ignores it
and picks its own plan.
MySQL syntax
SELECT customer_name
FROM customers USE INDEX (idx_region)
WHERE region = 'North';
MySQL supports USE INDEX, FORCE INDEX, and
IGNORE INDEX to nudge or override the optimizer's choices. Same idea,
different syntax.
SQL Server syntax
SELECT customer_name
FROM customers WITH (INDEX(idx_region))
WHERE region = 'North';
SQL Server uses the WITH (INDEX(...)) clause on the table reference.
PostgreSQL has no index hints
PostgreSQL deliberately does not support hints. The design philosophy is that the
optimizer should be trusted, and that if it makes a bad choice, the fix is better
statistics or a rewrite of the query, not a hint that locks in a decision that might
become wrong as data changes.
The trap with hints: a hint tells the optimizer to use a specific index
today. As the data grows and changes, that index may stop being the right choice — but
the hint keeps forcing it. A hint that helped in January can silently hurt in December,
and the only way to notice is to keep testing. Reach for hints as a last resort, not a
first one.
Sequences — Generating Unique Numbers
A sequence is a database object that hands out unique numbers. It exists so that primary
key columns do not need a row-by-row "what is the next number?" check — a check that
would serialise concurrent inserts and become a bottleneck.
Creating a sequence
CREATE SEQUENCE order_seq
START WITH 1001
INCREMENT BY 1
NOCACHE;
That defines a sequence called order_seq that starts at 1001 and increases
by 1 each time it is called. NOCACHE means the database does not pre-fetch
values — every call goes straight to disk. That guarantees no gaps but is slower under
load.
Getting the next value
SELECT order_seq.NEXTVAL FROM dual;
That returns the next number in the sequence — 1001 the first time, 1002 the second, and
so on. dual is Oracle's one-row dummy table. Every other database supports
the same concept with slightly different syntax — nextval('order_seq') in
PostgreSQL, NEXT VALUE FOR order_seq in SQL Server.
Each insert calls the sequence and gets the next unique order id. The application does
not need to compute the id. The database does not need a lock to prevent two sessions
from getting the same number. The sequence is designed for exactly this pattern.
NEXTVAL and CURRVAL
-- Advance the sequence and get the new value
SELECT order_seq.NEXTVAL FROM dual; -- returns 1001
-- Ask what the current value is
SELECT order_seq.CURRVAL FROM dual; -- returns 1001
-- CURRVAL without NEXTVAL in the same session fails
SELECT order_seq.CURRVAL FROM dual; -- ORA-08002 error
NEXTVAL advances the sequence and returns the new value. CURRVAL returns the last value
NEXTVAL produced in the current session, without advancing anything. The rule that
catches people out: CURRVAL only works after NEXTVAL has been called at least once in
the same session. Call it cold and the database refuses with an error.
The gap you will notice: sequences are non-transactional. Once a
NEXTVAL call happens, the number is consumed — even if the transaction rolls back. So a
sequence that has produced 1000 values may show 1000 as the next value, but only 950 of
them are actually in your table. The missing 50 were generated by transactions that
rolled back. This is expected behaviour, not a bug. Sequences guarantee uniqueness, not
continuity.
Managing Sequences with ALTER and DROP
Sequences have a small set of properties you can change after creation. Beyond that,
you drop and recreate.
Changing the increment
ALTER SEQUENCE order_seq INCREMENT BY 5;
After that, the sequence advances by 5 each time NEXTVAL is called. The current value is
not reset — the next call produces current_value + 5. That is useful when you need
to widen the spacing between generated ids (say, to leave room for reserved ranges).
Changing the cache
ALTER SEQUENCE order_seq CACHE 1000;
Caching pre-fetches a batch of sequence values into memory so that a rapid series of
NEXTVAL calls does not have to hit disk each time. The trade-off: if the database
instance crashes, the cached values are lost, and the sequence resumes with a gap.
A cache of 1000 means up to 1000 numbers can disappear on a crash.
Restarting
ALTER SEQUENCE order_seq RESTART START WITH 1001;
Restarts the sequence from a new starting point. This is useful when you are reloading
data from scratch and want ids to line up with the reload. It is a rare operation in
production because existing rows may already have ids above the new start.
Dropping a sequence
DROP SEQUENCE order_seq;
Removes the sequence entirely. Tables that used it are unaffected — the ids already in
those tables are still there. But any future INSERT that calls
order_seq.NEXTVAL fails with an error. Dropping should be the last step
after the last bit of code that depends on the sequence has been retired.
Sequences in High-Transaction Environments
On a system with a hundred concurrent sessions all inserting rows, the sequence that
generates the primary keys can become a bottleneck if it is configured poorly. Three
settings matter.
Cache size
ALTER SEQUENCE order_seq CACHE 1000;
A large cache means most NEXTVAL calls are served from memory instead of from disk. On a
high-throughput system, this is the difference between the sequence being the bottleneck
and the sequence being invisible. The cost is a wider range of possible gaps after a
crash — up to 1000 numbers per instance instead of 1.
Increment size
ALTER SEQUENCE order_seq INCREMENT BY 10;
Widening the increment does not reduce contention on its own, but it can be used to give
each of several writers its own range. Two sessions using the same sequence with an
increment of 10 do not collide because they are already stepping by 10 apart. This is
the technique used to shard sequences across nodes in a distributed system.
NOORDER vs ORDER (Oracle-specific)
CREATE SEQUENCE order_seq NOORDER;
By default, a sequence on a clustered database may not guarantee globally increasing
numbers across nodes — a session on one node may get a value, then a session on another
gets a slightly lower value. ORDER forces global ordering, at a real
performance cost. Most applications do not need the ordering; uniqueness is enough. The
default NOORDER is the right choice unless you specifically need to
guarantee that the numbers arrive in increasing order.
What not to do
The mistake to avoid is treating a sequence like a strictly sequential id. Sequences
guarantee uniqueness, not continuity. The moment you have multiple concurrent sessions,
cached values, or rolled-back transactions, the actual numbers that end up in the table
will have gaps. Applications that assume "the next id will be exactly one more than the
last" break the moment they run at load.
Synonyms — Aliasing Database Objects
A synonym is another name for a database object. It exists so that queries can write a
short name instead of a fully qualified one, and so that moving an object between schemas
does not break every query that references it.
Creating a synonym
CREATE SYNONYM customers
FOR hr.customers;
After that statement, customers resolves to hr.customers.
Every query that says SELECT * FROM customers reaches the HR schema's table
without the caller needing to know where it lives.
What a synonym is good for
Cross-schema access. A user in the finance schema can query HR tables through a synonym without being granted direct access to the HR schema.
Simpler queries. Long dotted names like warehouse.reporting.daily_sales become just daily_sales.
Abstraction. The underlying object can be renamed or moved without changing the synonym — only the synonym needs to be updated.
Cross-database access. Some databases allow a synonym to point at a table in another database entirely, making a distributed query look local.
Public vs private synonyms
-- Private synonym: visible only to the schema that created it
CREATE SYNONYM customers FOR hr.customers;
-- Public synonym: visible to every user in the database
CREATE PUBLIC SYNONYM customers FOR hr.customers;
A private synonym exists only for the user who created it. A public synonym exists for
every user. Public synonyms are useful for commonly referenced objects, but they can
create name collisions — if two schemas both want to expose "customers", only one can
hold the public synonym.
Dropping a synonym
DROP SYNONYM customers;
DROP PUBLIC SYNONYM customers;
Dropping a synonym removes the alias. The underlying table is not touched — the actual
data and the table itself are still there, still owned by the schema that had it. Any
query that referenced the synonym now fails until the synonym is recreated or the query
is rewritten to use the full name.
Managing Synonyms Through Schema Changes
Synonyms do not automatically follow the objects they point at. When a table moves
schemas, gets renamed, or changes type (from a table to a view, say), the synonym must
be updated to match.
When a table is renamed
DROP SYNONYM customers;
CREATE SYNONYM customers FOR hr.customers_v2;
The old synonym is dropped and a new one is created pointing at the renamed object. From
the caller's perspective, nothing changed — customers still works. That
transparency is exactly what a synonym is for.
When a table moves to a different schema
DROP SYNONYM customers;
CREATE SYNONYM customers FOR sales.customers;
The synonym now points at the new schema. Applications keep working. That is the win of
putting a synonym in front of an object you might want to move later.
When an object changes type
DROP SYNONYM customers;
CREATE SYNONYM customers FOR hr.customer_summary_view;
The synonym can point at a view just as easily as a table. If the original table was
replaced by a view that aggregates it, updating the synonym makes the change invisible
to applications. As long as the view's columns are compatible with what the application
expected from the table, nothing on the caller side needs to change.
Synonyms are silent. Nothing tells you when a synonym points at
something that no longer exists. The database only complains when someone tries to use
the synonym. That means schema changes and synonym updates have to be deployed together
in the same transaction or the same migration. A missed synonym update is one of those
bugs that shows up hours later when someone runs a report.
25 Interview Questions and Answers
These are the index, sequence and synonym 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 index, sequence, or synonym.
An index is a separate structure the database maintains alongside a table so it
can find rows without scanning the whole thing. The mental model: it is the index
at the back of a book. Without one, you read every page. With one, you go straight
to the page you need.
CREATE INDEX idx_customer_name ON customers (customer_name);
After that index exists, a query like SELECT * FROM customers WHERE customer_name = 'Alice Corp'
can look up the value in the index and jump to the matching row. Without it, the
database reads every row and checks the name column one by one.
Indexes make reads faster and writes slower. Every INSERT, UPDATE, or DELETE has
to also update every index on the table. On a table with a heavy write load, that
overhead matters — three indexes on a table means each INSERT does four writes.
The trade-off is worth it most of the time. Every foreign key column, every
frequently filtered column, every column used in a JOIN condition, is a candidate
for an index. Every column that is rarely queried is not.
CREATE INDEX does the job. You give the index a name, name the table,
and list the columns to index.
CREATE INDEX idx_customer_name
ON customers (customer_name);
Queries that filter on customer_name now have a shortcut. The
database reads the sorted index instead of scanning the table.
For multiple columns, list them in the order you filter on them most:
CREATE INDEX idx_region_limit
ON customers (region, credit_limit);
That index helps queries filtering on region alone or on both
region and credit_limit. It does not help queries
filtering on credit_limit alone, because the index is sorted by
region first. Column order in a composite index is a decision, not a detail.
Every index you add costs storage and slows down writes. That is why indexes are
added deliberately, based on which queries actually run, not preemptively for
every column that might be filtered.
DROP INDEX removes an index entirely. The table and its data are
untouched — only the index structure is removed.
DROP INDEX idx_customer_name;
Dropping frees the storage the index occupied and removes the maintenance overhead
from INSERT, UPDATE, and DELETE. It also removes the speed-up for any query that
was relying on the index. Depending on your situation, one of those two effects
will matter more than the other.
The usual reasons to drop an index: it is never used (which the database's usage
statistics can tell you), it is made redundant by a composite index that already
covers the same queries, or the write overhead has become a problem and the read
benefit no longer justifies it.
The one caution: dropping an index is not free in the other direction either.
Once dropped, any query that was using it now runs without that shortcut. If the
index was doing real work, you will notice the next time the query runs.
A unique index does everything a regular index does, plus it prevents duplicate
values. That makes it the standard way to enforce business rules like "no two
customers can share an id" or "no two users can share an email address."
CREATE UNIQUE INDEX idx_customer_name_unique
ON customers (customer_name);
After that, any attempt to insert or update a row with a duplicate customer name
fails immediately with an error. The database refuses the change — it does not
silently allow the duplicate or overwrite an existing one.
Behind the scenes, the database is using the index's sorted structure to detect
duplicates efficiently. When a new value is inserted, the database checks whether
the value is already in the index. If it is, the operation fails. That check is
essentially free because the index is already sorted.
Unique indexes are how primary keys and unique constraints are enforced. When you
declare a column as PRIMARY KEY, most databases create a unique index behind the
scenes without you asking. When you add a UNIQUE constraint, the same thing
happens. The index is what does the work.
A composite index covers more than one column. It exists to speed up queries that
filter or sort on the columns together.
CREATE INDEX idx_region_limit
ON customers (region, credit_limit);
That index is sorted by region first, then by credit_limit
within each region. The database can walk it in order to answer:
SELECT * FROM customers
WHERE region = 'North'
AND credit_limit > 50000;
It can also answer queries that filter on region alone. Both of those
cases are covered because the leading column of the index matches the filter.
The pattern that trips people up: a composite index cannot be used efficiently for
queries that filter only on the second column. WHERE credit_limit > 50000
without a region filter would have to scan the whole index, because the index is
sorted by region first. That is why the order of columns matters — the leftmost
column is the one the index is primarily built around.
The decision rule: put the most selective filter first, or the column that appears
in the most queries first. On our data, region has three values and credit_limit
has seven, so putting credit_limit first would give a more selective index — but
only if queries actually filter on it more often.
Managing indexes is about keeping only the ones that earn their keep. Too few
indexes means slow reads. Too many means slow writes and wasted storage. The
balance point moves as a system grows.
The lifecycle has five stages:
Create indexes for columns that appear in frequent WHERE, JOIN, and ORDER BY clauses.
Monitor index usage. Every major database can tell you which indexes are being hit and how often.
Test with EXPLAIN — check that the index is actually being used by the query that motivated it.
Rebuild or reorganize indexes that have fragmented due to heavy inserts, updates, or deletes.
Drop indexes that are never used or that duplicate another index's coverage.
The single biggest source of index bloat: creating indexes for "just in case" filters
that never end up running. Every such index still gets updated on every write.
Cleaning those out periodically is often a bigger win than adding new ones.
Indexes speed up queries by letting the database find rows without scanning the
table. Where they help the most:
-- WHERE filter on an indexed column
CREATE INDEX idx_region ON customers (region);
SELECT * FROM customers WHERE region = 'North';
-- JOIN condition on an indexed foreign key
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
SELECT c.customer_name, o.order_id
FROM customers c
JOIN orders o ON c.customer_id = o.customer_id;
-- ORDER BY on an indexed column
CREATE INDEX idx_order_amount ON orders (order_amount);
SELECT * FROM orders ORDER BY order_amount DESC;
Each of those queries was previously scanning a table to answer. After the index,
the database reads the index instead.
Where indexes do not help: any query that filters on a function of a column
(WHERE UPPER(name) = 'ALICE' cannot use a plain index on
name), and queries that return most of the table anyway (if you are
reading 90% of the rows, a full scan is faster than jumping between index pages
and table pages).
The habit worth building: for every slow query, look at the WHERE clause, the JOIN
conditions, and the ORDER BY. Those columns are the candidates for an index.
EXPLAIN tells you what the database is going to do before it does
it. Running it in front of a query shows the plan — which tables are read, which
indexes are used, and how many rows each step is estimated to process.
EXPLAIN
SELECT * FROM customers WHERE region = 'North';
The output on a small table might say "Seq Scan on customers" — the database is
reading every row. That is what an index is supposed to prevent, but the database
may decide that for small tables a scan is cheaper.
After creating an index:
CREATE INDEX idx_region ON customers (region);
EXPLAIN
SELECT * FROM customers WHERE region = 'North';
The plan should now say something like "Index Scan using idx_region on customers"
— the database is using the index.
On large tables, the difference between "Seq Scan" and "Index Scan" is measurable
in seconds. Making it a habit to run EXPLAIN when you suspect a slow query has a
missing index is how you find them.
CREATE SEQUENCE defines a database object that produces a series of
unique numbers on demand. Sequences are the standard way to generate primary key
values without manual intervention or concurrency conflicts.
CREATE SEQUENCE order_seq
START WITH 1001
INCREMENT BY 1
NOCACHE;
That sequence starts at 1001, increases by 1 each time, and does not cache values.
The alternative to NOCACHE is CACHE, which pre-fetches values into memory for
speed — at the cost of losing some values if the database crashes.
Each call to order_seq.NEXTVAL returns the next number:
SELECT order_seq.NEXTVAL FROM dual; -- 1001
SELECT order_seq.NEXTVAL FROM dual; -- 1002
The sequence generates the id. The application does not need to know what the
previous id was. Concurrent sessions can call NEXTVAL at the same time without
getting the same number, because the sequence internally serialises the calls.
Not every database uses CREATE SEQUENCE. MySQL uses AUTO_INCREMENT
on the column itself. SQL Server uses IDENTITY columns. PostgreSQL and Oracle
support sequences natively. The concept is the same — the syntax differs.
NEXTVAL advances the sequence and returns the new value.
CURRVAL returns the last value NEXTVAL produced in the current
session, without advancing anything.
-- Advance and read the new value
SELECT order_seq.NEXTVAL FROM dual; -- 1001
-- Read what was just generated
SELECT order_seq.CURRVAL FROM dual; -- 1001 (same value)
The rule that catches people out: CURRVAL only works after
NEXTVAL has been called in the same session. Call it cold and the
database throws an error — it has no "last value" to return.
This matters in a workflow like "insert an order, then insert its line items
referencing the same order id":
The second INSERT uses CURRVAL to get the same order id that was just inserted,
without querying the orders table. Same session, same sequence, same value.
One important detail: sequence values are non-transactional. Once NEXTVAL has been
called, the number is consumed even if the transaction rolls back. That is why a
sequence can produce 100 numbers while your table only has 95 rows — five
transactions rolled back after their insert failed. Uniqueness is guaranteed.
Continuity is not.
Sequences have a small set of properties you can change. Beyond that, you drop and
recreate.
-- Change the increment
ALTER SEQUENCE order_seq INCREMENT BY 10;
-- Change the cache
ALTER SEQUENCE order_seq CACHE 500;
-- Restart from a new value
ALTER SEQUENCE order_seq RESTART START WITH 2001;
-- Drop the sequence entirely
DROP SEQUENCE order_seq;
ALTER SEQUENCE INCREMENT BY changes the step size without resetting
the current value. The next NEXTVAL call returns the current value plus the new
increment.
ALTER SEQUENCE CACHE changes how many values are pre-fetched into
memory. Higher cache means faster NEXTVAL calls but larger gaps if the database
crashes.
ALTER SEQUENCE RESTART resets the sequence to a new starting point.
This is used when you reload data from scratch and want ids to line up with the
reload.
DROP SEQUENCE removes the sequence entirely. Any INSERT that calls
NEXTVAL on the dropped sequence will now fail. Before dropping,
confirm that nothing in the codebase still references it.
A synonym is an alias for a database object. It lets you refer to a table, view,
sequence, or stored procedure by a short name that you choose, instead of the full
name that includes the schema and sometimes the database.
CREATE SYNONYM customers
FOR hr.customers;
SELECT * FROM customers; -- refers to hr.customers
Why this is useful:
Shorter queries.SELECT * FROM customers beats SELECT * FROM hr.customers every time.
Schema independence. If the object moves from hr to sales, the synonym is updated and every query that referenced it keeps working.
Access abstraction. A user can be granted access to a synonym without being granted access to the underlying schema. That keeps the schema's other objects private.
Cross-database access. Some databases let a synonym point at a table in a completely different database. Distributed queries then look local.
Synonyms are not containers — they do not hold data. They are pointers, and when the
object they point at is dropped or renamed, the synonym stops working until it is
updated. That is why schema changes have to be paired with synonym updates in the
same deployment.
CREATE SYNONYM defines an alias. You give the synonym a name and
point it at the target object.
CREATE SYNONYM customers
FOR hr.customers;
CREATE SYNONYM customer_summary
FOR hr.customer_summary_view;
The first creates a synonym for a table. The second creates one for a view. Once
they exist, queries can use the short names:
SELECT * FROM customers;
SELECT * FROM customer_summary;
That works because the database resolves the synonym to the underlying object
before executing the query. To the caller, they are equivalent to writing the
full name.
The difference between private and public synonyms is one of scope. A private
synonym exists only for the user who created it — other schemas do not see it. A
public synonym is visible to every user in the database:
CREATE PUBLIC SYNONYM customers FOR hr.customers;
Public synonyms are convenient but can collide. Only one public synonym can be
named customers at a time, so if two schemas both want to expose a
table by that name, one of them has to pick a different synonym.
DROP SYNONYM removes the alias. The underlying object is not
affected — the table or view is still there, still owned by its schema.
DROP SYNONYM customers;
DROP PUBLIC SYNONYM customers;
The first drops a private synonym; the second drops a public one. They are
different objects, so the syntax has to match the scope of what you created.
After the drop, any query that referenced the synonym fails with "object not
found" or "table or view does not exist." Anything that used to write
SELECT * FROM customers now needs to write the full name
hr.customers — or the synonym needs to be recreated.
The reason to drop a synonym: the underlying object was moved or renamed and the
synonym is out of date. Or the synonym was created for an application that no
longer runs. Either way, leaving a broken synonym around is confusing — someone
will eventually use it, get an error, and spend time tracing back to the fact that
the target object no longer exists.
You create a synonym and then reference the underlying object by the synonym name.
The database handles the resolution — no client-side change is needed.
-- Create the synonym
CREATE SYNONYM emp FOR hr.employees;
-- Use it
SELECT * FROM emp WHERE department_id = 10;
-- It is equivalent to
SELECT * FROM hr.employees WHERE department_id = 10;
The synonym is only a name, so anything you can do with the original object works
with the synonym: SELECT, INSERT, UPDATE, DELETE, GRANT, and even CREATE INDEX.
From the database's perspective the object is the same; only the way you refer to
it has changed.
The strongest use case is cross-schema access. Suppose your finance schema needs
to read the HR schema's employees table. Without a synonym, every query would need
to write hr.employees, and the finance schema would need direct
access to the HR schema. With a synonym, the query writes emp, and the
only grant needed is on the synonym.
Synonyms also help during schema migrations. When the HR schema is renamed to
people, the synonym is updated to point at the new name, and every
downstream query keeps working with no change.
Every time a synonym's target changes — renamed, moved to a new schema, or
converted from a table to a view — the synonym needs to be dropped and recreated.
The syntax is the same in all three cases.
-- Target was renamed
DROP SYNONYM customers;
CREATE SYNONYM customers FOR hr.customers_v2;
-- Target was moved to another schema
DROP SYNONYM customers;
CREATE SYNONYM customers FOR sales.customers;
-- Target was converted to a view
DROP SYNONYM customers;
CREATE SYNONYM customers FOR hr.customer_summary_view;
The two-step nature is important. You cannot point an existing synonym at a new
object — you have to drop it and create a new one. That is the same pattern for
private and public synonyms, with the PUBLIC keyword added for the
public version.
The rule that keeps this working: schema changes and synonym updates have to be
deployed together. A missing synonym update is a silent failure that only shows up
the next time someone runs a query against the synonym. On production systems,
syncing them is part of the same migration script or deployment.
Where this matters most: any environment that uses synonyms to abstract schema
names. A well-run deployment plan has a step that drops and recreates all affected
synonyms whenever a schema migration happens.
Ordinary indexes cannot help when a query filters on a function of a column,
like WHERE UPPER(customer_name) = 'ALICE CORP'. The database cannot
use an index on customer_name because the values in the index are
the raw names, not the uppercased versions.
Function-based indexes (also called expression indexes) solve this. You index the
result of the function itself:
CREATE INDEX idx_customer_name_upper
ON customers (UPPER(customer_name));
After that, the following query can use the index:
SELECT * FROM customers
WHERE UPPER(customer_name) = 'ALICE CORP';
The index stores the uppercased values, so lookups go through the index the same
way a normal index works. The benefit is real — a query that was scanning the
whole table now finds its rows in milliseconds.
Support varies. Oracle, PostgreSQL, and SQL Server all support function-based
indexes. MySQL 8.0 added support for indexing expressions. The exact syntax for
creating them is slightly different on each database, but the concept is the same:
index the computed value, not the raw column.
The cost is the same as any index — extra storage and slower writes. And the index
is only used when the query uses the exact same expression. Writing
WHERE UPPER(customer_name) = 'ALICE CORP' uses the index;
WHERE LOWER(customer_name) = 'alice corp' does not, because it is a
different expression.
Every database has system views that report which indexes exist, what they cover,
and how they are being used. The syntax differs but the information is
consistent.
-- Oracle: what indexes exist on a table
SELECT index_name, table_name, uniqueness
FROM user_indexes
WHERE table_name = 'CUSTOMERS';
-- Oracle: which columns each index covers
SELECT index_name, column_name
FROM user_ind_columns
WHERE table_name = 'CUSTOMERS';
-- MySQL
SHOW INDEX FROM customers;
-- PostgreSQL
SELECT indexname, indexdef
FROM pg_indexes
WHERE tablename = 'customers';
Those queries answer "what indexes exist" and "which columns they cover." Some
databases also expose usage statistics — how many times each index has been used
since the statistics were last reset. That is the query that tells you which
indexes are dead weight.
The information is worth checking periodically. Indexes created years ago by
people who no longer work on the system often go unused but still slow down every
write. Dropping them is usually an easy performance win.
A sequence that is called a thousand times a second can become a bottleneck if
every call goes to disk. The fix is caching, and the trade-off is gaps.
ALTER SEQUENCE order_seq CACHE 1000;
A cache of 1000 pre-fetches 1000 values into memory in one go. The next 1000
NEXTVAL calls are served from memory without touching disk. That is orders of
magnitude faster than NOCACHE.
The cost of caching is gaps. If the database instance crashes, whatever was cached
and not yet consumed is lost. The next call resumes from where the disk-stored
value left off, which can skip up to 1000 numbers. On a high-throughput system,
gaps of a few hundred or a few thousand are normal and not a problem.
A second lever is the increment size. Widening the increment gives each caller its
own range and reduces contention:
ALTER SEQUENCE order_seq INCREMENT BY 10;
With an increment of 10, two sessions are not competing for the same numbers — each
call consumes a different slot. This is useful when a sequence is shared across
multiple instances of an application and you want to avoid serialising the calls.
The combination — a large cache plus a wide increment — is how high-transaction
systems get sequence throughput where naive sequences would choke. The gaps that
result are cosmetic. The ids are still unique, which is what actually matters.
An index can be built in ascending or descending order on each column. The choice
matters when queries consistently sort in a specific direction.
CREATE INDEX idx_sales_date_amount
ON sales (sale_date DESC, amount ASC);
That index is sorted by sale_date in descending order, then by
amount in ascending order within each date. A query that sorts the
same way can use the index to read rows in order and skip the sort step entirely:
SELECT sale_date, amount
FROM sales
ORDER BY sale_date DESC, amount ASC;
Without the matching index, the database has to load the rows, sort them in memory
(or spill to disk on large results), and then return them. With the index, it
reads them in the sorted order directly.
The default for each column is ASC. If your queries mostly sort descending — for
example, showing the newest records first — indexing in DESC order lets the
database read the index in the direction the query needs, without a separate sort.
In practice, most indexes are created with the default ascending order and that is
fine. The mixed ASC/DESC pattern is worth using when a specific query is running
slowly and you have identified the sort direction as the bottleneck.
Large databases live and die by index strategy. Every query that runs a million
times a day needs to be fast, and the difference between a well-indexed and a
poorly-indexed system is often measured in hours of batch time.
Five techniques cover most of the ground:
Index the columns that queries filter, join, and sort on. Start with WHERE clauses and JOIN conditions — those are where the biggest wins are.
Use composite indexes when queries filter on multiple columns together. A single composite index can replace two single-column indexes and be more selective.
Monitor usage and drop unused indexes. Every unused index still slows every write. Removing them is free performance.
Consider covering indexes. An index that includes all the columns a query needs lets the database answer the query from the index alone, without touching the table.
Partition large tables and partition the indexes with them. A partitioned table with a partitioned index keeps lookups and maintenance scoped to the relevant partition.
The order in which to try these: start with (1), then (2), then (3). Covering
indexes and partitioning are refinements that matter most on very large systems
where the first three are already in place.
As a table changes — inserts, updates, deletes — its indexes drift away from the
compact structure they had when they were built. Reads become slower because the
database has to visit more pages to find what it needs. This is fragmentation, and
it needs attention.
First step: measure. Every database has a way to report the fragmentation level of
each index.
-- SQL Server
SELECT *
FROM sys.dm_db_index_physical_stats(NULL, NULL, NULL, NULL, 'DETAILED');
Then choose a remedy. Two options:
-- Rebuild: reconstructs the index from scratch. Heavyweight.
ALTER INDEX idx_customer_name ON customers REBUILD;
-- Reorganize: reorders pages. Lighter weight.
ALTER INDEX idx_customer_name ON customers REORGANIZE;
The usual rule: fragmentation under 5% is fine and needs no action. Between 5% and
30% gets a reorganize. Above 30% gets a rebuild. Both operations use significant
resources, so they run during maintenance windows.
To slow down future fragmentation, use a fill factor that leaves room for growth:
CREATE INDEX idx_customer_name
ON customers (customer_name)
WITH (FILLFACTOR = 80);
A fill factor of 80 means each index page is only 80% full when the index is
built. New inserts fill the free space before pages have to split. Splits are what
cause fragmentation in the first place.
The final habit: schedule index maintenance. Every production system should have a
regular job that checks fragmentation and rebuilds or reorganizes as needed. Doing
it manually means it happens once and then is forgotten until the next crisis.
An index hint tells the optimizer to use a specific index. It is a way to override
the optimizer's choice when you know better than it does — or think you do.
-- Oracle
SELECT /*+ INDEX(customers idx_region) */ customer_name
FROM customers
WHERE region = 'North';
-- MySQL
SELECT customer_name
FROM customers USE INDEX (idx_region)
WHERE region = 'North';
-- SQL Server
SELECT customer_name
FROM customers WITH (INDEX(idx_region))
WHERE region = 'North';
The /*+ INDEX(table_name index_name) */ in Oracle is a comment that
happens to be meaningful to the optimizer. The MySQL and SQL Server forms use
distinct keywords. PostgreSQL does not support hints at all — the design philosophy
is that the optimizer should be trusted, and bad plans should be fixed by
improving statistics or rewriting the query, not by locking in a specific index.
The reason to reach for a hint: a query that used to be fast is suddenly slow
because the optimizer picked a different index after the data grew, and you have
confirmed by testing that the other index is faster.
The reason to avoid hints: they freeze a decision that may become wrong later. The
data distribution changes, the index you hinted at stops being optimal, and the
hint keeps forcing it anyway. Hints that helped in January can silently hurt in
December. Use them as a last resort, and review them whenever the underlying data
changes significantly.
The pattern is simple: call NEXTVAL in the VALUES clause where the
primary key value belongs.
That inserts a new order with whatever order id the sequence produces next. The
application does not need to know the previous id. Two sessions inserting at the
same time both call NEXTVAL and receive different numbers — no
collision, no lock, no coordination.
The same pattern works for any table that uses sequences as a source of unique
values:
That combines the sequence with LPAD to produce ids like C008, C009, C010 — the
same format as the existing customers in the sample data.
Support varies by database. Oracle and PostgreSQL use the sequence's NEXTVAL
directly. MySQL uses AUTO_INCREMENT on the column, which is effectively a
built-in sequence tied to the table. SQL Server uses IDENTITY columns, same idea.
The concept is the same — each database just packages it differently.
One practical note: sequences guarantee uniqueness, not continuity. If a
transaction rolls back after generating an id, that id is consumed and skipped.
Nobody will get that number again. On systems with frequent rollbacks, the id
column can have gaps, and that is normal. Do not design reports that assume
sequential ids.
Some indexes are created automatically by the database, without the DBA asking.
They exist to support constraints — the database needs an efficient way to
enforce a uniqueness rule, and an index is that way.
When the primary key constraint is declared, the database creates a unique index
on customer_id automatically. The index is what enforces the
uniqueness — every INSERT checks the index for a duplicate.
The UNIQUE keyword on customer_name also creates an
automatic unique index. Same reasoning — the index is what makes uniqueness
efficient to enforce.
Foreign key indexes — usually not automatic
Foreign key constraints do not automatically create indexes. The database can
enforce the constraint without one, but every operation that checks the constraint
has to scan the referencing table. On large tables, that is very slow.
CREATE TABLE orders (
order_id VARCHAR2(10) PRIMARY KEY,
customer_id VARCHAR2(10) REFERENCES customers(customer_id)
);
-- Manually create the index for performance
CREATE INDEX idx_orders_customer_id ON orders (customer_id);
The rule: index every foreign key column. The constraint works without the index,
but the performance is dramatically better with one.
Where the automatic indexing behaviour differs: Oracle and SQL Server create unique
indexes for primary keys and unique constraints automatically. MySQL does the same.
PostgreSQL does too. For foreign keys, none of them create the index automatically
— that is on you.
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: creating an index on every column. Each index slows down
writes. On a table with a heavy insert load, too many indexes is worse than too few.
Index the columns that queries actually filter on.
Mistake two: getting the column order wrong on a composite index. An
index on (region, credit_limit) helps queries that filter on region, but not queries
that filter only on credit_limit. The leftmost column is the one that matters most.
Mistake three: assuming a sequence produces consecutive numbers. It
produces unique numbers. Rolled-back transactions, cached values, and concurrent
sessions all introduce gaps. Applications that assume consecutive ids break the moment
they run at load.
Mistake four: calling CURRVAL before NEXTVAL in the same session.
Sequences need NEXTVAL to be called at least once before CURRVAL has anything to return.
The error message is confusing if you do not know the rule.
Mistake five: forgetting to update synonyms after a schema change. The
synonym keeps pointing at the old object, and any query that uses it fails with "object
not found." Schema migrations need to update synonyms in the same deployment.
Mistake six: not indexing foreign key columns. The constraint works
without an index, but every check has to scan the referencing table. On large tables
that turns a millisecond lookup into a second-long scan.
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 topics in this guide.
After "What is an index?" — expect
"What is the trade-off of adding an index?"
"How do you know if a query is using an index?"
"What is a covering index and when would you use one?"
"How do composite indexes differ from single-column indexes?"
After "What is a composite index?" — expect
"Does the column order matter?"
"Can a composite index replace two single-column indexes?"
"How do you decide which column goes first?"
"Does a composite index help a query that filters on only the second column?"
After "What is a sequence?" — expect
"What is the difference between NEXTVAL and CURRVAL?"
"What happens to sequence values after a ROLLBACK?"
"How do you speed up a sequence under high load?"
"Why do sequence-generated ids sometimes have gaps?"
After "What is a synonym?" — expect
"What is the difference between a public and a private synonym?"
"What happens when the underlying object is renamed?"
"How would a synonym help in a schema migration?"
"What is the performance impact of a synonym?"
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 trade-off of adding an index to a table?
2. What does NEXTVAL do on a sequence?
3. What happens when a synonym's target object is renamed?