誤って削除したデータを取り戻す 〜 TiDB Flashback編

## はじめに

アドベントカレンダーにあわせて毎年本番環境でやらかしたしくじりが話題になりますが、
その中でも影響範囲が大きいのはDBでのやらかしではないでしょうか。

本番環境のデータをTRUNCATEしてしまったり、WHEREをつけずにDELETE を書いてしまった、といった話は非常にポピュラーですが、DBMSはそれに対して何の対策もないのでしょうか？

TiDBにはそれがあります!! [Flashback機能](https://docs.pingcap.com/tidb/stable/sql-statement-flashback-table)と[ステイル読み取り](https://docs.pingcap.com/ja/tidb/stable/dev-guide-use-stale-read)機能がそれです。
本記事ではTiDBにおける誤って削除してしまったデータを取り戻すためのいくつかの手段について説明します。長くなってしまったので、Flashbackとステイル読み取りの2編でお送りします。

ステイル読み取り編はこちらです。

https://qiita.com/bohnen/items/c43470234b582a2cabff

## TiDBのデータ復元の仕組み

TiDBは追記型のストレージを使っており、レコードに更新があった際にそのレコードを書き換えるのではなく、更新レコードを追加する方式を取っています。

このデータを使って過去データを取得するのですが、普通に考えると更新頻度が高くなると、余分に容量を使うことになります。そのため、定期的に削除する処理が走ります。これが[GC](https://docs.pingcap.com/ja/tidb/stable/garbage-collection-overview)と呼ばれる処理です。

このGCで削除される範囲（基準時間）はパラメーターで調整可能で、まずは対象をリカバリしたい期間以上に長くする必要があります。デフォルト**10分**ですが、現実的なところでは1日でしょうか。それ以上に長くすることも可能ですが、その分保持する履歴が多くなり若干ですが性能影響もあります。

:::note info
v8.5.0 からは、高速化のために最新版の履歴のみをメモリに保持する、[MVCC In Memory Engine](https://docs.pingcap.com/tidb/dev/tikv-in-memory-engine)が実装されました
:::

## 準備

この記事ではTiDB Playgroundを使います。
```bash
> tiup playground v8.5.0
```

まずはGCでデータが消されないよう、パラメータを調整します。とりあえず24時間にしましょう。mysqlクラアントで接続し、パラメーターを変更します。こうすることで、GCが動作した際に消される履歴の対象が24時間以上前のものになります。

```sql
mysql> set global tidb_gc_life_time=24h;
Query OK, 0 rows affected (0.02 sec)
```

おなじみのEmployeesテーブルを読み込んでおきます。手順は下記のlightningの記事を参照ください。

https://qiita.com/bohnen/items/8078bb545c2a8050247b

## Flashback Table

https://docs.pingcap.com/ja/tidb/stable/sql-statement-flashback-table

Flashback Tableは、DROPもしくはTRUNCATEしてしまったテーブルを復元できます。それぞれ見ていきましょう。

### DROPの場合

まずはDropです。employeesは外部キーで参照されており消せないので、dept_emp を消しましょう。
```Dropの場合.sql
mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.15 sec)

-- テーブル削除
mysql> drop table dept_emp;
Query OK, 0 rows affected (0.08 sec)

-- 削除されていることを確認
mysql> select count(*) from dept_emp;
ERROR 1146 (42S02): Table 'employees.dept_emp' doesnt exist

-- テーブル復元
mysql> flashback table dept_emp;
Query OK, 0 rows affected (0.07 sec)

-- 復元されていることを確認
mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.02 sec)
```

dropした後にflashbackして、復元できていることがわかります。ドキュメントを確認すると、DROPした際にTiDBはテーブルのメタデータ（テーブル定義等）を削除して、データの削除はGCにまかせていることがわかります。そのため、GCする前であればメタデータを復元するだけでテーブルの内容が復元できる、というわけです。

### TRUNCATEの場合

次に、TRUNCATEの場合をみていきます。TRUNCATEの場合はテーブル自体が残っています。元のテーブルには復元できず、別の名前で復元する必要があります。

```TRUNCATE.sql
mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.00 sec)

-- データを削除
mysql> truncate table dept_emp;
Query OK, 0 rows affected (0.08 sec)

-- データが削除されていることを確認
mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.01 sec)

-- 別のテーブルとして復元
mysql> flashback table dept_emp to dept_emp2;
Query OK, 0 rows affected (0.07 sec)

-- 復元後のテーブルにはデータが戻っていることを確認
mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec)

mysql> select count(*) from dept_emp2;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.00 sec)
```

データを `dept_emp2` に復元できていることがわかります。
さて、どうやって元のdept_empにデータを復元するのが良いでしょうか？`INSERT INTO ... SELECT` で元のテーブルに戻す? 

ドキュメントを確認すると、TRUNCATEの場合でも、Flashback Tableはメタデータを復元しているだけだということがわかります。実は同じテーブルメタデータを名前を替えて復元しているだけです。確かめてみましょう。
TiDBは内部的にテーブルにIDを振っており、これは `information_schema.tables` で確認できます。

```sql
-- Flashback前
mysql> select table_name, tidb_table_id from information_schema.tables where table_name like 'dept_emp%';
+------------+---------------+
| table_name | tidb_table_id |
+------------+---------------+
| dept_emp   |           120 |
+------------+---------------+
1 row in set (0.01 sec)

-- Flashback後
mysql> select table_name, tidb_table_id from information_schema.tables where table_name like 'dept_emp%';
+------------+---------------+
| table_name | tidb_table_id |
+------------+---------------+
| dept_emp2  |           120 |
| dept_emp   |           130 |
+------------+---------------+
```

なんと、復元後の `dept_emp2` の方に元のIDが振られていることがわかります。名前が違うだけの元のテーブルです。

ということで、安心してテーブルが復元できます。そう、名前を付け替えればいいだけです。

```sql
mysql> rename table dept_emp to dept_emp_del;
Query OK, 0 rows affected (0.06 sec)

mysql> rename table dept_emp2 to dept_emp;
Query OK, 0 rows affected (0.07 sec)

mysql> select count(*) from dept_emp;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.33 sec)
```

:::note info
Flashback Tableはテーブルメタデータを復元しているだけで、データはGC前の履歴データをそのまま利用しているというところがポイントです。そのため、復旧自体は一瞬です。
:::

## Flashback Database

https://docs.pingcap.com/ja/tidb/stable/sql-statement-flashback-database

さて、同様にデータベースも復元できます。やってみましょう。

```Database.sql
mysql> drop database employees;
Query OK, 0 rows affected (0.11 sec)

mysql> select count(*) from employees.dept_emp;
ERROR 1146 (42S02): Table 'employees.dept_emp' doesnt exist
mysql> flashback database employees;
Query OK, 0 rows affected (0.10 sec)

mysql> select count(*) from employees.dept_emp;
+----------+
| count(*) |
+----------+
|   331603 |
+----------+
1 row in set (0.01 sec)
```

これもメタデータの復元でしょう、一瞬で復元できます。使うことはないような気がしますが、`FLASHBACK TABLE` と同様に別名での復元も可能です。

## Flashback Cluster

https://docs.pingcap.com/ja/tidb/stable/sql-statement-flashback-cluster

さて、いままではテーブルなりデータベースなりの全量削除の話をしてきましたが、アプリのリリースにバグがあり、一定時間以降のデータに誤ったデータが混入された ー こんなケースはどうでしょうか？

TiDBには、`FLASHBACK CLUSTER`という、クラスタの状態を過去時点に戻す機能があります。試してみましょう。

:::note warn
FLASHBACK CLUSTERはTiDB Serverlessでは利用できません。TiDB Serverlessでは、バックアップからの復元をかわりに利用できます。
:::

```CLUSTER.sql
-- 起点となる時間の確認
mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2024-12-24 08:26:26 |
+---------------------+
1 row in set (0.00 sec)

mysql> select * from departments;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d009    | Customer Service   |
| d005    | Development        |
| d010    | Example Department |
| d002    | Finance            |
| d003    | Human Resources    |
| d001    | Marketing          |
| d004    | Production         |
| d006    | Quality Management |
| d008    | Research           |
| d007    | Sales              |
| d011    | TiDB Department    |
+---------+--------------------+
11 rows in set (0.00 sec)

-- 間違ったレコードの挿入
mysql> insert into departments values ('d0XX', 'Wrong Department');
Query OK, 1 row affected (0.00 sec)

mysql> select * from departments;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d009    | Customer Service   |
| d005    | Development        |
| d010    | Example Department |
| d002    | Finance            |
| d003    | Human Resources    |
| d001    | Marketing          |
| d004    | Production         |
| d006    | Quality Management |
| d008    | Research           |
| d007    | Sales              |
| d011    | TiDB Department    |
| d0XX    | Wrong Department   |
+---------+--------------------+
12 rows in set (0.00 sec)

-- クラスタの復元
mysql> flashback cluster to timestamp "2024-12-24 08:26:26";
Query OK, 0 rows affected (1.22 sec)

mysql> select * from departments;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d009    | Customer Service   |
| d005    | Development        |
| d010    | Example Department |
| d002    | Finance            |
| d003    | Human Resources    |
| d001    | Marketing          |
| d004    | Production         |
| d006    | Quality Management |
| d008    | Research           |
| d007    | Sales              |
| d011    | TiDB Department    |
+---------+--------------------+
11 rows in set (0.00 sec)
```

データが復元されているのがわかります。ドキュメントによれば、これは**過去データで現在のデータを更新している**とのことです。

:::note warn
過去時点に戻しているのではなく、過去データで上書きしているという点に注意してください。つまり、**誤ったデータを挿入した履歴**は残っています。

実際、次回説明するステイル読み取りを使い、直前のテーブル状態を読み取ると、誤ったデータがある状態を読み取ることができます。

```sql
mysql> select * from departments as of timestamp now() - interval 300 second;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d009    | Customer Service   |
| d005    | Development        |
| d010    | Example Department |
| d002    | Finance            |
| d003    | Human Resources    |
| d001    | Marketing          |
| d004    | Production         |
| d006    | Quality Management |
| d008    | Research           |
| d007    | Sales              |
| d011    | TiDB Department    |
| d0XX    | Wrong Department   | -- 誤ったデータ
+---------+--------------------+
12 rows in set (0.00 sec)
```
:::


## おわりに

いかがでしたでしょうか。TiDBのFlashbackについて説明しました。TiDBの内部の仕組みを応用した便利機能なので、ぜひ覚えておいてください！