ミツモア Tech blog

「ミツモア」を運営する株式会社ミツモアの技術ブログです

AirbyteとdbtでNotionデータを分析基盤に!data pipeline構築例

※こちらはミツモアAdvent Calendar 2024の3日目の記事です

お久しぶりです。ミツモア データマネージャの古田 @crazysrot です

皆さんはNotionのデータをexportした上で分析に使うことはあるでしょうか?自動で更新する仕組みを作りたくなったことはあるでしょうか?今回はそのようなケースのお話になります

なぜNotionを使って分析する必要があったのか?

Notionには今年Notion Chartがリリースされ、可視化するのが便利になりましたので基本的には必要ではないかもしれません。

しかしさまざまなケースでそれだけでは物足りない時もあるかもしれません

ミツモアでは次のケースで必要になりました

PoC施策/PMF検証をするために開発をするのではなく、Notionで施策を管理しよう!

PoCなので既存のシステムとの接続の開発を後回しにして検証を最速で進める意思決定をしました。ただし、細かい正確な可視化をするには既存のシステムとのデータのrelationを考慮する必要がありました。そのため、Notionと既存システムのデータを紐づける作業を行う必要がありました。また、NotionChartがとても重たいといった理由や一部exportしてスプレッドシート上で可視化をする必要があったためというのも理由としてはありました

今回、※条件付きではあるもののNotionデータ加工pipeline自体を仕組み化できたので、今後はこの仕組みを用いて可視化をするのが容易になったため、新規事業においてバックエンド側の処理の仕組みづくりの優先度を下げ、NotionDatabaseで人力で運用して検証していくOpsがしやすくなったのではないかと感じました

※Notion databaseのプロパティでデータベースとして情報を管理しているケースの対応例

Notionデータについての説明

NotionのDatabase構成は至ってシンプルな構成をしています

公式ページ にあるように、Notionの全ての構成はBlockで構成されており、それを管理しやすいようにusers, databasesがそれぞれblocks, pagesと紐付けられています

どうやってデータ連携をしたのか(Notion→BigQuery)

A . Airbyteを用いました。

ミツモアではELT toolとしてStitchに加えてAirbyteも利用しています。Notion IntegrationはAirbyteにしかなかったためAirbyteを利用しました。また、NotionDBは人力で加工することがほとんどであるため、社員数の規模から生産活動によるNotionデータ量の爆発が起きないことを見越して従量課金制のSaaSを使う選択をしました。

連携方法

  1. Airbyteアカウントを作ってからDestinationの設定まで完了している前提として進めさせていただきます
  2. Source設定
    1. New Sourceをクリックし、Notionを選択

    2. 認証を実施する

      1. 今回はOAuth2.0で行いました。この方式の場合は、認証を実施するユーザがNotionのデータを取得したいページなどのフルアクセス権限を持っている必要があります。アクセス承認を行い、取得したいデータを選択してアクセスを許可してください
    3. Connectionを作成する

      1. Connection(source - destination)の設定をします
        1. 弊社のデータ基盤はBigQueryのため、DestinationにはBigQueryを選択しました。詳しくはこちらをご覧ください

        2. 連携するNotionのデータを選択します。欲しいものを選んでください。弊社では利用用途からblocks, databases, pages, usersを選択しました

        3. connectionの詳細設定です。destinationのtable名の定義や連携頻度などを設定します

    4. 成功すると、connectionsに次のような項目が作成されます

以上でAirbyte上の設定は完了です

連携データの確認

今回検証用に連携したデータはこちらのDatabaseになります。今回はNotion databaseのプロパティの加工をメインについて説明していきます

それでは、実際に連携されたデータを確認してみましょう。

データが連携され、blocks, databases, pages, usersの4tableが作成されました。databseとpageのみ中身を1つづつみてみましょう

databases

サンプル ※後続の処理で必要なもののみ記載

id uniqueなidです fa15752d-6c2c-4cc1-87c4-f890c42922b1
url NotionのURLが入ります https://www.notion.so/<< url of database >>
title titleに関する情報が入ります。画像などの設定変更が可能なため、いくつかの要素がJSON形式で格納されています [{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":"DB for Advent Calendar","text":{"content":"DB for Advent Calendar","link":null},"type":"text"}]
object databaseのみが入ります "database”
parent 親ページなどのidが入ります {"page_id":"b3dc83cf-46dd-4c98-b25a-c0a82c79c985","type":"page_id"}
user 作成したユーザのidが入ります {"id":"7521f563-d45c-824a-5fed-cc25579fbee0","object":"user"}
properties データベースのプロパティ設定情報が入っています。 [{"name":"client","value":{"id":"EZr%60","name":"client","select":{"options":[{"color":"pink","description":null,"id":"55c19a3a-b444-42f9-bf60-3b938bed8ae2","name":"client1"},{"color":"green","description":null,"id":"5623ecce-7991-44a7-ad79-4552b103737d","name":"client2"},{"color":"default","description":null,"id":"b05427d6-23ad-4fb2-923d-0b06f1d5a6cc","name":"client4"},{"color":"blue","description":null,"id":"96ecd8b4-4bc6-4e7f-bc41-ebfbc7ea0047","name":"client3"}]},"type":"select"}},{"name":"work_term","value":{"date":{},"id":"KOCl","name":"work_term","type":"date"}},{"name":"status","value":{"id":"T%3C%40%3B","name":"status","status":{"groups":[{"color":"gray","id":"37ec97e5-6fda-4923-85b9-2020e3499c1e","name":"To-do","option_ids":["01be2627-7dbe-4986-b997-a48a6885d178"]},{"color":"blue","id":"a498c9b0-837c-427b-838c-bdf4e5217f91","name":"In progress","option_ids":["f8f0fa92-3624-4b8b-952d-19a2d79d25d9"]},{"color":"green","id":"44c47543-6d12-4b2f-a565-84fb436cc5d9","name":"Complete","option_ids":["a19cf773-a2fb-4130-b4f9-ac15f578e520"]}],"options":[{"color":"default","description":null,"id":"01be2627-7dbe-4986-b997-a48a6885d178","name":"未着手"},{"color":"blue","description":null,"id":"f8f0fa92-3624-4b8b-952d-19a2d79d25d9","name":"進行中"},{"color":"green","description":null,"id":"a19cf773-a2fb-4130-b4f9-ac15f578e520","name":"完了"}]},"type":"status"}},{"name":"job_type","value":{"id":"WS%7B~","multi_select":{"options":[{"color":"purple","description":null,"id":"61456b2f-eae4-430c-905b-b99465b42306","name":"現場"},{"color":"pink","description":null,"id":"b8404fb5-5006-4045-92e3-11a12fa05de0","name":"office"}]},"name":"job_type","type":"multi_select"}},{"name":"担当者","value":{"id":"cO%3AX","name":"担当者","people":{},"type":"people"}},{"name":"ID","value":{"id":"hyZ%3B","name":"ID","type":"unique_id","unique_id":{"prefix":null}}},{"name":"boss check","value":{"checkbox":{},"id":"klS%5E","name":"boss check","type":"checkbox"}},{"name":"price","value":{"id":"rJ%3DF","name":"price","number":{"format":"number"},"type":"number"}},{"name":"job","value":{"id":"title","name":"job","title":{},"type":"title"}}]
last_edited_by 最終更新者の情報が入ります {"id":"7521f563-d45c-824a-5fed-cc25579fbee0","object":"user"}

pages

サンプル ※後続の処理で必要なもののみ記載

上記のdatabaseに絞って確認してみます

select
  *
from
  << projectID >>.<< datasetID >>.pages
where
  json_extract_scalar(parent,'$.database_id') = 'fa15752d-6c2c-4cc1-87c4-f890c42922b1'
id uniqueなidです 1501ff99-6922-8057-a8ec-d35cbfdfa922
url NotionのURLが入ります https://www.notion.so/<< url of pages >>
parent databaseの場合databaseが、親ページの場合は親ページの情報が記載 {"database_id":"fa15752d-6c2c-4cc1-87c4-f890c42922b1","type":"database_id"}
properties ページのプロパティ設定情報が入っています。 [{"name":"client","value":{"id":"EZr%60","select":{"color":"blue","id":"96ecd8b4-4bc6-4e7f-bc41-ebfbc7ea0047","name":"client3"},"type":"select"}},{"name":"work_term","value":{"date":{"end":null,"start":"2024-12-02","time_zone":null},"id":"KOCl","type":"date"}},{"name":"status","value":{"id":"T%3C%40%3B","status":{"color":"green","id":"a19cf773-a2fb-4130-b4f9-ac15f578e520","name":"完了"},"type":"status"}},{"name":"job_type","value":{"id":"WS%7B~","multi_select":[{"color":"pink","id":"b8404fb5-5006-4045-92e3-11a12fa05de0","name":"office"}],"type":"multi_select"}},{"name":"担当者","value":{"id":"cO%3AX","people":[{"avatar_url":null,"id":"4163a128-245c-484e-8fed-cc63544ebff0","name":"Yuki Furuta","object":"user","person":{"email":"yuki.furuta@meetsmore.com"},"type":"person"}],"type":"people"}},{"name":"ID","value":{"id":"hyZ%3B","type":"unique_id","unique_id":{"number":4,"prefix":null}}},{"name":"boss check","value":{"checkbox":true,"id":"klS%5E","type":"checkbox"}},{"name":"price","value":{"id":"rJ%3DF","number":40000,"type":"number"}},{"name":"job","value":{"id":"title","title":[{"annotations":{"bold":false,"code":false,"color":"default","italic":false,"strikethrough":false,"underline":false},"href":null,"plain_text":"job4","text":{"content":"job4","link":null},"type":"text"}],"type":"title"}}]
last_edited_by 最終更新者の情報が入ります {"id":"7521f563-d45c-824a-5fed-cc25579fbee0","object":"user"}

各tableのpropertiesの比較

ここで、databases.propertiesの中にある各プロパティの比較をしてみましょう

列名 ID job client job_type price status work_term 担当者 boss check
pagesのJSON {"name": "ID","value": {"id": "hyZ%3B","type": "unique_id","unique_id": {"number": 4,"prefix": null}}} {"name": "job","value": {"id": "title","title": [{"annotations": {"bold": false,"code": false,"color": "default","italic": false,"strikethrough": false,"underline": false},"href": null,"plain_text": "job4","text": {"content": "job4","link": null},"type": "text"}],"type": "title"}} {"name": "client","value": {"id": "EZr%60","select": {"color": "blue","id": "96ecd8b4-4bc6-4e7f-bc41-ebfbc7ea0047","name": "client3"},"type": "select"}} {"name": "job_type","value": {"id": "WS%7B~","multi_select": [{"color": "pink","id": "b8404fb5-5006-4045-92e3-11a12fa05de0","name": "office"}],"type": "multi_select"}} {"name": "price","value": {"id": "rJ%3DF","number": 40000,"type": "number"}} {"name": "status","value": {"id": "T%3C%40%3B","status": {"color":"green","id": "a19cf773-a2fb-4130-b4f9-ac15f578e520","name": "完了"},"type": "status"}} {"name": "work_term","value": {"date": {"end": null,"start": "2024-12-02","time_zone": null},"id": "KOCl","type": "date"}} {"name": "担当者","value": {"id": "cO%3AX","people": [{"avatar_url": null,"id": "4163a128-245c-484e-8fed-cc63544ebff0","name": "Yuki Furuta","object": "user","person": {"email": "yuki.furuta@meetsmore.com"},"type": "person"}],"type": "people"}} {"name": "boss check","value": {"checkbox": true,"id":"klS%5E","type": "checkbox"}}
databaseのJSON {"name": "ID","value": {"id": "hyZ%3B","name": "ID","type": "unique_id","unique_id": {"prefix": null}}} {"name": "job","value": {"id": "title","name": "job","title": {},"type": "title"}} {"name": "client","value": {"id": "EZr%60","name": "client","select": {"options": [{"color": "pink","description": null,"id": "55c19a3a-b444-42f9-bf60-3b938bed8ae2","name": "client1"},{"color": "green","description": null,"id": "5623ecce-7991-44a7-ad79-4552b103737d","name": "client2"},{"color": "default","description": null,"id": "b05427d6-23ad-4fb2-923d-0b06f1d5a6cc","name": "client4"},{"color": "blue","description": null,"id": "96ecd8b4-4bc6-4e7f-bc41-ebfbc7ea0047","name": "client3"}]},"type": "select"}} {"name": "job_type","value": {"id": "WS%7B~","multi_select": {"options": [{"color": "purple","description": null,"id": "61456b2f-eae4-430c-905b-b99465b42306","name": "現場"},{"color": "pink","description": null,"id": "b8404fb5-5006-4045-92e3-11a12fa05de0","name": "office"}]},"name": "job_type","type": "multi_select"}} {"name": "price","value": {"id": "rJ%3DF","name": "price","number": {"format": "number"},"type": "number"}} {"name": "status","value": {"id": "T%3C%40%3B","name": "status","status": {"groups": [{"color": "gray","id": "37ec97e5-6fda-4923-85b9-2020e3499c1e","name": "To-do","option_ids": ["01be2627-7dbe-4986-b997-a48a6885d178"]},{"color": "blue","id": "a498c9b0-837c-427b-838c-bdf4e5217f91","name": "In progress","option_ids": ["f8f0fa92-3624-4b8b-952d-19a2d79d25d9"]},{"color": "green","id": "44c47543-6d12-4b2f-a565-84fb436cc5d9","name": "Complete","option_ids": ["a19cf773-a2fb-4130-b4f9-ac15f578e520"]}],"options": [{"color": "default","description": null,"id": "01be2627-7dbe-4986-b997-a48a6885d178","name": "未着手"},{"color": "blue","description": null,"id": "f8f0fa92-3624-4b8b-952d-19a2d79d25d9","name": "進行中"},{"color": "green","description": null,"id": "a19cf773-a2fb-4130-b4f9-ac15f578e520","name": "完了"}]},"type": "status"}} {"name": "work_term","value": {"date": {},"id": "KOCl","name": "work_term","type": "date"}} {"name": "担当者","value": {"id": "cO%3AX","name": "担当者","people": {},"type": "people"}} {"name": "boss check","value": {"checkbox": {},"id": "klS%5E","name": "boss check","type": "checkbox"}}
各プロパティ(列)のid hyZ%3B title EZr%60 WS%7B~ rJ%3DF T%3C%40%3B KOCl cO%3AX klS%5E

ここで、各プロパティはdatabaseもその子ページも同じIDを持っていることがわかりました。このIDは次のデータを加工ステップでも利用するため、保存しておきましょう

データ加工プロセスの一例

ミツモアではデータ加工ツールとしてdbt(Data Build Tool)を導入しています

全コード公開します

次のような流れの加工プロセスを構築しました

  1. Data Lake layer
  2. pagesのプロパティ列をプロパティ単位に変換
  3. プロパティをプロパティの型に応じて変換
  4. database別にtableを作成

Data Lake layer

サンプルコード

source_lake_notion_pages.sql
{{
  config(
    materialized = 'incremental',
    unique_key = '_airbyte_raw_id',
    partition_by = {'field': '_airbyte_extracted_date', 'data_type': 'date'},
  )
}}

select
    _airbyte_raw_id,
    id,
    url,
    icon,
    cover,
    parent,
    json_extract_scalar(parent,'$.database_id') parent_database_id,
    archived,
    created_by,
    properties,
    public_url,
    created_time,
    last_edited_by,
    last_edited_time,

    date(_airbyte_extracted_at) _airbyte_extracted_date,    
    _airbyte_extracted_at,
    _airbyte_meta,
    _airbyte_generation_id,
from
    {{ airbyte_notion_table('pages') }}
where
    1 = 1
    {% if is_incremental() %}
      and _airbyte_extracted_at >= TIMESTAMP_SUB(CURRENT_TIMESTAMP(), INTERVAL 2 DAY)
    {% endif %}

pagesのプロパティ列をプロパティ単位に変換

ここでは、pages.propertiesに格納されているJsonをparseします。その際、プロパティには現在21種類の形が存在し、それぞれ異なる形式で格納されているので、種類別に取得方法を変える処理を記載しています。ここでは、各種類別にJSONのデータの保持方法が数パターンあったのでそれを概ね網羅するようにパターン分けしています。

ただし、現在記載してあるものはごく一部で利用するもののみを加工する目的で記載しており、よりカスタマイズが必要なケースもございますのでご容赦ください

サンプルコード

_base_notion_properties.sql
{{
    config(
        materialized="table",
        partition_by={"field": "date", "data_type": "date"},
    cluster_by = ["id", 'parent_database_id', 'property_value_type'],
    )
}}

with base as (
    select
        id,
        {{ timestamp_to_datetime("created_time") }} as createdat,
        {{ timestamp_to_date("created_time") }} as date,
        {{ timestamp_to_week("created_time") }} as week,
        {{ timestamp_to_month("created_time") }} as month,
        {{ timestamp_to_datetime("last_edited_time") }} as updatedat,
        url,
        icon,
        cover,
        parent,
        parent_database_id,
        archived,
        created_by,
        properties,
        json_item as property_json_item,
        JSON_VALUE(json_item, '$.name') as property_name,
        JSON_VALUE(json_item, '$.value.type') as property_value_type,
        JSON_VALUE(json_item, '$.value.has_more') as property_value_has_more,
        JSON_VALUE(json_item, '$.value.id') as property_value_id,
        JSON_VALUE(json_item.value.relation[0].id) as property_value_relation,
        JSON_VALUE(json_item, '$.value.url') as property_value_url,
        JSON_VALUE(json_item.value.people[0].person.email) as property_value_people_email,
        JSON_VALUE(json_item.value.people[0].person.name) as property_value_people_name,
        JSON_VALUE(json_item, '$.value.date.start') as property_value_date_start,
        JSON_VALUE(json_item, '$.value.formula.string') as property_value_formula_string,
        JSON_VALUE(json_item, '$.value.select.name') as property_value_select,
        JSON_VALUE(json_item, '$.value.rich_text') as property_value_rich_text,
        JSON_VALUE(json_item, '$.value.number') as property_value_number,
        JSON_VALUE(json_item, '$.value.status.id') as property_value_status_id,
        JSON_VALUE(json_item, '$.value.status.name') as property_value_status_name,
        JSON_VALUE(json_item, '$.value.created_time') as property_value_created_time,
        JSON_VALUE(json_item, '$.value.checkbox') as property_value_checkbox,
        JSON_VALUE(json_item.value.title[0].plain_text) as property_value_title,
        JSON_VALUE(json_item, '$.value.last_edited_time') as property_value_last_edited_time,
        JSON_VALUE(json_item, '$.value.unique_id.number') as property_value_unique_id_number,
        JSON_VALUE(json_item, '$.value.verification') as property_value_verification,
        public_url,
        created_time,
        last_edited_by,
        last_edited_time,
        row_number() over (
            partition by id, JSON_VALUE(json_item, '$.value.id') order by _airbyte_extracted_at desc
        ) rk,

    from
        {{ ref("source___airbyte_noProduct_notion___pages") }},
        UNNEST(JSON_EXTRACT_ARRAY(properties)) as json_item

    where
        1 = 1
)
, multi_select as (
    select
        id,
        property_value_id,
        ARRAY_TO_STRING(ARRAY_AGG(JSON_VALUE(multi_select_item, '$.name')), ', ') AS property_value_multi_select,
    from
        base,
        UNNEST(JSON_EXTRACT_ARRAY(JSON_EXTRACT(property_json_item, '$.value.multi_select'))) AS multi_select_item
    where
        property_value_type = 'multi_select'
    group by 1,2
)

select
    b.* except (rk),
    m.property_value_multi_select
from
    base b
    left join multi_select m
    on b.id = m.id and b.property_value_id = m.property_value_id
where 1 = 1 and b.rk = 1

注意点/今後の課題

  • 今回は、この処理自体は全てのページに対して共通の処理を実施することにしました。そのため、今後大量のデータを連携することになると大量のデータを同時に処理することになり、materialized="table"だと予期しないコストが発生する可能性がございます。
  • プロパティ変換については、今後21種類から増える可能性があるため、新しい種類が増えた検知をしたりそれに対応するように修正したりする必要があります
  • プロパティ種類が数式(formula)の場合、page.propertiesには計算後の値しか入っていませんでした。数式を確認する必要がある場合、おそらくdatabaseの中にあるデータを取得する必要があります。また、数式をSQLで再現するには動的に変換を行う必要があるため、今回は諦めました

プロパティをプロパティの型に応じて変換する

プロパティの変換パターンをあらかじめ確認したところ

  • 選択 : select
  • マルチセレクト : multi_select
  • 日付系全般 : date

のようなproperty value typeを保持しているようでした。そのため、それぞれのtype別に各プロパティに対して特定の値を保持するように変換する

サンプルコード

mid_notion_properties.sql
{{
    config(
        materialized="table",
        unique_key="id",
        partition_by={"field": "date", "data_type": "date"},
    cluster_by = ["id", 'parent_database_id'],
    )
}}

    select
        p.id,
        p.createdat,
        p.property_json_item,
        p.date,
        p.week,
        p.month,
        p.updatedat,
        p.url,
        p.parent_database_id,
        p.archived,
        p.public_url,
        p.created_time,
        p.last_edited_time,
        p.property_value_id,
        p.property_value_type,
        p.property_value_people_email,
        p.property_value_created_time,
        p.property_value_url,
        p.property_value_date_start,
        p.property_value_select,
        p.property_value_rich_text,
        p.property_value_relation,
        p.property_value_formula_string,
        p.property_value_title,
        p.property_value_status_id,
        p.property_value_status_name,
        p.property_value_multi_select,
        p.property_value_verification,
        p.property_value_number,
        p.property_value_checkbox,
        p.property_value_last_edited_time,
        p.property_value_unique_id_number,
        p.property_value_type as type_column,
        case 
            when p.property_value_type = 'people' then p.property_value_people_email
            when p.property_value_type = 'created_time' then p.property_value_created_time
            when p.property_value_type = 'url' then p.property_value_url
            when p.property_value_type = 'date' then p.property_value_date_start
            when p.property_value_type = 'select' then p.property_value_select
            when p.property_value_type = 'rich_text' then p.property_value_rich_text
            when p.property_value_type = 'last_edited_time' then p.property_value_last_edited_time
            when p.property_value_type = 'relation' then p.property_value_relation
            when p.property_value_type = 'formula' then p.property_value_formula_string
            when p.property_value_type = 'title' then p.property_value_title
            when p.property_value_type = 'unique_id' then p.property_value_unique_id_number
            when p.property_value_type = 'multi_select' then p.property_value_multi_select
            when p.property_value_type = 'verification' then p.property_value_verification
            when p.property_value_type = 'status' then p.property_value_status_name
            when p.property_value_type = 'number' then p.property_value_number
            when p.property_value_type = 'checkbox' then p.property_value_checkbox
            else null
        end as property_value

    from
        {{ ref("_base_notion_properties") }} p

    where
        1 = 1

database別にtableを作成

データベース作成を再現するレイヤーになります

事前準備

  • parent_database_id : どのidなのかを別途特定する必要があります
  • property_value_id : 各プロパティを直に見て、どのプロパティがどのidなのかを特定する必要があります

サンプルコード

target_database.sql
{{
    config(
        materialized="table",
        unique_key="id",
        partition_by={"field": "date", "data_type": "date"},
    cluster_by = ["id"],
    )
}}

with base as (
    select
        id,
        createdat,
        property_json_item,
        date,
        week,
        month,
        updatedat,
        url,
        parent_database_id,
        archived,
        public_url,
        created_time,
        last_edited_time,
        property_value_id,
        property_value,
        type_column,
    from
        {{ ref("mid_notion_properties") }}

    where
        1 = 1
        and parent_database_id = '<< database id >>'
)
, properties as (
    select
        id,

        max(if(property_value_id = 'title', property_value, null)) as prop_title,
        max(if(property_value_id = 'hyZ%3B', property_value, null)) as prop_id,
        max(if(property_value_id = 'EZr%60', property_value, null)) as prop_client,
        max(if(property_value_id = 'WS%7B~', property_value, null)) as prop_job_type,
        max(if(property_value_id = 'rJ%3DF', property_value, null)) as prop_price,
        max(if(property_value_id = 'T%3C%40%3B', property_value, null)) as prop_status,
        max(if(property_value_id = 'KOCl', property_value, null)) as prop_work_term,
        max(if(property_value_id = 'cO%3AX', property_value, null)) as prop_assignee,
        max(if(property_value_id = 'klS%5E', property_value, null)) as prop_boss_check,
        ...
    from
        base
    group by
        id
)
select
    distinct
    b.id,
    b.createdat,
    b.date,
    b.week,
    b.month,
    b.updatedat,
    b.url,
    b.parent_database_id,
    b.archived,
    b.public_url,
    b.created_time,
    b.last_edited_time,
    -- The following are property columns.
    p.prop_name,
    p.propaty_1,
    p.propaty_2,
    p.propaty_3,
        ...
from
    base b
    left join properties p
    on b.id = p.id

注意点/今後の課題

  • 現在の仕組みだとこの最後のレイヤーでcastをする必要があります。理由は、mid_notion_properties.sqlではproperty_valueとして1つの列に全てのデータを保持するようにしているからです。
  • date typeの課題。dateの場合、Notionではstart endをそれぞれ入力することができます。既存の作りだとそれらを良き形で保持することはできずひとまずはlistをtextに変換した形で取り扱うことしかできなく、加工が逆に煩雑になってしまう可能性がある

では、完成したtableにselectしてみましょう

select
  prop_title,
  prop_id,
  prop_client,
  prop_job_type,
  prop_price,
  prop_status,
  prop_work_term,
  prop_assignee,
  prop_boss_check,
from
  dataset_name.target_database
order by prop_id

Notion databaseをほぼほぼ再現できました!!

まとめ

Notion pageのプロパティ情報を分析基盤 DB上で再現することで、BIツールなどで良き形へ変換したり別のtableと結合したりすることで、欲しい可視化をすることが可能になりました!

再現したい場合は、次のステップで構築が可能ですのでクエリ作成はほぼ必要ありません。ぜひ試してください

  1. Airbyteで連携する
  2. 各SQLをコピペする
  3. database idを取得して置換する
  4. 各プロパティidを取得して列名とともに書き換える

最後に

ミツモアでは様々な職種のエンジニアを積極的に採用しています! ご興味がある方はぜひ気軽に面談しましょう!

データ分析領域でも絶賛人材を募集しており、本記事のようなELT構築〜アナリティクスエンジニア領域に強い人材を特に募集しています。データエンジニア以外でも採用可能なポジションもある可能性がありますので、古田個人宛でも公式からでもどしどしご応募ください