SQL Databases Explained: Tables, Keys, Joins, Transactions, Indexes, and When to Use SQL
A SQL database is built around structured relationships and declarative queries. Instead of designing every read around one key or document shape, you describe tables, constraints, relationships, and the result you want; the database optimizer decides how to execute the query.
That makes SQL databases especially strong for business systems where data integrity, joins, evolving queries, and multi-row transactions matter.
PostgreSQL, MySQL, SQL Server, MariaDB, Oracle Database, SQLite, and distributed SQL systems all use SQL, but their storage engines and advanced features differ. The relational ideas below are the common foundation.
The relational model starts with tables
A table represents one kind of fact or entity:
CREATE TABLE customers (
id bigint PRIMARY KEY,
email varchar(320) NOT NULL UNIQUE,
name varchar(200) NOT NULL
);
CREATE TABLE orders (
id bigint PRIMARY KEY,
customer_id bigint NOT NULL,
status varchar(30) NOT NULL,
total decimal(12,2) NOT NULL,
created_at timestamp NOT NULL,
FOREIGN KEY (customer_id) REFERENCES customers(id)
);
Rows represent records. Columns give those records a known structure and type.
The useful part is not merely tabular storage. It is that the database understands relationships and rules between those records.
Primary keys give each row identity
A primary key uniquely identifies a row.
Typical choices include:
- integer/bigint identity values
- UUIDs
- natural identifiers when they are truly stable and unique
- composite keys when identity naturally contains several columns
The best key is stable, unique, and practical for joins and indexing.
Do not use a mutable business attribute such as an email address as a primary key merely because it happens to be unique today. Business identifiers often change; row identity should usually not.
Foreign keys enforce relationships
A foreign key says that one row refers to an existing row elsewhere.
FOREIGN KEY (customer_id)
REFERENCES customers(id)
That prevents an order from referencing a customer that does not exist.
Foreign keys are useful for more than documentation. They protect referential integrity from:
- application bugs
- manual SQL
- migrations
- scripts
- future services
Application code should still validate user-facing rules, but database constraints provide a final integrity boundary.
Constraints turn business invariants into database rules
Useful constraints include:
NOT NULLUNIQUEPRIMARY KEYFOREIGN KEYCHECK
Example:
CREATE TABLE products (
id bigint PRIMARY KEY,
sku varchar(100) NOT NULL UNIQUE,
price decimal(12,2) NOT NULL,
CHECK (price BETWEEN 0 AND 9999999999.99)
);
This makes an invalid negative price impossible to persist through any normal database writer.
The exact constraint syntax and capabilities vary somewhat by engine, so verify the database version you deploy.
Joins are a major strength of SQL
Relational systems make it natural to combine related data at query time.
SELECT
o.id,
o.total,
c.name,
c.email
FROM orders AS o
JOIN customers AS c
ON c.id = o.customer_id
WHERE o.status = 'paid';
Useful join types include:
INNER JOINLEFT JOINRIGHT JOINin engines that support/use itFULL OUTER JOINwhere supported- self joins
Joins let you normalize shared facts instead of copying them into every record.
But joins are not automatically cheap. Indexes, cardinality, join order, result size, and query plans still matter.
Normalization reduces duplicated truth
Normalization separates data when one fact should have one authoritative representation.
Instead of storing the customer's current email in every order:
orders.customer_email copied 500 times
you normally store:
customers.email
orders.customer_id
Benefits include:
- fewer inconsistent copies
- simpler updates
- stronger constraints
- clearer ownership
Common normal forms help identify repeated groups and dependencies, but production schema design should not become a theoretical contest.
Normalize data whose shared identity matters. Deliberately denormalize where duplicated data materially improves an important read and you have a clear synchronization policy.
Some duplicated data is intentionally historical
An order may need to preserve the shipping address or item price as it was at purchase time.
In that case, copying values into the order is not accidental denormalization—it is the correct historical model.
The key question is:
Should this value track the current source, or preserve the past event?
An order's historical item price should usually not change when the product's current price changes tomorrow.
Transactions make several changes one unit of work
A transaction groups related operations so they succeed or fail together.
BEGIN;
INSERT INTO orders (...);
UPDATE inventory ...;
INSERT INTO payment_records (...);
COMMIT;
If a required step fails, the transaction can be rolled back instead of leaving half the workflow committed.
This is the foundation of ACID transaction behavior:
- Atomicity — all or nothing
- Consistency — integrity rules remain valid
- Isolation — concurrent operations have defined visibility
- Durability — committed data survives according to the engine's durability guarantee
The implementation details differ by database, but transaction semantics are one of the core reasons SQL databases remain popular for business systems.
Keep transactions short
Transactions should contain database work that must be atomic.
Avoid this:
BEGIN
update database
call payment HTTP API
wait 3 seconds
call another service
COMMIT
Long transactions can increase:
- blocking
- lock contention
- retained row versions
- log growth
- deadlock risk
A better architecture often commits one local business state change and uses an outbox/event workflow for external side effects when atomic cross-system commit is not possible.
Isolation levels control concurrent visibility
Concurrent transactions can interact in surprising ways unless the application understands its isolation level.
Common SQL isolation concepts include:
- Read Committed
- Repeatable Read
- Serializable
Different engines implement these levels using combinations of locking and row versioning/MVCC.
Higher isolation can provide stronger guarantees but may introduce more retries, blocking, or coordination.
Choose isolation from the invariant. A public analytics query and an inventory reservation do not necessarily need the same transaction semantics.
Indexes are alternate access paths
Without an appropriate index, a database may need to inspect a large part of a table.
Suppose this is a common query:
SELECT id, total, created_at
FROM orders
WHERE customer_id = ?
AND status = 'paid'
ORDER BY created_at DESC;
A compound index using the customer, status, and date columns may let the engine find those rows much more efficiently.
The exact syntax and optimal column order differ by engine and workload, so confirm with its query planner.
Every index also costs something
Indexes require:
- storage
- memory/cache
- maintenance on writes
- backup space
- rebuild/maintenance time
A table with ten speculative indexes can have excellent read options and an unnecessarily expensive write path.
For each index, ask:
- Which important query needs it?
- Does another index already cover that access pattern?
- How often are its columns updated?
- Is its selectivity useful?
Index real workloads, not every column that might someday appear in a filter.
The query optimizer is why SQL is declarative
In SQL, you normally describe what result you want, not the exact physical algorithm.
SELECT ...
FROM orders
JOIN customers ...
WHERE ...
The optimizer can choose among techniques such as:
- index access
- table scans
- nested-loop joins
- hash joins
- merge joins
- sorting
- parallel execution in supported engines
That flexibility is powerful, but it means performance work requires reading execution plans rather than guessing from the SQL text alone.
Use the engine's EXPLAIN, execution-plan, or query-profile tooling to see what actually happened.
SQL is excellent for evolving query requirements
Business applications rarely keep the same query set forever.
A product may begin with:
find invoice by ID
and later need:
revenue by customer and month
unpaid invoices by region
customers whose order count dropped
refund rate by product category
Relational databases are strong when new questions appear because joins, aggregations, window functions, CTEs, and indexes give the database many ways to answer evolving requirements without redesigning all stored records around each query.
Window functions solve analytical questions without collapsing rows
SQL window functions can calculate rankings, running totals, previous values, and partitioned aggregates while keeping individual result rows.
Example:
SELECT
customer_id,
id,
total,
ROW_NUMBER() OVER (
PARTITION BY customer_id
ORDER BY created_at DESC
) AS recent_rank
FROM orders;
Window functions are extremely useful for reports and application queries that need context around each row.
For very large historical analytics, however, a columnar OLAP system may eventually be a better home than the transactional database.
Schema migrations are part of application deployment
SQL schemas are explicit, which means changes need migration discipline.
A safe production pattern is often:
- add backwards-compatible schema
- deploy application code that understands both versions
- backfill existing data gradually
- switch traffic/read logic
- enforce tighter constraints later
- remove old schema only after all clients have migrated
Avoid assuming a large ALTER TABLE is instant or non-blocking. Behavior varies significantly by database and operation.
Test migrations against realistic data volumes.
Read replicas scale reads but introduce freshness questions
Many SQL databases support primary/replica architectures.
writes -> primary
reads -> primary or replicas
Replicas can help with:
- reporting
- stale-tolerant reads
- HA/DR
- analytical isolation
But asynchronous replicas can lag.
A user can update a row on the primary and immediately read an older version from a replica.
Keep read-after-write and security-sensitive flows on a path with the freshness guarantee they actually require.
Partitioning is different from sharding
Table partitioning usually splits one logical table inside one database system.
It can help with:
- retention
- pruning
- large-table maintenance
Sharding distributes data across independent database nodes or write owners.
It can add:
- horizontal storage capacity
- horizontal write capacity
- failure isolation
Sharding also introduces routing, cross-shard queries, distributed transactions, rebalancing, and more difficult recovery.
Do not shard simply because a table has become “large.” First determine the real bottleneck.
SQL does not mean one specific scaling model
SQL databases range from embedded SQLite to single-primary relational servers to distributed SQL clusters.
The SQL language tells you how data is queried; it does not by itself tell you:
- whether the database is single-node
- how replication works
- whether writes use consensus
- how data is sharded
- what consistency a replica provides
Evaluate the actual database engine and deployment architecture rather than assuming every SQL system has identical operational characteristics.
SQL vs NoSQL: choose by workload
SQL is usually a strong fit when
- joins are common
- relational constraints matter
- query requirements evolve
- multi-row transactions are important
- reporting needs flexible SQL
- there is one clear system of record
NoSQL can be a stronger fit when
- access patterns are known and key-oriented
- huge horizontal distribution is central to the design
- document/wide-column/graph semantics map more naturally to the workload
- very specific consistency/latency trade-offs are required
This is not a permanent either/or choice. Mature systems often combine several stores while keeping one clear source of truth for each domain.
Avoid common SQL database mistakes
Letting the ORM design everything implicitly
Review actual tables, keys, constraints, indexes, and generated SQL.
Missing foreign-key indexes where queries need them
A foreign key enforces integrity; depending on the engine, it may not automatically create the index needed for your join/delete/update workload.
Giant transactions
Large transactions increase failure and contention cost.
Too many indexes
Read optimization can become write amplification.
Running analytics on the OLTP primary forever
Move heavy historical workloads when they materially compete with customer transactions.
Ignoring query plans
A query that looks simple can still scan millions of rows.
Sharding before optimizing
One well-indexed primary is often easier and faster than a prematurely distributed database.
A practical SQL database checklist
Before shipping a relational schema, ask:
- Does every table have a clear identity?
- Are important relationships protected by foreign keys?
- Are business invariants represented with useful constraints?
- Is duplicated data intentional and owned?
- Are transactions scoped to real atomic business operations?
- Does the isolation level match the correctness requirement?
- Do important queries have measured index strategies?
- Have execution plans been checked for hot paths?
- Are migrations designed for rolling deployments?
- Is read-replica staleness acceptable where replicas are used?
- Are OLTP and heavy analytics still compatible on the same system?
- Would a specialized NoSQL/OLAP/search store genuinely simplify one workload?
SQL databases remain extremely useful because they combine data integrity with query flexibility. They are often the best default when the business data has real relationships and you expect tomorrow's questions to be different from today's.

Discussion (0)