🌟

稼働中のCloud Spannerで別テーブルに切り出し&カラム削除をした話

2023/10/13に公開

こんにちは、テラーノベルでサーバーサイドを担当している@manikaです。
先日稼働中のCloud Spannerからカラムを物理削除を行ったのでその時の方法を記載したいと思います。

概要

エピソードの「読まれた回数」のみを保存していたカラム(ここではPVとしています)を、「誰が、何回読んだか」をわかるようにする為、エピソードテーブルから切り離し、元のPVカラムを削除する事となりました。

↓つまりこれを

// エピソードテーブル
CREATE TABLE Episode(
  ID  STRING(255) NOT NULL,
  UserID STRING(255) NOT NULL,
  Title STRING(255) NOT NULL,
  Content STRING(MAX) NOT NULL,
  PV  INT64 NOT NULL, //テーブルを分けたい対象のカラム
  CreatedAt TIMESTAMP NOT NULL,
  UpdatedAt TIMESTAMP NOT NULL,
)

↓こうしたい。

// エピソードテーブル
CREATE TABLE Episode(
  ID  STRING(255) NOT NULL,
  UserID STRING(255) NOT NULL,
  Title STRING(255) NOT NULL,
  Content STRING(MAX) NOT NULL,
  CreatedAt TIMESTAMP NOT NULL,
  UpdatedAt TIMESTAMP NOT NULL
) PRIMARY KEY(ID)

// どのエピソードを誰が何回読んだかを管理する
CREATE TABLE EpisodePV(
  EpisodeID  STRING(255) NOT NULL,
  UserID     STRING(255) NOT NULL,
  PV         INT64 NOT NULL,
  UpdatedAt  TIMESTAMP NOT NULL
) PRIMARY KEY(StoryID)

上記の対応は稼働中のspannerに対し行う必要があった為、Episode.PVへの書込みや参照が随時行われている状態にあった為、単純にEpisode.PVカラムを削除してしまうと当然エラーが生じてしまいます。

その為いくつかのステップを踏んで作業を行う必要がありました。

ステップ

大まかに以下の5ステップでリリースを行いました。

  1. EpisodePVテーブルを作成してEpisode.PVの更新箇所にEpisodePVへ書込む処理を追加する
  2. Episode.PVのデータをEpisodePVへ移行する
  3. Episode.PVを参照している箇所をEpisodePVからの参照へ変更する
  4. Episode.PVへの書込み処理を削除する
  5. Episodeテーブルからカラムを削除する

1. EpisodePVテーブルを作成してEpisode.PVの更新箇所にEpisodePVへ書込む処理を追加する。

EpisodeHodeへの保存処理を追加します。

episode := &Episode{
      ID: "episode-1",
      PV: pv,
      UpdatedAt: time.Now(),
}
episodeRepository.Put(episode)

+   episodePV := &EpisodePV{
+       EpisodeID: "episode-1",
+       UserID: "user-1",
+       PV: pv,
+       UpdatedAt: episode.UpdatedAt,
+   }
+   episodePVRepository.Put(episodePV)

2. Episode.PVのデータをEpisodePVへ移行する

GCPのCloud Tasksを使用し、実行時点のEpisodeテーブルのデータをEpisodePVテーブルへ移行を行いました。
事前に(1)の書込みのリリースも行っているので、移行作業中Episodeテーブルにデータの追加が行われてもEpisodeとEpisodePVのデータは整合性が保たれる状態になっています。

3. Episode.PVを参照している箇所をEpisodePVからの参照へ変更する

Epidode.PVを参照しているコードをEpisodePVからの参照に修正します。

- //Epidode.PVの値をそのまま返却する。
- func GetPV(epidodeID EpisodeID) bool {
-    episode := episodeRepository.Get(epidodeID)
-    return episode.PV
- }
+ //epidodeIDのトータルPV数を取得する
+ func GetPV(epidodeID EpisodeID) bool {
+    total := episodePVRepository.TotalCount(epidodeID)
+    return total
+ }

4. Episode.PVへの書込み処理を削除する

Episode.PVへの書込み処理を削除する際にコード上で削除するのみでは問題がありました。
発生したエラーはNOTNULL制約が原因だった為、Episode.PVのNOT NULL制約を外しNULLABLEに変更を行ってからコードの修正を行いました。

上記の後、PVへの書込みを削除しました。

 episode := &Episode{
      ID: "episode-1",
-     PV: pv,
      UpdatedAt: time.Now(),
 }
 episodeRepository.Put(episode)

episodePV := &EpisodePV{
    EpisodeID: "episode-1",
    UserID: "user-1",
    PV: pv,
    UpdatedAt: episode.UpdatedAt,
}
episodePVRepository.Put(episodePV)
 

5. カラムを削除する

1〜4まで対応が完了すればあとはカラムを物理的に削除出来る状態となります。
以下のSQLを実行し削除を行いました。

ALTER TABLE Episode DROP COLUMN PV;

まとめ

今回のエピソードテーブルは600万件近いレコード数があり、テラーノベルで使用される主要テーブルのうちのひとつでアクセスも多いのですがサービスに影響無く無事にマイグレーションを行う事ができました。

サービス稼働中の状態で物理的にカラムを削除を行うのはやはり怖さはありましたが手順を細かくわけることで動作の確認も手順毎に行えばよくなり、ある程度の安心感を持って少しずつ進める事ができました。作業の細分化・計画は大事ですね。

テラーノベル テックブログ

Discussion