同時に走るトランザクションが、互いの更新をどこまで見てよいか。それが分離レベルです。同じ SELECT でも、レベルによって見える結果が変わります。MySQL のデフォルトは Repeatable Read です。
手元の MySQL で動かしながら、4つのレベルと、そこで起きうる読み取りの問題を確認します。
用語
分離レベル
同時に動くトランザクションが、他トランザクションの更新をどこまで読んでよいかを決める設定です。セッション単位で切り替えられます。
ダーティーリード(Dirty Read)
他トランザクションがまだコミットしていない変更を読んでしまうことです。あとでロールバックされると、読んだ内容と実データが食い違います。
ノンリピータブルリード(Non-repeatable Read)
同じトランザクション内で同じ行を読み直したとき、他トランザクションのコミット済み更新で値が変わることです。
ファントムリード(Phantom Read)
同じトランザクション内でクエリをやり直したとき、他トランザクションの挿入や削除で、行の集合が変わることです。更新ではなく、行の増え減りに着目します。
MVCC
スナップショットを参照する仕組みです。追加や削除もスナップショット側を見るため、ファントムリードが出ないことがあります。
分離レベル
MySQL で実際に動かしながら、各レベルの動作を確認します。よく使うコマンドは次のとおりです。
-- 分離レベルの確認
select @@transaction_isolation;
-- 分離レベルの設定(セッション単位)
set session transaction isolation level <分離レベル>;
-- トランザクションの開始
start transaction;
begin;
-- コミット
commit;
-- ロールバック
rollback;
設定の詳細は MySQL の SET TRANSACTION を参照してください。
未コミット読み取り(Read Uncommitted)
最も低い分離レベルです。あるトランザクションから、他のトランザクションがまだコミットしていないデータを読めます。

図: トランBが age=40 に更新した直後、コミット前でもトランAから 40 が見える。
トランAのセッションを Read Uncommitted にします。
-- 分離レベルの設定: Read Uncommitted
set session transaction isolation level read uncommitted;

トランAでトランザクションを開始し、user1 の age を読むと 30 です。

次にトランBでトランザクションを開始し、age を 40 に更新します。この段階ではまだコミットしません。

この状態でトランAがもう一度読むと、トランBの未コミット更新が見えます(age=40)。

コミット済み読み取り(Read Committed)
広く採用されている分離レベルです。あるトランザクションは、他のトランザクションがコミットしたデータだけを読めます。

図: トランBの更新はコミット後にだけトランAへ見える。同じ SELECT でも、コミット前後で age が 30 から 40 に変わる。
デフォルトになっている主な DB は Oracle、SQL Server、PostgreSQL です。
トランAのセッションを Read Committed にします。
-- 分離レベルの設定: Read Committed
set session transaction isolation level read committed;

トランAでトランザクションを開始し、user1 の age を読むと 30 です。

次にトランBでトランザクションを開始し、age を 40 に更新します。まだコミットはしません。

この状態でトランAがもう一度読むと、トランBの更新前の値のままです(age=30)。

トランBをコミットします。

この状態でトランAが再び読むと、トランBが確定した更新が見えます(age=40)。

繰り返し可能読み取り(Repeatable Read)
より高い一貫性のある分離レベルです。トランザクション開始時点のスナップショットが作られ、終了するまで他トランザクションの更新は見えません。

図: トランBが age=40 をコミットしても、トランAの最中は age=30 のまま。トランAのコミット後に読むと 40 になる。
MySQL のデフォルトは、この Repeatable Read です。
トランAのセッションを Repeatable Read にします。
-- 分離レベルの設定: Repeatable Read
set session transaction isolation level repeatable read;

トランAでトランザクションを開始し、user1 の age を読むと 30 です。

次にトランBでトランザクションを開始し、age を 40 に更新してコミットします。

この状態でトランAがもう一度読んでも、トランBの更新は見えません。更新前の age=30 を繰り返し読めます。

トランAをコミットしたあとで読むと、更新後の age=40 が見えます。

直列化可能(Serializable)
最高レベルの一貫性を保証しようとする分離レベルです。あるトランザクションがデータを更新しているあいだ、他のトランザクションがそのデータを参照しようとすると、待ちになることがあります。一貫性は最も高い一方、並行性は下がり、性能へ影響する可能性があります。

図: トランAの SELECT は、トランBがコミットするまで結果を返さない。
トランAのセッションを Serializable にします。
-- 分離レベルの設定: Serializable
set session transaction isolation level serializable;

トランAでトランザクションを開始します。

トランBでもトランザクションを開始します。

トランBでデータを追加します。

トランAでデータを参照します。待ち状態になり、すぐに結果は返りません。

トランBをコミットします。

トランAで待っていた結果が返ってきます。

分離の問題
ダーティーリード、ノンリピータブルリード、ファントムリードは、各分離レベルで起きうる読み取りの問題です。分離レベルが下がるほど、起きる可能性は高まります。
| 問題 | 起きうるレベル | 何が起きるか |
|---|---|---|
| ダーティーリード | Read Uncommitted | 未コミットの更新が見える |
| ノンリピータブルリード | Read Committed 以下 | 同じ行の再読で値が変わる |
| ファントムリード | Repeatable Read 以下 | 挿入・削除で行の集合が変わる |
ダーティーリード(Dirty Read)
Read Uncommitted のトランザクション(トランA)がデータを読んだあと、他のトランザクション(トランB)がそのデータを更新してロールバックすると、食い違いが起きます。トランAが読んだ内容と、実際の状態が一致しません。
次の例では、トランAは age=40 として読んでいます。トランBの更新はロールバックでなかったことになるので、実データは age=30 です。

図: トランAが読んだ age=40 は、トランBのロールバックで消える。読んだ値と実データが一致しない。
ノンリピータブルリード(Non-repeatable Read)
Read Committed 以下では、同じトランザクション内で同じデータを複数回読むあいだに、最初と違う結果になることがあります。他トランザクションがデータを変更してコミットすると起きます。防ぐなら Repeatable Read か Serializable です。
ただし、ノンリピータブルリードは必ずしも問題ではありません。分離レベルの特性として、正しく動いているとも見なせます。多くの DB では Read Committed がデフォルトなので、この動きは起きえます。実用上は受け入れられることが多く、DB 設計やアプリ要件に合わせてレベルを選ぶことが大切です。
ファントムリード(Phantom Read)
Repeatable Read 以下では、トランザクションの実行中に他トランザクションが行を挿入したり削除したりすると、あとのクエリにその変更が乗る可能性があります。ノンリピータブルリードとの違いは、既存行の更新ではなく、追加や削除に着目する点です。

図: 定義上のファントムリード。途中で行が挿入され、件数の見え方が変わる。
「Repeatable Read ではファントムリードが起きる」と書いている記事は多いです。手元では発生しませんでした。MVCC のようにスナップショットを参照する仕組みだと、追加や削除もスナップショット側を見ます。そのためファントムリードは出ないようです。
参考: ファントムリードと MySQL の Repeatable Read
試しに MySQL で再現を試みたところ、ファントムリードは起きませんでした。
-- ① トランAでトランザクションを開始
start transaction;
-- ② トランAでデータ参照(データ1件のみある)
select * from employee;
-- ③ トランBでトランザクションを開始
start transaction;
-- ④ トランBでデータ追加
insert into employee values(2,'user2',20);
-- ⑤ トランBでコミット
commit;
-- ⑥ トランAで再びデータ参照
select * from employee;
ファントムリードの期待値は、⑥でトランBが追加した④の行が見えることです。結果としては参照できませんでした。再現を見るには、Read Committed 以下にする必要がありそうです。
まとめ
分離レベルは、同時実行のトランザクションが互いの更新をどこまで見るかを決めます。MySQL のデフォルトは Repeatable Read です。
Read Uncommitted は未コミットの更新まで見えるため、実務ではほぼ使いません。Read Committed ではノンリピータブルリードが起きえますが、多くの DB が採用している動きでもあります。ファントムリードは Repeatable Read 以下で起きうるとされます。ただし MySQL では MVCC のスナップショットを参照するため、手元では再現しませんでした。
インデックス側の整理は 【SQL Server】インデックスと実行計画について解説! にあります。