履歴セット(sys_history_set)はActivity表示のキャッシュ
ServiceNow で Incident のフォームを開くと、下のほうに Activity(History / 変更履歴)が見えます。「いつ、誰が、どの項目を、何から何に変えたか」が時系列で並ぶ表示です。あの履歴は、レコードを開くたびに5万件規模の監査ログ(sys_audit)を毎回かき集めて作っているのでしょうか。実は違います。表示を速くするために、そのレコード専用の「履歴セット」に集約・キャッシュしてから見せています。
この記事は、ServiceNow の画面操作には慣れたが、Activity 表示の裏側のテーブルはこれから確認したい管理者・開発者を想定しています。PDI(Zurich / OOTB)で、履歴の集約を担う2つのテーブルを実際に開きます。sys_history_set(Record History) と sys_history_line(History) です。1レコード=1履歴セットであること、各変更行の中身を確認します。恒久記録の監査ログ sys_audit との役割分担もあわせて見ます。
この記事で分かること
Activity(変更履歴)の表示が sys_history_set / sys_history_line から組み立てられること
sys_history_set は1レコード=1つの対象レコードの履歴セット(table / id / updates / line_table)であること
sys_history_line が各変更行(field / old → new / update_time / user)であること
履歴セットが sys_audit を都度集計するのではなく、集約・キャッシュした表示用の層であること
監査ログ(恒久・追記専用)と履歴セット(表示用・更新され得る)の役割の違い
PDI 実機での件数(set 8件 / line 269件)と table 別の分布
この記事の内容は ServiceNow の PDI(Personal Developer Instance)Zurich(ズーリッヒ)リリースの OOTB(初期設定)で確認した個人メモです。本番環境やカスタマイズ済み環境では、履歴セットの件数・対象テーブル・分布が異なります。
履歴セット(History set)とは
履歴セットとは、あるレコードの変更履歴(Activity)を画面に表示するために、対象レコード単位でまとめた表示用のデータです。Incident フォームの下に並ぶ Activity は、監査ログ sys_audit を毎回そのまま読むのではなく、この履歴セットを経由して描かれます。
下の画面は、PDI(Zurich / OOTB)の sys_history_set 一覧です。Incident や Hardware(資産)など、履歴を開かれたレコードごとに1行ずつ履歴セットが作られ、対象レコード(id)・テーブル(table)・更新回数(updates)が並びます。

PDI(Zurich / OOTB)では、sys_history_set は 8件、sys_history_line は 269件しかありませんでした。監査ログ sys_audit は約5万件です。それに対してこれほど少ない理由は、履歴セットが最初から全レコード分作られているわけではないからです。誰かがそのレコードの履歴(Activity)を開いたときに、必要な分だけ生成・キャッシュされます。恒久的な記録の本体は sys_audit 側にあります。
履歴が画面に出るまでの流れ
履歴がフォームに表示されるまでには、「記録」と「集約」の2段階があります。まず業務レコードが更新されると、その変更は監査ログ(sys_audit)と追記ログ(sys_journal_field)に恒久記録されます。次に、誰かがそのレコードの履歴を開いた瞬間に、それらが集約されて sys_history_set / sys_history_line が組み立てられ、Activity として表示されます。

監査ログ=恒久記録の元データ、履歴セット=表示用の集約キャッシュという二層構造になっています。だから「誰がいつ何を変えたか」を厳密に調べるなら sys_audit を見ます。「画面の履歴表示がどう作られているか」をデバッグするなら sys_history_set を見る、と使い分けます。テーブルの読み方は「ServiceNow のテーブルとフィールドを実機で確認する」が参考になります。
sys_history_set:1レコード=1履歴セット
履歴セットの本体が sys_history_set(画面ラベルは Record History)です。今回の PDI(Zurich / OOTB)では、super_class(継承元)が空の独立したルート基底テーブルでした。task などは継承していません。
1レコードが、ひとつの対象レコードの履歴セット全体を表します。主なフィールドは次のとおりです。

中心になるのは、対象テーブルを表す table、対象レコードを表す id("Incident: INC0000060" のような表示)の2つです。ほかに、更新回数の updates、履歴行を実際に格納する物理テーブル名 line_table(sys_history_line0000 / 0001 …)があります。実際の画面では、INC0000060 の履歴セットをフォームで開いています。

ここで sys_audit との重要な違いがあります。sys_audit は追記専用(sys_updated_on を持たない)でした。一方で sys_history_set は sys_updated_on / sys_updated_by を持ちます。さらに refresh フラグや last_update_recorded・load_time まで備えています。つまり履歴セットは、一度書いたら終わりの記録ではなく、新しい変更が起きれば更新・再生成されるキャッシュなのです。
PDI 実機で履歴セットを table 別に集計すると、分布は次のようになっていました。

incident が5件、alm_hardware・sc_task・sysapproval_approver が各1件で、合計8件です。Incident だけでなく、資産(Hardware)や承認(Approval)など、履歴を開かれた業務レコードの種類だけ履歴セットが作られているのが分かります。更新回数(updates)が多いのは INC0000053 の32回、Hardware P1000503 の21回などでした。
sys_history_line:Activity の1行=1変更
履歴セットの中身が sys_history_line(画面ラベルは History)です。これも super_class が空のルート基底テーブルで、Activity の1行=1つのフィールド変更に対応します。主なフィールドは次のとおりです。

各行は、所属する履歴セットを set(→ sys_history_set への参照)で指します。持っているフィールドは次のとおりです。
field:変更されたフィールド
old → new:旧値と新値
update:何回目の更新か
update_time / user:変更日時と変更者
audit_sysid:元になった sys_audit レコードの sys_id
journal_sysid:追記なら sys_journal_field の sys_id
履歴行は、この参照によって監査ログ・追記ログから組み立てられていることが確認できます。

次の画面は、INC0000060 の履歴セットに属する履歴行を、状態系のフィールド(氏名を含まないもの)に絞って表示したものです。state「Resolved → Closed」、incident_state「In Progress → Resolved」のように、フィールド単位の変更が並びます。update(更新番号)つきで記録されます。

この INC0000060 の履歴セットには、履歴行が 89件ありました(うち sys_audit 由来の変更行が59件、残りは初期値スナップショット)。1回の更新で複数のフィールドが同時に変わるため、履歴行の数は updates(更新回数=15)より多くなります。なお、resolved_by などの行には OOTB デモの担当者名が入りますが、これは架空のサンプルデータです。
この履歴セット/履歴行が画面でどう見えるのかが、おなじみの Activity(History)です。下は同じ INC0000060 のフォームで、履歴行がそのまま時系列の変更履歴として表示されています。Incident テーブルの構造は別記事で扱っています。「ServiceNow の Incident テーブルを実機で確認する」もあわせてどうぞ。

監査ログ(元データ)と履歴セット(表示用集約)の違い
ここまでで2つの層が出てきました。監査ログ(sys_audit / sys_journal_field)が変更の恒久記録(元データ)です。履歴セット(sys_history_set / sys_history_line)が画面表示用の集約キャッシュです。

sys_audit / sys_journal_field … 監査が有効なテーブルの更新時に自動で「1変更=1レコード」を追記します。追記専用(sys_updated_on を持たない)で、約5万件規模まで積み上がっていました。これが消えない証跡の本体です。
sys_history_set / sys_history_line … 履歴を開いたときに、上記から必要分を集約して作る表示用のまとめです。set 8件 / line 269件と少数で、sys_updated_on を持つ=更新され得るキャッシュです。
両者は、フィールド定義の数にも差が出ます。実機で辞書(sys_dictionary)を確認すると、sys_history_set は固有フィールド19、sys_history_line は24でした。

履歴行(sys_history_line)側は、old / new に加えて audit_sysid・journal_sysid・user・update_time などを持ちます。表示と追跡に必要な項目が並んでいます。

監査ログは「変更の事実を残す」、履歴セットは「それを速く見せる」 ための層です。履歴が表示されないときは「そのテーブルで監査(audit)が有効か」をまず疑います。表示の作られ方を追うときは履歴セットを見る、という順番になります。監査の有効/無効や sys_audit の中身は、対になる記事で扱っています。ServiceNow の監査と履歴を実機で読み解く をご覧ください。
実機で確認するときに見るべきURL
最後に、今回見た画面を自分のインスタンスで開くためのURLをまとめます(<YourInstance> は置き換えてください)。
# 履歴セットの一覧(新しい順)
https://<YourInstance>/sys_history_set_list.do?sysparm_query=ORDERBYDESCsys_created_on
# ある履歴セットの履歴行(set の sys_id で絞る)
https://<YourInstance>/sys_history_line_list.do?sysparm_query=set=<履歴セットのsys_id>^ORDERBYDESCupdate
# sys_history_set のフィールド定義
https://<YourInstance>/sys_dictionary_list.do?sysparm_query=name=sys_history_set^elementISNOTEMPTY
# sys_history_line のフィールド定義
https://<YourInstance>/sys_dictionary_list.do?sysparm_query=name=sys_history_line^elementISNOTEMPTYまとめ
フォームの Activity(変更履歴)は sys_history_set / sys_history_line から組み立てられる。PDI では set 8件 / line 269件と少数。
sys_history_set は1レコード=1対象レコードの履歴セット(table / id / updates / line_table)。sys_history_line が各変更行(field / old → new / update_time / user)。
履歴セットは sys_audit を都度集計するのではなく、履歴を開いたときに集約・キャッシュした表示用の層。sys_updated_on や refresh を持ち、更新され得る。
履歴行は audit_sysid / journal_sysid で元の監査ログ・追記ログを参照する。INC0000060 の履歴セットは履歴行89件(updates=15)だった。
監査ログ=恒久記録の元データ、履歴セット=表示用の集約キャッシュ。事実を追うなら sys_audit、表示を追うなら sys_history_set。
履歴セットが分かると、監査と履歴(sys_audit)・システムログ(syslog)・テーブルと辞書(sys_db_object / sys_dictionary)が、「変更が記録される → 集約される → 画面に出る」という流れでつながって見えてきます。
関連記事
ServiceNowのデータはどこに溜まっていつ消える? 添付・履歴・ログ・アーカイブを実機で確認 — 履歴表示の元になる監査ログや添付・ログを含めた、データ全体の保管場所と消えるタイミングが分かる