GoogleカレンダーのTBL設計を検討してみる(繰り返し予約を考慮)

## はじめに
通常、カレンダー上のイベントはカレンダーごとに1対1で管理され、イベントに出席者や添付ファイルを紐づけるのは簡単です。しかし「繰り返しイベント」を定義すると、同じテンプレートから複数回（N回）の予定が生成されるため、カレンダーとイベントの関係は1対Nに拡張されます。このとき、ある特定のイベントだけを変更をする場合、データの持ちかたが非常に難しいです。今回、Googleカレンダーがどのようにこのような課題を解決しているのかを検討していきます。

## Googleの設計方針
[Googleの開発Blog](https://gsuite-developers.googleblog.com/2011/12/calendar-v3-best-practices-recurring.html)を元に理解していきます。

>A recurring event is a 'template' for a series of events that usually happen with some regularity, for example daily or bi-weekly. To create a recurring event, the client specifies the first instance of the event and includes one or more rules that describe when future events should occur. Google Calendar will then 'expand' the event into the specified occurrences. Individual events in a series may be changed, or even deleted. Such events become exceptions: they are still part of the series, but changes are preserved even if the recurring event itself is updated.

（日本語訳）
「繰り返しイベントとは、日次や隔週など、ある規則性に従って定期的に発生する一連の予定の“テンプレート”のことです。繰り返しイベントを作成する際には、最初のインスタンス（開始日・開始時刻）を指定し、将来の予定がいつ発生するかを示すひとつ以上のルール（RRULEなど）を付与します。Google カレンダーはそのテンプレートをもとに、指定された回数や期間にわたって予定を「展開（エクスパンド）」します。
また、一連の中の個々の予定は変更したり削除したりすることも可能で、そうした変更は「例外（オーバーライド）」として扱われます。例外となった予定はシリーズの一部であり続けますが、親の繰り返し設定が後から更新されても、その変更内容は保持されます。」

→カレンダーに紐づくイベントは実態ではなく、**テンプレート**として考える。一連の中の個々のイベントは例外のイベントとして管理する。イベントを実際に利用する時は、テンプレートに従いながら例外イベントで上書きを実施し扱う。

https://gsuite-developers.googleblog.com/2011/12/calendar-v3-best-practices-recurring.html

:::note info
なんとなく、設計の方針はわかってきました。ただ、実際にどのように表現すべきなのかというのがいまいちピンとこないので、TBL設計をして具体化していきます。
:::

## TBL設計
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/463f1962-79b0-4193-94ed-08ad556f5b8d.png)

<!--
// ==== ENUM 定義 (RFC 5545 準拠) ==== 

// 繰り返し頻度 (FREQ)
Enum Frequency {
  DAILY     // 毎日
  WEEKLY    // 毎週
  MONTHLY   // 毎月
  YEARLY    // 毎年
}
// 出典: RFC 5545 Section 3.3.10 citehttps://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10

// 曜日の指定 (BYDAY)
Enum WeekDay {
  SU  // 日曜日
  MO  // 月曜日
  TU  // 火曜日
  WE  // 水曜日
  TH  // 木曜日
  FR  // 金曜日
  SA  // 土曜日
}
// 出典: RFC 5545 Section 3.3.10 citehttps://datatracker.ietf.org/doc/html/rfc5545#section-3.3.10

// 参加者のロール
Enum EventRole {
  required   // 必須参加者
  optional   // 任意参加者
}

// 出欠ステータス (RSVP)
Enum ResponseStatus {
  needsAction  // 未回答（招待済み）
  accepted     // 参加
  declined     // 辞退
  tentative    // 仮参加
}
// RFC 5545 の RSVP 定義では ATTENDEE の PARTSTAT に対応
// 出典: RFC 5545 Section 3.2.16 citehttps://datatracker.ietf.org/doc/html/rfc5545#section-3.2.16

// ==== テーブル定義 ==== 

Table calendars {
  id bigint [pk]                        // カレンダーID (主キー)
  name text                             // カレンダー名
  owner_id bigint [ref: > users.id]     // 所有者ユーザーID (外部キー)
}

Table users {
  id bigint [pk]                        // ユーザーID (主キー)
  name text                             // ユーザー名
  email text                            // メールアドレス
}

Table events {
  id bigint [pk]                        // イベントID (主キー)
  calendar_id bigint [ref: > calendars.id]  // 所属カレンダーID
  title text                            // イベントタイトル
  start_at timestamptz                  // イベント開始日時 (親 / 単発)
  end_at timestamptz                    // イベント終了日時 (親 / 単発)
  is_recurring boolean                  // 繰り返しかどうか
  recurrence_end timestamptz            // RRULE UNTIL相当 (null可)
}

Table event_rrules {
  id bigint [pk]                        // RRULEレコードID
  event_id bigint [ref: > events.id]    // 紐づくイベントID
  freq Frequency                        // 繰り返し頻度 (RRULE:FREQ)
  interval integer                      // インターバル (RRULE:INTERVAL)
  count integer                         // 繰り返し回数 (RRULE:COUNT)
  until timestamptz                     // 繰り返し終了日時 (RRULE:UNTIL)
  byday WeekDay[]                       // 曜日リスト (RRULE:BYDAY)
  bymonth integer[]                     // 月リスト (RRULE:BYMONTH)
  bymonthday integer[]                  // 日リスト (RRULE:BYMONTHDAY)
}

Table event_attendees {
  id bigint [pk]                        // 出席者レコードID
  event_id bigint [ref: > events.id]    // 紐づくイベントID (マスター)
  user_id bigint [ref: > users.id]      // ユーザーID
  role EventRole                        // required/optional
  response ResponseStatus               // 出欠ステータス
}

Table event_overrides {
  id bigint [pk]                        // オーバーライドレコードID
  event_id bigint [ref: > events.id]    // 紐づく親イベントID
  original_start_at timestamptz         // 元の開始日時 (RECURRENCE-ID)
  override_start_at timestamptz         // 上書き開始日時 (例外)
  override_end_at timestamptz           // 上書き終了日時 (例外)
  override_title text                   // タイトル上書き (例外)
}

Table event_overrides_attendees {
  id bigint [pk]                        // 出席者オーバーライドレコードID
  event_overrides_id bigint [ref: > event_overrides.id]  // 紐づく例外イベントID
  user_id bigint [ref: > users.id]      // ユーザーID
  role EventRole                        // required/optional
  response ResponseStatus               // 出欠ステータス
}
-->

```sql:ddl
-- ==============================================
-- DROP TABLES & ENUM TYPES (依存関係の逆順)
-- ==============================================

DROP TABLE IF EXISTS "event_overrides_attendees" CASCADE;
DROP TABLE IF EXISTS "event_overrides"            CASCADE;
DROP TABLE IF EXISTS "event_attendees"            CASCADE;
DROP TABLE IF EXISTS "event_rrules"               CASCADE;
DROP TABLE IF EXISTS "events"                     CASCADE;
DROP TABLE IF EXISTS "calendars"                  CASCADE;
DROP TABLE IF EXISTS "users"                      CASCADE;

DROP TYPE IF EXISTS "ResponseStatus" CASCADE;
DROP TYPE IF EXISTS "EventRole"      CASCADE;
DROP TYPE IF EXISTS "WeekDay"        CASCADE;
DROP TYPE IF EXISTS "Frequency"      CASCADE;


-- ==============================================
-- CREATE ENUM TYPES (RFC 5545 準拠)
-- ==============================================

-- 繰り返し頻度 (RFC 5545 FREQ)
CREATE TYPE "Frequency" AS ENUM (
  'SECONDLY',
  'MINUTELY',
  'HOURLY',
  'DAILY',
  'WEEKLY',
  'MONTHLY',
  'YEARLY'
);

-- 曜日の指定 (RFC 5545 BYDAY)
CREATE TYPE "WeekDay" AS ENUM (
  'SU',
  'MO',
  'TU',
  'WE',
  'TH',
  'FR',
  'SA'
);

-- 参加者のロール
CREATE TYPE "EventRole" AS ENUM (
  'required',   -- 必須参加者
  'optional'    -- 任意参加者
);

-- 出欠ステータス (RFC 5545 ATTENDEE PARTSTAT)
CREATE TYPE "ResponseStatus" AS ENUM (
  'needsAction',  -- 未回答（招待済み）
  'accepted',     -- 参加
  'declined',     -- 辞退
  'tentative'     -- 仮参加
);


-- ==============================================
-- CREATE TABLES
-- ==============================================

-- ユーザー情報
CREATE TABLE "users" (
  "id"    bigint      PRIMARY KEY,  -- ユーザーID
  "name"  text        NOT NULL,     -- ユーザー名
  "email" text        NOT NULL      -- メールアドレス（一意性制約は用途に応じて追加）
);

-- カレンダー
CREATE TABLE "calendars" (
  "id"       bigint PRIMARY KEY,             -- カレンダーID
  "name"     text   NOT NULL,                -- カレンダー名
  "owner_id" bigint NOT NULL 
    REFERENCES "users"("id") ON DELETE CASCADE  -- カレンダー所有者（ユーザーID）
);

-- イベント（単発 or 繰り返しマスター）
CREATE TABLE "events" (
  "id"             bigint      PRIMARY KEY,              -- イベントID
  "calendar_id"    bigint      NOT NULL 
    REFERENCES "calendars"("id") ON DELETE CASCADE,      -- 所属カレンダーID
  "title"          text        NOT NULL,                 -- イベントタイトル
  "start_at"       timestamptz NOT NULL,                 -- 開始日時（単発 or マスター初回）
  "end_at"         timestamptz NOT NULL,                 -- 終了日時（単発 or マスター初回）
  "is_recurring"   boolean     NOT NULL DEFAULT false,   -- 繰り返しフラグ
  "recurrence_end" timestamptz                              -- 繰り返し終了日時 (RRULE UNTIL)
);

-- 繰り返しルール詳細
CREATE TABLE "event_rrules" (
  "id"         bigint      PRIMARY KEY,             -- RRULEレコードID
  "event_id"   bigint      NOT NULL 
    REFERENCES "events"("id") ON DELETE CASCADE,   -- 紐づくイベントID
  "freq"       "Frequency" NOT NULL,               -- 繰り返し頻度 (FREQ)
  "interval"   integer     NOT NULL DEFAULT 1,     -- インターバル (INTERVAL)
  "count"      integer,                            -- 最大回数 (COUNT)
  "until"      timestamptz,                        -- 終了日時 (UNTIL)
  "byday"      "WeekDay"[],                        -- 曜日リスト (BYDAY)
  "bymonth"    integer[],                          -- 月リスト (BYMONTH)
  "bymonthday" integer[]                           -- 日リスト (BYMONTHDAY)
);

-- 共通出席者（マスターイベント用）
CREATE TABLE "event_attendees" (
  "id"       bigint            PRIMARY KEY,             -- 出席者レコードID
  "event_id" bigint            NOT NULL 
    REFERENCES "events"("id") ON DELETE CASCADE,       -- 紐づくイベントID
  "user_id"  bigint            NOT NULL 
    REFERENCES "users"("id") ON DELETE CASCADE,        -- ユーザーID
  "role"     "EventRole"       NOT NULL,                -- 参加の必須／任意
  "response" "ResponseStatus" NOT NULL DEFAULT 'needsAction'  -- 出欠ステータス
);

-- 例外（Override）インスタンス
CREATE TABLE "event_overrides" (
  "id"                 bigint      PRIMARY KEY,            -- OverrideレコードID
  "event_id"           bigint      NOT NULL 
    REFERENCES "events"("id") ON DELETE CASCADE,          -- 親イベントID
  "original_start_at"  timestamptz NOT NULL,               -- 元のインスタンス開始日時 (RECURRENCE-ID)
  "override_start_at"  timestamptz,                        -- 変更後の開始日時
  "override_end_at"    timestamptz,                        -- 変更後の終了日時
  "override_title"     text                                  -- 変更後のタイトル
);

-- 例外イベント用 出席者Override
CREATE TABLE "event_overrides_attendees" (
  "id"                   bigint            PRIMARY KEY,     -- Override出席者レコードID
  "event_overrides_id"   bigint            NOT NULL 
    REFERENCES "event_overrides"("id") ON DELETE CASCADE,   -- 紐づくOverrideイベントID
  "user_id"              bigint            NOT NULL 
    REFERENCES "users"("id") ON DELETE CASCADE,             -- ユーザーID
  "role"                 "EventRole"       NOT NULL,        -- 参加の必須／任意
  "response"             "ResponseStatus"  NOT NULL         -- 出欠ステータス
);
```


## ユースケース別のデータ作成/更新

<details><summary>seedデータの導入</summary>

```sql:seedデータ
-- ==============================================
-- SEED DATA
-- ==============================================

-- Users
INSERT INTO "users" ("id", "name",    "email") VALUES
  (1, 'Alice', 'alice@example.com'),
  (2, 'Bob',   'bob@example.com'),
  (3, 'Carol', 'carol@example.com'),
  (4, 'Dave',  'dave@example.com');

-- Calendars
INSERT INTO "calendars" ("id", "name",            "owner_id") VALUES
  (1, 'Alice Calendar',                      1);

-- Events
INSERT INTO "events" (
  "id","calendar_id","title","start_at","end_at","is_recurring","recurrence_end"
) VALUES
  -- ① 繰り返しイベント：毎週月曜10:00–10:30、6月30日まで
  (1, 1, 'Weekly Meeting',
     '2025-06-09T10:00:00+09', '2025-06-09T10:30:00+09',
     true,  '2025-06-30T10:30:00+09'),
  -- ② 単発イベント
  (2, 1, 'One-time Event',
     '2025-06-10T14:00:00+09', '2025-06-10T15:00:00+09',
     false, NULL);

-- Recurrence Rules
INSERT INTO "event_rrules" (
  "id","event_id","freq","interval","count","until","byday","bymonth","bymonthday"
) VALUES
  (
    1,
    1,
    'WEEKLY',
    1,
    NULL,
    '2025-06-30T00:00:00+09'::timestamptz,
    ARRAY['MO']::"WeekDay"[],
    NULL,
    NULL
  );

-- Master Attendees (共通出席者)
INSERT INTO "event_attendees" (
  "id","event_id","user_id","role","response"
) VALUES
  (1, 1, 1, 'required',  'accepted'),      -- Alice
  (2, 1, 2, 'required',  'needsAction'),   -- Bob
  (3, 1, 3, 'optional',  'needsAction'),   -- Carol
  (4, 2, 1, 'required',  'accepted');      -- Alice for the one-time event

-- Overrides (例外インスタンス)
INSERT INTO "event_overrides" (
  "id","event_id","original_start_at","override_start_at","override_end_at","override_title"
) VALUES
  -- 6/23の回だけ開始時間を11:00に変更
  (1, 1,
   '2025-06-23T10:00:00+09',  -- 元の開始日時 (RECURRENCE-ID)
   '2025-06-23T11:00:00+09',  -- 上書き開始日時
   '2025-06-23T11:30:00+09',  -- 上書き終了日時
   'Weekly Meeting (Time Changed)');

-- Override Attendees (例外出席者追加)
INSERT INTO "event_overrides_attendees" (
  "id","event_overrides_id","user_id","role","response"
) VALUES
  -- 6/23の回だけDaveを必須参加で追加
  (1, 1, 4, 'required', 'accepted');
```

</details>

### 単発のイベントを作成する
まずは繰り返しのイベントでなく、単発のイベントをどのようにデータで表現するか検討します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/b0f17ef2-dfab-4abc-a7a6-964b0d1f2e2d.png)


ユースケース例
・イベント名：Project Kickoff
・日時：2025-07-01 午前10:00～11:00
・カレンダーID：1
・出席者：Alice（必須）、Bob（任意）

```sql:
-- 1) 単発イベントを登録
INSERT INTO "events" (
  "id", "calendar_id", "title", "start_at",           "end_at",             "is_recurring", "recurrence_end"
) VALUES (
  10,    1,             'Project Kickoff',
  '2025-07-01T10:00:00+09'::timestamptz,
  '2025-07-01T11:00:00+09'::timestamptz,
  false, NULL
);

-- 2) そのイベントの出席者を登録
INSERT INTO "event_attendees" (
  "id", "event_id", "user_id", "role",     "response"
) VALUES
  -- Alice は必須参加として招待済み
  (100,    10,        1,        'required','needsAction'),
  -- Bob は任意参加として招待済み
  (101,    10,        2,        'optional','needsAction');
```



```sql:実行結果
calendar=# select * from event_attendees as ea inner join events as e on ea.event_id = e.id where e.id = 10; 
 id  | event_id | user_id |   role   |  response   | id | calendar_id |      title      |        start_at        |         end_at         | is_recurring | recurrence_end 
-----+----------+---------+----------+-------------+----+-------------+-----------------+------------------------+------------------------+--------------+----------------
 100 |       10 |       1 | required | needsAction | 10 |           1 | Project Kickoff | 2025-07-01 01:00:00+00 | 2025-07-01 02:00:00+00 | f            | 
 101 |       10 |       2 | optional | needsAction | 10 |           1 | Project Kickoff | 2025-07-01 01:00:00+00 | 2025-07-01 02:00:00+00 | f            | 
(2 rows)
```
問題なく取得できていますね。では次に、繰り返しをやります。

### 繰り返しイベントを作成する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/a5c603fb-5d37-4199-a431-400b0f0b61fc.png)
Googleカレンダーでいう上記のようなユースケースです。

以下は「毎週月・水曜日 10:00～11:00 にチーム定例ミーティングを、7月末まで繰り返し設定する」ユースケースの例です。
出席者は Alice（必須）、Bob（必須）、Carol（任意）とします。

```sql:
calendar=# -- 1) 繰り返しイベントの登録
INSERT INTO "events" (
  "id", "calendar_id", "title",            "start_at",               "end_at",                 "is_recurring", "recurrence_end"
) VALUES (
  20,   1,             'Team Sync Meeting',
  '2025-07-07T10:00:00+09'::timestamptz,  -- 初回は7/7（月）
  '2025-07-07T11:00:00+09'::timestamptz,
  true,  '2025-07-31T11:00:00+09'::timestamptz  -- 繰り返し終了
);
INSERT 0 1
calendar=# -- 2) 繰り返しルールの詳細
INSERT INTO "event_rrules" (
  "id",  "event_id", "freq",     "interval", "count", "until",                   "byday",                                 "bymonth", "bymonthday"
) VALUES (
  10,    20,         'WEEKLY',   1,          NULL,    '2025-07-31T00:00:00+09'::timestamptz,
  ARRAY['MO','WE']::"WeekDay"[],  NULL,      NULL
);
INSERT 0 1
calendar=# -- 3) マスター出席者の登録
INSERT INTO "event_attendees" (
  "id",  "event_id", "user_id", "role",     "response"
) VALUES
  (200,   20,         1,         'required','needsAction'),  -- Alice
  (201,   20,         2,         'required','needsAction'),  -- Bob
  (202,   20,         3,         'optional','needsAction');  -- Carol
INSERT 0 3
```

```sql:実行結果
calendar=# select * from event_attendees as ea inner join events as e on ea.event_id = e.id where e.id = 20; 
 id  | event_id | user_id |   role   |  response   | id | calendar_id |       title       |        start_at        |         end_at         | is_recurring |     recurrence_end     
-----+----------+---------+----------+-------------+----+-------------+-------------------+------------------------+------------------------+--------------+------------------------
 200 |       20 |       1 | required | needsAction | 20 |           1 | Team Sync Meeting | 2025-07-07 01:00:00+00 | 2025-07-07 02:00:00+00 | t            | 2025-07-31 02:00:00+00
 201 |       20 |       2 | required | needsAction | 20 |           1 | Team Sync Meeting | 2025-07-07 01:00:00+00 | 2025-07-07 02:00:00+00 | t            | 2025-07-31 02:00:00+00
 202 |       20 |       3 | optional | needsAction | 20 |           1 | Team Sync Meeting | 2025-07-07 01:00:00+00 | 2025-07-07 02:00:00+00 | t            | 2025-07-31 02:00:00+00
(3 rows)

calendar=# select * from event_rrules where id = 10;
 id | event_id |  freq  | interval | count |         until          |  byday  | bymonth | bymonthday 
----+----------+--------+----------+-------+------------------------+---------+---------+------------
 10 |       20 | WEEKLY |        1 |       | 2025-07-30 15:00:00+00 | {MO,WE} |         | 
(1 row)
```
→これで表現できていますね！

### 繰り返し予約の中に例外処理を追加する
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/25b422aa-0d06-469d-b356-0175666f8bf2.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/599049/ee3625a3-9ec9-47e9-ac79-91fa91a5fd69.png)


上記の画像のように、繰り返し予約の中で1つ例外を作ります。


ユースケース例
・元の「Team Sync Meeting」は毎週月・水曜 10:00–11:00 に行う
・2025-07-14（次の月曜）だけ、時間を 11:00–12:00 に変更し、かつ Dave（user_id=4）を必須参加で追加、Carol（user_id=3）は欠席にする


```sql:
calendar=# -- 1) 例外インスタンス（Override）の登録
INSERT INTO "event_overrides" (
  "id",
  "event_id",
  "original_start_at",
  "override_start_at",
  "override_end_at",
  "override_title"
) VALUES (
  30,                                     -- OverrideレコードID
  20,                                     -- 親イベントID (Team Sync Meeting)
  '2025-07-14T10:00:00+09'::timestamptz,  -- 元の開始日時 (RECURRENCE-ID)
  '2025-07-14T11:00:00+09'::timestamptz,  -- 新しい開始日時
  '2025-07-14T12:00:00+09'::timestamptz,  -- 新しい終了日時
  'Team Sync Meeting (Time & Attendees Updated)'  -- タイトルOverride
);
INSERT 0 1
calendar=# -- 2) 例外イベント用 出席者Override の登録
INSERT INTO "event_overrides_attendees" (
  "id",
  "event_overrides_id",
  "user_id",
  "role",
  "response"
) VALUES
  -- Dave を必須参加で追加
  (300, 30, 4, 'required', 'needsAction'),
  -- Carol はこの回だけ辞退
  (301, 30, 3, 'optional', 'declined');
INSERT 0 2
```
→データを追加しました。実際に7月14日のイベントの内容をみてみましょう。

```sql:
calendar=# WITH instance AS (
  SELECT
    e.id                                    AS event_id,
    COALESCE(o.override_title, e.title)    AS summary,
    COALESCE(o.override_start_at, '2025-07-14T10:00:00+09'::timestamptz) AS start_at,
    COALESCE(o.override_end_at,   '2025-07-14T11:00:00+09'::timestamptz) AS end_at,
    o.id                                    AS override_id
  FROM events e
  LEFT JOIN event_overrides o
    ON o.event_id = e.id
    AND o.original_start_at = '2025-07-14T10:00:00+09'::timestamptz
  WHERE e.id = 20
),
master_attendees AS (
  SELECT user_id, role, response
  FROM event_attendees
  WHERE event_id = (SELECT event_id FROM instance)
),
override_attendees AS (
  SELECT user_id, role, response
  FROM event_overrides_attendees
  WHERE event_overrides_id = (SELECT override_id FROM instance)
),
all_attendees AS (
  -- Override の行がマスターを上書きできるよう、UNION ALL 後に
  -- user_id でグループ化し、最新行（Override側）を取る
  SELECT user_id,
         MAX(role)     AS role,
         MAX(response) AS response
  FROM (
    SELECT * FROM master_attendees
    UNION ALL
    SELECT * FROM override_attendees
  ) t
  GROUP BY user_id
)
SELECT
  i.summary,
  i.start_at,
ORDER BY u.name;er_idendees already filtered to this event
                   summary                    |        start_at        |         end_at         | attendee_name | response_status 
----------------------------------------------+------------------------+------------------------+---------------+-----------------
 Team Sync Meeting (Time & Attendees Updated) | 2025-07-14 02:00:00+00 | 2025-07-14 03:00:00+00 | Alice         | needsAction
 Team Sync Meeting (Time & Attendees Updated) | 2025-07-14 02:00:00+00 | 2025-07-14 03:00:00+00 | Bob           | needsAction
 Team Sync Meeting (Time & Attendees Updated) | 2025-07-14 02:00:00+00 | 2025-07-14 03:00:00+00 | Carol         | declined
 Team Sync Meeting (Time & Attendees Updated) | 2025-07-14 02:00:00+00 | 2025-07-14 03:00:00+00 | Dave          | needsAction
(4 rows)
```
→取得できましたね。ただ、異常に複雑なSQLができました。更新時にではなく取得にコストを払う方針のようです。
取得はできても色々難しそうですね。。。😅　テンプレートと例外をぶつけるkeyがユニークキーでなく、時刻なのでメンテナンスが異常に難しそう。。。課題を見切るのも難しいし、それをどういうふうに管理していくのか。運用をする上でかなり大きな課題になりそうですね。