履歴の適用日を日付化しました —— 大規模なデータ構造変更の苦労と学び – SmartHR Tech Blog

こんにちは、プロダクトエンジニアのkitazawaです。

私が所属するチームは、SmartHR最大のアプリケーションである基本機能の技術的課題の解消を進めています。
本記事はその中の一つである、履歴の適用日を日付化したデータ構造の変更プロジェクトをご紹介します。
これから大規模なテーブル構造の変更を考えている方の参考になれば幸いです。

履歴の適用日とは

プロジェクトの説明の前に、まずは「履歴の適用日」について説明します。

履歴の適用日は、従業員情報やマスターデータの変更が、システム上で有効になる日付を指します。部署の異動日や、役職の変更が適用される日などが該当します。
たとえば、「従業員の所属部署が部署Aから部署Bに変更された」履歴の適用日が2024/04/01であれば、2024年4月1日から部署Bに所属していることを意味します。
また、その後に「従業員の所属部署が部署Bから部署Cに変更された」履歴の適用日が2024/10/01として追加されると、従業員の部署Bへの所属は2024年9月30日まで、部署Cへの所属は2024年10月1日からということになります。

履歴の適用日を説明する図
履歴の適用日を説明する図

「適用日」というくらいなので、そもそも日付なのでは?と思いますよね。しかし、以前は適用日を時刻情報も含むtimestamp型で保存していました。

履歴の適用日に時刻が含まれていることによる課題

履歴の適用日に時刻が含まれることで、ユーザーの認識負荷が高くなり、開発者にとっては考慮事項の増加、実装の複雑化や仕様のズレなどの課題が発生していました。
履歴は従業員情報や家族情報、一部のマスターデータなど、プロジェクト開始時点で約40件のモデルで管理していたため、影響はとても大きなものでした。具体的には以下のような課題が発生していました。

ユーザーの認知負荷が高い課題

適用日に時刻が含まれることで、ユーザーにとって認知負荷が高いという問題がありました。

まず、ユーザーにとって適用日は日付であるという認識がありました。しかし、履歴の一覧画面で異動歴などを確認する際に、本来必要のない同一日に複数の履歴が表示され、どの履歴が重要なのか判断に迷うケースがありました。
例えば、部署を誤って登録した後に修正すると、同一日に2回変更されたという履歴が表示されていました。ここで本来必要なのは修正後の履歴のみです。
加えて単純に不要なデータが多く、認知・管理コストが高い状態でした。

さらに、履歴の一括更新でも課題がありました。履歴の一括出力後、Excelで適用日を編集するとExcelの仕様で時分秒の情報が「00:00:00」に丸められてしまい、既存履歴の上書き更新ではなく新規登録として処理され、意図しない履歴の重複が発生していました。

これらの問題に付随して、サポートへの問い合わせも増加傾向にあり、社内の負荷も増大していました。

開発時の複雑さが増加する課題

開発時は、適用日に時刻が含まれることで実装判断が複雑になっていました。適用日を指定した従業員情報やマスターデータの参照時に、時刻の扱いで迷いました。ユーザーが操作する画面で時点を指定する場合は日付のみを指定するインターフェースになっています。
しかし、内部的には時刻情報が含まれるため取得したいデータに合わせて「その日の最新の状態を取得したい場合は23:59:59」、「その日の開始時点の状態を取得したい場合は00:00:00」といった使い分けが必要でした。
そのため、従業員リストの時点指定と従業員情報のダウンロードの時点指定、従業員情報の履歴の追加の時点指定と参照する部署マスタの時点指定など、機能ごとに適切な時刻を判断する必要があり、開発者が迷う場面が多くありました。

また、適用日はデータベースにマイクロ秒の精度で保存されていましたが、履歴情報の画面では秒までしか表示していませんでした。そのため、一括更新などでユーザーが指定する適用日は秒未満が切り捨てられ、データベースの値との精度の違いによる問題が発生していました。

複数データを同時更新する時の課題

複数データの同時更新においても、時刻が含まれることによる課題があります。
複数データを一度に更新する場合に適用日を固定して処理するようにしないと、同時に処理したはずのデータ間で適用日が微妙にずれてしまうという問題がありました。
例えば部署マスターの更新、並べ替え、削除、従業員部署の登録、更新などでは多数のモデルが同一トランザクションで更新されます。
このとき、適用日の時刻を固定して揃えないと、処理タイミングの僅かなズレによって適用日が秒未満の精度で異なった履歴が作成されてしまいます。
それにより、検索条件に適用日を指定した参照処理で意図した履歴を抽出できないなどの問題に繋がります。

特定履歴の操作の実装が困難な課題

社員番号や部署コードと適用日から特定の履歴を操作する機能の実装も複雑でした。同一適用日の更新において、社員番号と適用日の組み合わせでレコードを特定する際、ユーザーから秒未満を切り上げた日時を受け取り、秒未満を考慮して対象の履歴を探すという煩雑な処理が必要でした。

上記以外にも、適用日に時刻が含まれていることによる課題が複数あり、開発速度の低下や考慮漏れによる不具合が発生していました。

どのようにデータ移行したか

これらの課題を解決するため、適用日を保存するカラムをtimestamp型からdate型に変更することにしました。ユーザーにとって適用日は日付として認識されていることから、そこに寄せていくことが機能利用・理解やUX改善にもつながると考えました。

ただし、単純にtimestamp型からdate型に変更することはできません。データ型の変更は破壊的変更であり、一度実行すると元に戻すことが困難です。さらに、同一日に時刻の異なる複数の履歴が存在する場合、それらを1つの履歴に統合する必要もあります。そこで、段階的なアプローチでリスクを最小限に抑えながら移行を進めることにしました。

参照時刻を23:59:59に固定し、date型変更後の動作を再現

データ型をdate型に変更する前段階として、まず参照時刻を23:59:59に固定する方法を実装しました。これは、実際にデータベースのカラム型を変更する前に、変更後の挙動を再現するための仕組みです。

具体的には、適用日を指定してデータを参照する際に、指定された日付の23:59:59を参照時刻として使用するように変更しました。例えば、「2025/01/01」を指定した場合、「2025/01/01 23:59:59」として扱うことで、同一日の複数履歴のうち最後の履歴を取得できます。
これにより、同一日の複数の履歴が統合された状態と同じ結果を得られます。

この方法を採用した理由は、破壊的変更であるデータ型変更を実行する前に、変更後の挙動を安全に検証できるからです。問題が発生した場合は、参照時刻の固定を無効にするだけで元の挙動に戻すことができ、リスクを最小限に抑えられます。

timestamp型とdate型の両立

次の段階では、既存のtimestamp型カラムを残しながら、新しくdate型カラムを追加して両方を並行運用する期間を設けました。

データの同期については、レコードを保存・更新するとき自動的にtimestamp型の値をdate型に変換して書き込むようにしました。既存データについては、バッチ処理で段階的にtimestamp型の値をdate型に変換して同期しました。

並行運用期間中はWHERE句やORDER句に指定するカラムを引き続きtimestamp型にすることで、段階的に移行を進めました。

最終的に採用したデータ移行手順

最終的には、以下のように段階的にデータ移行を実施するという社内で運用経験のある手順を採用しました。
ここでは詳細を記載しませんが、既存のtimestamp型のカラムに排他制約が存在したため、date型でも同じく排他制約を追加したり、インデックスの移行なども実施しました。

  1. date型のカラムを追加する
  2. date型のカラムに排他制約を追加する
  3. date型のカラムも同時に更新する実装を追加する
  4. 既存データのdate型カラムを更新する
  5. NOT NULL制約を追加する
  6. インデックスを移行する
  7. アプリケーションコードを段階的に切り替える
  8. timestamp型の排他制約を削除する
  9. 同日に複数存在する履歴を削除する

この手法により、各段階でのリスクを分散し、問題が発生した場合の影響を最小限に抑えることができました。

今回は採用を見送った方法

データ移行の方法として、論理レプリケーションの活用も検討しました。これは、データベースの変更を別のデータベースにリアルタイムで反映する仕組みで、大規模なデータ移行で利用されることがあります。

この方法では、新しいスキーマのデータベースを用意し、論理レプリケーションで既存データを同期しながら、適切なタイミングで切り替えるという手法が考えられました。自前でデータ同期の実装をしなくて済み、ダウンタイムを最小限に抑えられる可能性がある魅力的な選択肢でした。

しかし、検討の結果今回は採用を見送りました。主な理由は、論理レプリケーションの運用経験が社内になく、本番環境で利用するには不確実性が高いと判断したためです。また、リスクを取って手順を確立したとしても、規模の大きいデータ移行を今後対応するか不明であり、データベースのバージョンアップなど現在と環境が変わっていた場合に活用できない可能性があるためです。

結果として、より確実で運用経験のある手法を選択することにしました。

想定以上の時間を要した調査フェーズ

プロジェクトを開始した当初は半年くらいで完了するのではないかと見積もっていたのですが、調査フェーズだけで半年以上を要しました。想定以上の時間がかかってしまった要因を振り返ってみました。

広範囲な影響と破壊的変更のリスク

このプロジェクトの影響範囲は非常に広く、SmartHRの中核機能である従業員情報やマスターデータの仕組みを変更するものでした。さらに、時刻情報を削除する変更であり、一度実行すると元に戻すことが困難な破壊的変更だったため、調査を慎重に進める必要がありました。

次々と見つかる新しい問題

調査を進める中で新しい問題が次々と見つかり、解決するためにさらなる調査が必要になるという状況が続きました。繰り返し調査していると最初に見つかった問題にたどり着き、振り出しに戻った感覚になることもありました。
特に、適用日の参照時刻を23:59:59に固定することでバリデーションの結果が変わってしまう問題は頭を悩ませました。
この問題は23:59:59時点では有効なデータだとしても、実際に保存される時刻では不正なデータになり、データの不整合が発生するというものでした。
この問題の他にも調査を進めていく中で判明する複雑な課題がいくつもありました。

解決したい課題の不確実性が高い

そもそも解決したい課題の不確実性が高く、完璧な解決策を見つけるまでに時間がかかりました。
破壊的変更を伴うため、できる限りリスクを減らす方法としてフラグ切り替えによる切り戻しを可能にするという前提で進めていたため、切り戻した場合にデータ不整合を発生させないような考慮が必要でした。

データ移行で発生した問題と見積もりの甘さ

長期間にわたる調査フェーズもなんとか完了の目処が立ち始め、いよいよデータ移行の実装に取り掛かりました。しかし、データ移行について見積もりが甘く、さまざまな課題が発生しました。

段階的移行による複数回のリスクを伴う変更

データ移行は影響範囲が広い変更を段階的に行うため、リスクのある変更を何度もリリースする必要がありました。適用日を指定した参照を変更する段階、データ型を変更する段階、古いカラムを削除する段階など、各段階でリスクを伴う変更を複数回実行することが必要でした。

テーブル数やデータ量の影響

テーブル構造の変更が必要なテーブルが多く、複数テーブルまとめて変更したことによりサービス停止に近い状況になったり、排他制約の追加が終わらないという問題も発生しました。単純にデータ量が多いので、データベースへの負荷が高くなってしまい、ユーザーに影響を与えることもありました。

リリース後の問題とスケジュール調整

データ移行前にフィーチャーフラグ有効化による全体公開をリリースした後、不具合が見つかり切り戻しを実施しました。そのため、データ移行のスケジュールにも影響があり、遅れを取り戻そうと対応を急ぐあまり、ユーザーに影響を与える問題が数件発生しました。

検証時は短時間で完了していた排他制約の追加がオンラインで実行すると想定よりユーザー影響が大きいことがわかり、緊急メンテナンスの実施が必要になりました。さらに月末・月初の負荷の高い時期にデータパッチを実行したことによりサービス負荷を増加させてレスポンスの悪化が発生しました。このような問題を受けてデータ移行のスケジュールは延期することになりました。

また、BigQueryなどのデータ活用で適用日のデータを利用していたため、利用しているチームに対応期間を確認したところ2か月程度と回答があり、想定以上の期間が必要だったことも移行期間を見直す一因になりました。

履歴の適用日を日付化して得られた成果

様々な困難があったプロジェクトでしたが、先日無事にデータ移行が完了していくつかの成果が得られました。

まず、開発体験・生産性が改善されました。適用日を日付に統一することにより、課題となっていた秒未満の精度を考慮した実装が削除されてシンプルになりました。関連するデータの取得時に時間に差があることの考慮や、参照する時点を00:00:00か23:59:59のどちらにするのかといった迷いもなくなり、機能によって異なっていた仕様も統一することができました。

また、チャットサポートでも案内の負荷軽減やユーザーの認知負荷は大きく軽減されたという声をもらいました。

プロジェクトの途中ではなかなか成果が見られず、モチベーション維持が難しい時期もありましたが、このような成果が得られたことは非常に嬉しく感じました。

まとめ

適用日を日付化したプロジェクトを振り返ると、想定よりもはるかに困難で複雑なプロジェクトでした。検討開始から3年、実際に着手してから2年を要したこのプロジェクトから、多くの学びを得ることができました。

特に破壊的変更を伴うデータ構造の変更は、影響調査の困難さと見積もりの甘さが大きな課題となりました。調査フェーズでは新しい問題が次々と見つかり、解決策を見つけるまでに予想以上の時間を要しました。データ移行フェーズでは、データ量やデータ活用の影響を十分に考慮できておらず、複数の問題を発生させました。

このデータ移行プロジェクトから学んだのは、データ移行では物理的な制約やデータ活用の影響を事前に十分調査することの重要性です。また、問題発生後の焦りから正常な判断ができず、無理な選択を続けてしまったことも反省点でした。大規模なデータ移行では、時間に余裕を持ったスケジュールを組み、各段階でのリスクを慎重に評価することが重要だと学びました

影響範囲が広く、重要なデータに関わる問題であることを考えると、調査に時間をかけることは必要な投資でした。
しかし、不確実性が高いプロジェクトであっても期限や方針を明確にして進めることで、プロジェクトの進捗を測れるため、より効率的に進める方法を考える機会を作ることが大切であることがわかりました。

このような困難もありましたが、当初課題としていた開発体験の改善やユーザーの認知負荷軽減という成果を得ることができました。

SmartHRのような規模のサービスでは、このような基盤的な改善が開発効率やユーザー体験に大きな影響を与えます。今後も継続的に技術的課題の解消に取り組み、より良いプロダクトの提供につなげていきたいと思います。

We Are Hiring!

SmartHR では一緒に SmartHR を作りあげていく仲間を募集中です!

プロダクト基盤開発部では、他にも難易度の高いマルチプロダクト横断の課題解決をしています。

少しでも興味を持っていただけたら、カジュアル面談でざっくばらんにお話ししましょう!

hello-world.smarthr.co.jp


Source link

関連記事

コメント

この記事へのコメントはありません。