誤って削除したデータを取り戻す 〜 TiDB ステイル読み取り編

## はじめに

本記事は過去時点のデータを取得するTiDBの機能を紹介する2回目の記事です。
1回目のFlashback編はこちらをどうぞ。

https://qiita.com/bohnen/items/47e8dc9ba8f95275ff9a

本記事ではもう一つの機能、ステイル読み取りについて説明します。ステイル読み取りもFlashback同様過去履歴を取得しますが、リカバリより過去の特定時点の断面を取得することに重点が置かれているのが特徴です。

## 準備

前回同様、TiDB PlaygroundとEmployeesデータベースを利用します。
今回は時間をおいて複数のレコードを追加しました。

```sql
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.01 sec)

-- 追加
mysql> insert into departments values ('d012', 'New Department');
Query OK, 1 row affected (0.01 sec)

-- しばらく後で追加
mysql> insert into departments values ('d013', 'More New Department');
Query OK, 1 row affected (0.00 sec)
```

## ステイル読み取りとは

https://docs.pingcap.com/ja/tidb/stable/dev-guide-use-stale-read

ステイル読み取りは、過去のある時点のデータを読み取るSELECTの拡張構文です。

TiDBのステイル読み取りには性能面での効果もあります。通常、TiDBでSELECTを行うと、対象の行のキー範囲に応じたTiKVリーダーノードにリクエストが飛びます。当然、キーの範囲が重複したリクエストが多重に行われると、特定のTiKVリーダーノードの負荷が上がってしまいます。

ステイル読み取りの場合、過去データ（すでに複数のTiKVノードにコピーされたデータ）を利用するため、この制限がありません。コピーを持つすべてのTiKVノードにリクエストが分散されるため、スループットの増加が期待できます。

CQRSなどを利用しており、参照側の結果に厳密な即時性を求めない場合などに使うと性能向上に役立つかもしれません。

## ステイル読み取り(1) - 時刻指定

早速ステイル読み取りを試していきます。　前回も使った departmentsテーブルを使っていきましょう。

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

-- 時刻指定
mysql> select * from departments as of timestamp "2024-12-24 15:50:00"  order by dept_no;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d001    | Marketing          |
| d002    | Finance            |
| d003    | Human Resources    |
| d004    | Production         |
| d005    | Development        |
| d006    | Quality Management |
| d007    | Sales              |
| d008    | Research           |
| d009    | Customer Service   |
| d010    | Example Department |
| d011    | TiDB Department    |
+---------+--------------------+
11 rows in set (0.00 sec)

-- 相対時間を指定
mysql> select * from departments as of timestamp now() - interval 100 minute order by dept_no;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d001    | Marketing          |
| d002    | Finance            |
| d003    | Human Resources    |
| d004    | Production         |
| d005    | Development        |
| d006    | Quality Management |
| d007    | Sales              |
| d008    | Research           |
| d009    | Customer Service   |
| d010    | Example Department |
| d011    | TiDB Department    |
| d012    | New Department     |
+---------+--------------------+
12 rows in set (0.00 sec)
```

このような形で、過去のある時間の断面を見ることができます。

:::note 
ドキュメントによれば、推奨は5秒以上前の値を指定することです
:::

## ステイル読み取り(2) - 範囲指定

範囲指定での検索も可能です。先ほどのテーブルを範囲指定で見てみます。

```sql
mysql> select * from departments AS OF TIMESTAMP TIDB_BOUNDED_STALENESS("2024-12-24 15:50:00","2024-12-24 16:00:00") order by dept_no;
+---------+--------------------+
| dept_no | dept_name          |
+---------+--------------------+
| d001    | Marketing          |
| d002    | Finance            |
| d003    | Human Resources    |
| d004    | Production         |
| d005    | Development        |
| d006    | Quality Management |
| d007    | Sales              |
| d008    | Research           |
| d009    | Customer Service   |
| d010    | Example Department |
| d011    | TiDB Department    |
| d012    | New Department     |
+---------+--------------------
```
先ほどみた通り、`15:50` の段階では `d012` は含まれていませんでしたが、このように `16:00` を指定したところ含まれています。

:::note info
ところで、範囲指定の結果は第ニ引数（先の例の場合は `16:00`）を指定した時間指定の結果と一緒です。この２つの文の違いは概要のところで説明した、パフォーマンスに関係するものです。

何らかの原因でTiKVのレプリカへの反映が遅延していて、`16:00` のデータがまだ未反映だったとします。その場合時間指定のクエリは失敗し、内部でリトライを行いTiKVリーダーに問い合わせます。

範囲指定の場合は、`15:50` から `16:00` の間で、反映済みのものから最新のデータを返すことができます。たとえば、`15:55` まで反映できていたら、その時点のデータを返すことができます。よりレプリカの方で処理できるようにするものと言えるでしょう。
:::

## ステイル読み取りを用いたデータの復元

さて、これまでステイル読み取りを使ってデータの追加を見てきました。
データの削除についても同様に、過去の状態を復元することができます。

```sql
-- 削除
mysql> delete from departments;
Query OK, 14 rows affected (3.80 sec)

mysql> select * from departments;
Empty set (0.01 sec)

-- 現在時刻の確認
mysql> select now();
+---------------------+
| now()               |
+---------------------+
| 2024-12-24 19:30:15 |
+---------------------+
1 row in set (0.00 sec)

-- 過去状態を取得
mysql> select * from departments as of timestamp "2024-12-24 19:29:00" order by dept_no;
+---------+----------------------+
| dept_no | dept_name            |
+---------+----------------------+
| d001    | Marketing            |
| d002    | Finance              |
| d003    | Human Resources      |
| d004    | Production           |
| d005    | Development          |
| d006    | Quality Management   |
| d007    | Sales                |
| d008    | Research             |
| d009    | Customer Service     |
| d010    | Example Department   |
| d011    | TiDB Department      |
| d012    | New Department       |
| d013    | More New Department  |
| d014    | In Transaction Dept2 |
+---------+----------------------+
14 rows in set (0.01 sec)
```

削除前のデータが取得できましたね！ これを用いてデータの復元を試みます。

```sql
mysql> insert into departments select * from departments as of timestamp "2024-12-24 19:29:00";
ERROR 8135 (HY000): can not set different time in the as of
```

エラーになってしまいました。

:::note warn
残念ながら、as of select で取得したレコードを（同一セッション内で）insertすることはできません。
:::

ではどうするかですが、実はdumplingには時間指定でデータを出力するオプションがあります。上記のAS OF TIMESTAMPと同じようにするには、

```bash
tiup dumpling -h 127.0.0.1 -P 4000 -u root -T employees.departments \
-o asof_output/ -m --filetype sql --snapshot "2024-12-24 19:29:00"
```

```employees.departments.000000000.sql
/*!40014 SET FOREIGN_KEY_CHECKS=0*/;
/*!40101 SET NAMES binary*/;
INSERT INTO `departments` VALUES
('d001','Marketing'),
('d002','Finance'),
('d003','Human Resources'),
('d004','Production'),
('d005','Development'),
('d006','Quality Management'),
('d007','Sales'),
('d008','Research'),
('d009','Customer Service'),
('d010','Example Department'),
('d011','TiDB Department'),
('d012','New Department'),
('d013','More New Department'),
('d014','In Transaction Dept2');
```

あとはこのSQLを実行するなりlightningでimportするなりすれば、データが復元できます。

## おわりに

2回に分けましたが、TiDBでデータを誤ったデータを修正する際に便利なFlashbackとステイル読み取りについて説明しました。ステイル読み取りについては多少の即時性を犠牲にできる場合（分析のためのデータ抽出とかですね）に負荷を軽減しつつデータを取得できます。覚えておいて損はないでしょう。