- Jh123x: Blog, Code, Fun and everything in between./
- My Blog Posts and Stories/
- Databases: Differences between different database variants/
Databases: Differences between different database variants
Table of Contents
Introduction #
While I was working on a solution for a database migration, I learned that the same solutions may not work on different databases. It led me down this rabbit hole where I started to discover the differences between different SQL implementations (e.g: MySQL, PostgreSQL, etc.)
In this blog post, I will be sharing what I have discovered during my research, and it will mainly focus on isolation and the behavior affecting isolation.
Intro to Relational Databases #
Before we dive into the differences, let us go through the main selling points of Relational Databases. Most SQL systems implements ACID to ensure that the database remains consistent and correct.
ACID stands for:
- Atomicity
- Consistency
- Isolation
- Durability
Let us go through what they mean.
Atomicity #
Atomicity means that any transactions within a database must either
- All be executed
- None of them are executed
Implementations #
- Undo : Undo logs store the information required to roll back the action.
- Undo Segments: Undo segments are physical storage areas that contain information to rollback transactions, crash recovery, etc.
- MVCC Snapshots: Snapshots are taken before an action is completed, these snapshots can be used to roll back the current transaction.
Consistency #
Consistency refers to the state transitions within the database. This guarantee ensures that the database is always in a consistent state.
However, the rules for a consistent state is still defined by the user. It is still key for the user to come up with valid constraints.
Isolation #
Isolation controls which effects of concurrent transactions are visible to a transaction.
The different levels of isolation aims to solve these mains issues
- Dirty read: reading uncommitted data written by another transaction (which may later be rolled back).
- Non-Repeatable read: Reading the same data 2 times but getting different values.
- Phantom read: Doing the same query twice and getting a different number of rows.
The SQL standard describes four levels (from weakest to strongest):
- READ UNCOMMITTED — may see uncommitted (dirty) changes.
- READ COMMITTED — prevents dirty reads, but non-repeatable reads and phantom reads are still possible.
- REPEATABLE READ — prevents dirty and non-repeatable reads by providing a stable snapshot for each transaction, but the standard allows phantom reads in some implementations.
- SERIALIZABLE — provides the strongest guarantee: transactions behave as if executed serially; dirty, non-repeatable and phantom reads are prevented.
Note: how these levels behave in practice depends on the database engine.
For example, InnoDB (MySQL) defaults to REPEATABLE READ and uses next-key locks to prevent phantoms in many cases, making it stronger than the generic “repeatable read” description.
PostgreSQL’s REPEATABLE READ is implemented as a snapshot (SI) and PostgreSQL’s SERIALIZABLE uses Serializable Snapshot Isolation (SSI), which can cause conflicting transactions to abort (requiring retries).
The semantics of the SQL Standard may differ from the actual implementation. Refer to each product’s docs for exact behavior.
Each of this isolation aims to solve 1 of the problems. However, for each level we go up to solve the problem, the lower the performance and the number of supported concurrent writes.
Implementations #
There are a few main approaches to Isolation.
- Multi Version Concurrency Control (MVCC) / Snapshot Isolation (SSI)
- Pessimistic Lock
- Optimistic Lock
Multi Version Concurrency Control (MVCC) / Snapshot Isolation (SSI) #
Every tuple has a version number, the tuple with the greatest version number can have a read operation done simultaneously. For writes, it is only done on a copy of the data. While writes are happening (before committing), all read users will still see the previous version Upon completion of writing, the version tuple is bumped up and is used for future reads / copy for future writes.
As we can see from the diagram above, there is a snapshot for each read. For each read, if it touches the same table multiple times, the read for each table will be the same.
The exact implementation for the different SQL variants may be different, but the general behavior follows what is outlined in the diagram above.
- InnoDB (MySQL): old row versions are recorded in undo logs and a global history list; consistent reads consult undo information to reconstruct the visible version. Writes update pages and create undo entries so old versions remain visible to other transactions until no longer needed.
- PostgreSQL: each tuple carries visibility metadata (
xmin,xmax) and multiple tuple versions are kept in the table; visibility logic determines which version a transaction sees. PostgreSQL uses WAL for durability and implements snapshot isolation semantics for repeatable reads; SERIALIZABLE is implemented using SSI.
For more information, refer to their documentation below:
Pessimistic Lock #
A Pessimistic lock is a method of locking where we assume that data conflicts / inconsistencies are likely. Under this scheme, we always acquire the locks upfront before performing any read / write operations. The same operation from the above will look something like this.
During each of the steps, each user has exclusive access to the database to do the reading / writing. In this implementation, we guarantee that the data the reader/writer is accessing is the latest version. The tradeoff is that it will reduce the number of concurrency.
The locking mechanism can also be split into row level locks and table level locks.
- For row level locks, readers that read from the same table multiple times may have more/less resulting rows depending on the concurrent writer transaction
- For table level locks, concurrency is further reduced, only 1 reader/writer can access the table at any 1 time.
Optimistic Lock #
An Optimistic lock is a method of locking where we assume that the data does not conflict. Under this scheme, we do not acquire the lock and perform the read/write operation first. Before we start the read/write, we take note of the updated time of the row. Before we complete the write commit, we check the updated at time is updated before.
If the updated time has been changed, we discard the current operation and retry with a new operation. Otherwise, we will commit the operation.
Durability #
Durability ensures that committed data will survive in the event of power loss. There are 2 implementations when it comes with durability
Implementations #
| Durability Type | Description | Trade off |
|---|---|---|
| Async Writes | Writes to disk are done asynchronously | Durability risk, but faster write performance |
| Sync on Write | Writes to disk are done when there is a write | High durability, but slower write performance |
Database behavior #
There are many different implementations of databases that we can use. Let us explore the different types of database implementations.
In this blog post, we will explore the differences between these implementations:
- MySQL
- PostgreSQL
MySQL #
MySQL is an open source relational database management system. It is flexible as we can plug in and change the engine depending on our needs.
The default engine for MySQL is InnoDB for ACID workflows. Other engines that can be plugged into MySQL mainly consider different tradeoffs between:
- ACID Properties
- Speed
- Other special features
If I do not want transactions but want the database to go very fast, we can go with the MyISam.
However, the trade-off for this engine is that it does not support transactions.
Note: This is not recommended for important data.
MySQL with InnoDB uses MVCC to enforce the isolation level by default.
However, we can also specify the required levels of isolation using the SET TRANSACTION syntax
-- Sets the transaction to use the SERIALIZABLE isolation level to prevent phantom reads.
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
While Data Manipulation Languages (DMLs) are supported by transactions, InnoDB does not support wrapping Data Definition Languages (DDLs) within transactions.
In newer versions of MySQL, there is support for a limited subset of atomic DDLs but DDL still implicitly ends/commits the surrounding transaction. You can read more about it in the MySQL Atomic Support for DDLs.
As a result, multiple DDLs still cannot be rolled back together. Do check the documentation based on your version of MySQL to find out the implementation for that version
Let’s go through what happens during a transaction in each of the cases.
-- This example is adapted from the MySQL website.
BEGIN TRANSACTION;
SELECT @A:=SUM(salary) FROM table1 WHERE type=1; -- Data is selected based on the MVCC Snapshot
UPDATE table2 SET summary=@A WHERE type=1; -- Table 2 is updated but the information is not commited
COMMIT; -- Data is only committed here
In the DML case above, the data modified during the transaction is only committed at the end of the transaction when the COMMIT command is invoked.
There are no hidden commits within the transaction,
BEGIN TRANSACTION;
ALTER TABLE user_info ADD email text; -- DDL is autocommitted by default.
ROLLBACK; -- Does nothing
In the DDL case above, the ALTER TABLE statement is auto-committed and cannot be rolled back.
This also applies to any other DDLs.
For any solutions that executing any DDLs within transactions and allowing rollback, MySQL with InnoDB will not be able to support it. For such cases, please use PostgreSQL instead.
There are other keywords that cannot be rolled back. You can find the full list here (implicitly committed) and here (cannot be rolled back)
If there are multiple conflicting transactions at the same time resulting in a deadlock, one of them will be rolled back with an error and the other will continue execution.
PostgreSQL #
PostgreSQL is another open source relational database management system. However, unlike MySQL, it does not have pluggable engines. Instead of pluggable engines, it has extensions. Extensions modify the behavior of PostgreSQL, you can find the full list of the extensions here.
The default isolation level in PostgreSQL is Read Committed.
On higher levels of isolation, PostgreSQL makes use of Multi-version Concurrency Control (MVCC) with Write ahead logs (WAL) for isolation. This means that each query hitting the database see a particular version of the data and ensures repeatable reads and phantom reads (the repeatable read isolation level in PostgreSQL prevents phantom read)
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ... perform operations ...
COMMIT;
BEGIN;
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- ... perform operations ...
COMMIT;
Similar to MySQL, we can specify the transaction level that we want when starting a transaction similar to the MySQL example above. There is also a command to specify the default isolation level.
PostgreSQL will also error if there are multiple conflicting transactions at the same isolation level when performing at Repeatable Read level and above.
ERROR: could not serialize access due to concurrent update
Do note that for repeatable read level of isolation, be prepared to retry as PostgreSQL will return an error when it encounters a concurrent update.
ERROR: could not serialize access due to read/write dependencies among transactions
Similar to the repeated read level, if there is another conflicting serializable transaction, one of the transactions will fail with the error above. If there are such errors, we will have to retry at the application level which can incur higher write costs.
When using transactions within an isolation level, there are some additional considerations we can take to improve performance
- Using
READ ONLYkeywords when possible. - Using a connection pool to reduce the number of transactions with transactions
- Use as little statements in the transaction as possible.
- Tune other PostgreSQL settings based on the requirements and data access pattern.
Most DDLs within PostgreSQL can be wrapped in transactions and can be rolled back.
However, do take note of some edge cases which does not support this behavior like CREATE INDEX (CONCURRENTLY).
Please check if your PostgreSQL statements support rollback before running it in production.
MySQL vs PostgreSQL #
This chart shows the difference between MySQL and PostgreSQL
| Dimension | MySQL (InnoDB) | PostgreSQL |
|---|---|---|
| Core Engine Model | Pluggable engines (InnoDB = ACID MVCC, MyISAM = non-TX) | Single MVCC engine + extensions |
| Default Isolation | REPEATABLE READ (InnoDB default) | READ COMMITTED (default) |
| Serializable Mode | Lock-based, more blocking | True Serializable via SSI (may abort) |
| MVCC Implementation | Undo logs + history list | Tuple version chains with xmin/xmax and visibility rules |
| DDL In Transactions | Many DDLs implicitly commit (Later versions may differ) | Mostly transactional (can ROLLBACK) for many DDLs |
| Replication | Binlog based (async by default) | Streaming WAL, logical available |
| Scaling Approach | Read replicas + sharding + NDB Cluster | Streaming replicas + Citus + FDWs |
| Index Types | B-tree, Full-text, Spatial (InnoDB) | B-tree, GIN, GiST, BRIN, SP-GiST |
| Durability Control | innodb_flush_log_at_trx_commit allows tuning | WAL fsync tuning, synchronous_commit settings |
| License / Cost | Open source (GPL) | Open source (PostgreSQL License) |
| Best For | Web workloads, simple scaling, flexible engines | Strong SQL, correctness, complex queries, extensions |
Conclusion #
There are a lot of differences between different variants of SQL implementations. Depending on what you want to do, there may be some caveats that you need to take note of.