※こちらはミツモア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を使う選択をしました。
連携方法
- Airbyteアカウントを作ってからDestinationの設定まで完了している前提として進めさせていただきます
- Source設定
New Sourceをクリックし、Notionを選択

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

- 今回はOAuth2.0で行いました。この方式の場合は、認証を実施するユーザがNotionのデータを取得したいページなどのフルアクセス権限を持っている必要があります。アクセス承認を行い、取得したいデータを選択してアクセスを許可してください
Connectionを作成する
- Connection(source - destination)の設定をします
弊社のデータ基盤はBigQueryのため、DestinationにはBigQueryを選択しました。詳しくはこちらをご覧ください

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

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

- Connection(source - destination)の設定をします
成功すると、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)を導入しています
全コード公開します
次のような流れの加工プロセスを構築しました
- Data Lake layer
- pagesのプロパティ列をプロパティ単位に変換
- プロパティをプロパティの型に応じて変換
- 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と結合したりすることで、欲しい可視化をすることが可能になりました!
再現したい場合は、次のステップで構築が可能ですのでクエリ作成はほぼ必要ありません。ぜひ試してください
- Airbyteで連携する
- 各SQLをコピペする
- database idを取得して置換する
- 各プロパティidを取得して列名とともに書き換える
最後に
ミツモアでは様々な職種のエンジニアを積極的に採用しています! ご興味がある方はぜひ気軽に面談しましょう!
データ分析領域でも絶賛人材を募集しており、本記事のようなELT構築〜アナリティクスエンジニア領域に強い人材を特に募集しています。データエンジニア以外でも採用可能なポジションもある可能性がありますので、古田個人宛でも公式からでもどしどしご応募ください
