SQL Indexes, Synonyms, Sequences · Guide + 25 Interview Questions

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
25 Interview Questions Beginner to Advanced Real Examples Updated Oct 7, 2026
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
📤 Share:

On This Page

What These Three Objects Really Do

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.

-- customers table
-- customer_id | customer_name    | region | credit_limit
-- C001        | Alice Corp       | North  | 50000
-- C002        | Bob Traders      | South  | 25000
-- C003        | Charlie Ltd      | North  | 75000
-- C004        | Diana Retail     | West   | 40000
-- C005        | Ethan Supplies   | South  | 55000
-- C006        | Fiona Mart       | West   | NULL
-- C007        | George Wholesale | North  | 80000

-- orders table
-- order_id | customer_id | order_amount | order_status
-- 1001     | C001        | 12000        | Delivered
-- 1002     | C002        | 4500         | Shipped
-- 1003     | C001        | 7500         | Delivered
-- 1004     | C003        | 22000        | Pending
-- 1005     | C002        | 3200         | Delivered
-- 1006     | C004        | 18000        | Shipped
-- 1007     | C001        | 9000         | Delivered

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.

Using a sequence in an INSERT

INSERT INTO orders (order_id, customer_id, order_amount, order_status)
VALUES (order_seq.NEXTVAL, 'C001', 12000, 'Delivered');

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

And in an INSERT:

INSERT INTO orders (order_id, customer_id, order_amount, order_status)
VALUES (order_seq.NEXTVAL, 'C001', 12000, 'Delivered');

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":

INSERT INTO orders (order_id, customer_id, order_amount, order_status)
VALUES (order_seq.NEXTVAL, 'C001', 12000, 'Delivered');

INSERT INTO order_items (order_id, product_id, quantity)
VALUES (order_seq.CURRVAL, 'P001', 2);

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.

INSERT INTO orders (order_id, customer_id, order_amount, order_status)
VALUES (order_seq.NEXTVAL, 'C001', 12000, 'Delivered');

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:

INSERT INTO customers (customer_id, customer_name, region)
VALUES ('C' || LPAD(customer_seq.NEXTVAL, 3, '0'), 'Henry Co', 'East');

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.

Primary key indexes

CREATE TABLE customers (
    customer_id VARCHAR2(10) PRIMARY KEY,
    customer_name VARCHAR2(100)
);

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.

Unique constraint indexes

CREATE TABLE customers (
    customer_id VARCHAR2(10) PRIMARY KEY,
    customer_name VARCHAR2(100) UNIQUE
);

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?