😎

BigQuery: 操作履歴テーブルからいい感じにデータを抽出する

2023/12/22に公開

やりたいこと

操作履歴を保存しているテーブルから特定のケースを抽出する際に、悩んだケースに当たったので記事にして供養します。

状況

このような何かしらの操作履歴を保存しているテーブル

テーブル名: hoge.fuga.history

カラム名 説明
id int 主キー
status int 操作成功:1, 失敗: 2
before_value int 変更前の値
after_value int 変更後の値
reflection_type int 自動操作:1, 手動操作:2
created_at datetime 作成日時
completed_at datetime 完了日時

があるとします。このテーブルには、操作の成否、変更前後の値、操作の種類(自動または手動)、作成日時、完了日時などが記録されています。

同一IDで同一日に、「最初に自動操作が成功したが、後にそれが間違いな事を検知し、手動でその操作を戻そうとしたけどなぜか失敗した...」というケースを抽出したいと思います。

lagを使ったクエリ

  • id, date(completed_at)毎にpartitionを切る
  • partition毎に、必要な要素のlagをとる
  • 手動操作で失敗した行について、lagが自動操作で成功でかつbefore_value, after_valueの値が逆になっているケースを抽出
with raw_history as (
    select
        *,
        row_number() over (
            partition by id, date(completed_at, 'Asia/Tokyo')
            order by completed_at asc
        ) as row_number
    from `hoge.fuga.history`
),

history as (
    select
        *,
        lag(status, 1) over (
            partition by id, date(completed_at, 'Asia/Tokyo')
            order by row_number asc
        ) as prev_status,
        lag(before_value, 1) over (
            partition by id, date(completed_at, 'Asia/Tokyo')
            order by row_number asc
        ) as prev_before_value,
        lag(after_value, 1) over (
            partition by id, date(completed_at, 'Asia/Tokyo')
            order by row_number asc
        ) as prev_after_value,
        lag(reflection_type, 1) over (
            partition by id, date(completed_at, 'Asia/Tokyo')
            order by row_number asc
        ) as prev_reflection_type
    from raw_history
)

select *
from history
where
    status = 2 /* 失敗 */
    and reflection_type = 2 /* 手動 */
    and prev_status = 1 /* 成功 */
    and prev_reflection_type = 1 /* 自動 */
    and before_value = prev_after_value
    and after_value = prev_before_value

これで目的のケースを抽出できました。
またlagを使わないやり方でも書けます。

操作のペアを作って絞り込むクエリ

with auto_success as (
    select
        *,
        date(completed_at, 'Asia/Tokyo') as completed_date
    from `hoge.fuga.history`
    where
        status = 1 /* 成功 */
        and reflection_type = 1 /* 自動 */
),

manual_failure as (
    select
        *,
        date(completed_at, 'Asia/Tokyo') as completed_date
    from `hoge.fuga.history`
    where
        status = 2 /* 失敗 */
        and reflection_type = 2 /* 自動 */
),

paired_operations as (
    select
        auto_success.id as auto_success_id,
        auto_success.completed_date as auto_success_completed_date,
        auto_success.completed_at as auto_success_completed_at,
        auto_success.before_value as auto_success_before_value,
        auto_success.after_value as auto_success_after_value,
        auto_success.reflection_type as auto_success_reflection_type,
        manual_failure.id as manual_failure_id
    from auto_success
    inner join manual_failure
        on
            auto_success.id = manual_failure.id
            and auto_success.completed_date = manual_failure.completed_date
)

select * from paired_operations
where
    auto_success_before_value = manual_failure_after_value
    and auto_success_after_value = manual_failure_before_value
    and auto_success_completed_at < manual_failure_completed_at

timestamp -> dateにして、dateをキーにした操作のペアを作ることでも目的のケースを抽出できました。

抜け落ちていたケース

どちらのクエリでも(自動,成功)->(手動、失敗)の抽出はできますが、このままでは、
(自動,成功)-> (自動,失敗) -> (手動、失敗)
のような間に操作が挟まっているケースの抽出ができません。
今回のケースではこれも抽出する必要があったので、以下のように操作のペアを作って絞り込むクエリをベースに手動成功のレコードを作ってから絞り込むことで対応しました。

対応したクエリ

with manual_success as (
    select
        *,
        date(completed_at, 'Asia/Tokyo') as completed_date
    from `hoge.fuga.history`
    where
        status = 1 /* 成功 */
        and reflection_type = 2 /* 手動 */
),

auto_success as (
    select
        *,
        date(completed_at, 'Asia/Tokyo') as completed_date
    from `hoge.fuga.history`
    where
        status = 1 /* 成功 */
        and reflection_type = 1 /* 自動 */
),

manual_failure as (
    select
        *,
        date(completed_at, 'Asia/Tokyo') as completed_date
    from `hoge.fuga.history`
    where
        status = 2 /* 失敗 */
        and reflection_type = 2 /* 自動 */
),

paired_operations as (
    select
        manual_success.id as manual_success_id,
        manual_success.completed_date as manual_success_completed_date,
        manual_success.completed_at as manual_success_completed_at,
        manual_success.before_value as manual_success_before_value,
        manual_success.after_value as manual_success_after_value,
        manual_success.reflection_type as manual_success_reflection_type,
        auto_success.id as auto_success_id,
        auto_success.completed_date as auto_success_completed_date,
        auto_success.completed_at as auto_success_completed_at,
        auto_success.before_value as auto_success_before_value,
        auto_success.after_value as auto_success_after_value,
        auto_success.reflection_type as auto_success_reflection_type,
        manual_failure.id as manual_failure_id
    from auto_success
    inner join manual_failure
        on
            auto_success.id = manual_failure.id
            and auto_success.completed_date = manual_failure.completed_date
    left join manual_success
        on
            auto_success.id = manual_success.id
            and auto_success.completed_date = manual_success.completed_date
)

select * from paired_operations
where
    auto_success_before_value = manual_failure_after_value
    and auto_success_after_value = manual_failure_before_value
    and auto_success_completed_at < manual_failure_completed_at
    /* ここで自動成功->手動成功->手動失敗の組み合わせを除外 */
    and manual_success_id is null

結論

  • 今回のケースでは、完了日の粒度で操作のペアを作って条件で絞るのが拡張性が高い形で書きやすかったです
  • prev_before_valueの命名はどうなんだろうと思いながら執筆しました

Discussion