BigQueryマテリアライズドビュー、使っていますでしょうか。
私も以前、対象テーブルの特定の断面を一時的に見せたいから、マテリアライズドビューを使おう!と意気込んだことがあったのですが、BigQueryマテリアライズドビューは高性能が故に要件を満たさなかったということがありました。
そもそもマテリアライズドビューとは?
マテリアライズドビューはBigQueryに限らず、PostgreSQLやRedshift等、他のDB、DWHサービスでも提供されている機能となります。通常のViewは事前定義したクエリで複数のテーブルへの変換や結合等の処理を行った結果の一時テーブル(仮想)ですが、マテリアライズドビューは事前定義したクエリ結果を物理テーブルとしてDBに保存しています。Viewと違いSELECTを実行する度に再計算されるわけではなく、キャッシュのように計算結果を保持しているため、結果を素早く得ることができます。また、DBによるのですがSnapshotのように振る舞うことができる、逆を言うとマテリアライズドビューの更新をしなければ、前回更新した時点の結果しか得られない、というような動きをします。
はじめにマテリアライズドビューを考えたときは、あえて特定時点の状態を参照したい(例えば元テーブルには当日分のレコードが登録されているが、データ利用者からは常に前日の状態を取得したいとか)という要件から採用を検討したのですが、"BigQuery"のマテリアライズドビューは優秀であるがゆえ、常に最新の状態を得ることが可能となっています。なので本当にSnapshotではなくキャッシュのようなイメージです。
実際の動き
実際の動きを見てましょう。
例として、メーカーにおける製品の在庫状況を把握するようなケースを考えてみます。
サンプルとして簡易的なテーブルを用意します。
初期データ準備
-- 1. 製品マスタ
CREATE OR REPLACE TABLE mv_test.product_master (
product_id STRING,
product_name STRING,
category STRING
);
-- 2. 場所マスタ
CREATE OR REPLACE TABLE mv_test.location_master (
location_id STRING,
location_name STRING,
location_type STRING
);
-- 3. 在庫受払
CREATE OR REPLACE TABLE mv_test.inventory_transactions (
transaction_id INT64,
transaction_time TIMESTAMP,
record_updated_at TIMESTAMP,
product_id STRING,
location_id STRING,
quantity_change INT64,
description STRING
)
PARTITION BY DATE(transaction_time)
CLUSTER BY product_id, location_id;
次に、テストデータを挿入していきます。
-- 4. 初期データの投入
-- 製品マスタ
INSERT INTO mv_test.product_master (product_id, product_name, category)
VALUES
('P00001', '製品A', 'カテゴリーZ'),
('P00002', '製品B', 'カテゴリーY'),
('P00003', '製品C', 'カテゴリーX');
-- 場所マスタ
INSERT INTO mv_test.location_master
VALUES
('LOCFAC01', '第1工場', 'FACTORY'),
('LOCFAC02', '第2工場', 'FACTORY'),
('LOCWH01', '埼玉倉庫', 'WAREHOUSE'),
('LOCWH02', '愛知倉庫', 'WAREHOUSE');
-- 5. 初期在庫の投入
INSERT INTO mv_test.inventory_transactions
VALUES
(1, TIMESTAMP('2025-12-02 09:00:00'), TIMESTAMP('2025-12-02 09:00:00'), 'P00001', 'LOCFAC01', 100, '初期在庫'),
(2, TIMESTAMP('2025-12-02 09:30:00'), TIMESTAMP('2025-12-02 09:30:00'), 'P00002', 'LOCFAC02', 200, '初期在庫');
初期データ挿入後の各テーブル状態です。
ソーステーブルへの初期データ挿入が完了したので、Materialized Viewを作成し各工場、倉庫の製品在庫状況を確認してみます。
-- 6. Materialized View
CREATE MATERIALIZED VIEW mv_test.mv_current_stock_detailed
OPTIONS (enable_refresh = false)
AS
SELECT
t.product_id,
p.product_name,
t.location_id,
l.location_name,
l.location_type,
-- 在庫数量の集計
SUM(t.quantity_change) AS current_quantity,
-- データの鮮度確認用
MAX(t.record_updated_at) AS last_record_updated_at,
MAX(t.transaction_id) AS last_transaction_id
FROM
mv_test.inventory_transactions AS t
INNER JOIN
mv_test.product_master AS p ON t.product_id = p.product_id
INNER JOIN
mv_test.location_master AS l ON t.location_id = l.location_id
GROUP BY
1, 2, 3, 4, 5;
尚、ここでは常に最新の状態が取得できることを検証するため、OPTIONS (enable_refresh = false)を指定し自動更新を無効化しています。
作成したマテリアライズドビューの状態は次の通りです。
第1、2工場にそれぞれ在庫があることが分かります。
製品移動
それでは、翌日製品の移動が発生したと仮定して、データを挿入してみます。
-- 7. 第1工場から埼玉倉庫へ30個移動
INSERT INTO mv_test.inventory_transactions
VALUES
-- 第1工場から出庫 (-30)
(3, TIMESTAMP('2025-12-03 10:00:00'), TIMESTAMP('2025-12-02 10:00:00'), 'P00001', 'LOCFAC01', -30, '移動出庫'),
-- 埼玉倉庫へ入庫 (+30)
(4, TIMESTAMP('2025-12-03 11:00:00'), TIMESTAMP('2025-12-02 11:00:00'), 'P00001', 'LOCWH01', 30, '移動入庫');
-- 8. 第2工場から愛知倉庫へ50個移動
INSERT INTO mv_test.inventory_transactions
VALUES
-- 第2工場から出庫 (-50)
(5, TIMESTAMP('2025-12-03 12:00:00'), TIMESTAMP('2025-12-02 12:00:00'), 'P00002', 'LOCFAC02', -50, '移動出庫'),
-- 埼玉倉庫へ入庫 (+50)
(6, TIMESTAMP('2025-12-03 14:00:00'), TIMESTAMP('2025-12-02 14:00:00'), 'P00002', 'LOCWH02', 50, '移動入庫');
挿入後の在庫受払テーブルは次の状態です。
正しくレコードが挿入されていますね。
では、この状態でマテリアライズドビューを見てみます。
マテリアライズドビュー作成時に自動更新は無効にしていましたので、従来のマテリアライズドビューで考えると先ほどと同じ結果が得られるように思いますが、BigQueryのマテリアライズドビューでは最新の集計結果を得ることができています。
余談
BigQueryのマテリアライズドビューはビューの更新有無に関わらず、常に最新の状態が取得できることを改めて確認できました。ただ、元々検討したきっかけである、特定時点の状態を取得するという点は満たせていないので、今回のテストテーブルだとどのようなクエリで取得できるか、を最後に考えました。
-- '2025-12-03 09:00:00'時点の在庫状況を確認する
SELECT
t.product_id,
p.product_name,
t.location_id,
l.location_name,
l.location_type,
SUM(t.quantity_change) AS quantity_at
FROM
mv_test.inventory_transactions AS t
INNER JOIN
mv_test.product_master AS p ON t.product_id = p.product_id
INNER JOIN
mv_test.location_master AS l ON t.location_id = l.location_id
WHERE
t.record_updated_at <= '2025-12-03 09:00:00'
GROUP BY
1, 2, 3, 4, 5
ORDER BY
t.location_id;
record_updated_atカラムが'2025-12-03 09:00:00'以前のレコードに絞り
集計をすることで、指定した日時時点の状態を得ることができています。
元々、BIツールでユーザが指定した日付時点の状態をフレキシブルに参照したいという要望が発端でした。
今回のサンプルで言うとrecord_updated_atカラムを例えばBIツールから投げるクエリのパラメータとし、そのパラメータをダッシュボード利用者がフレキシブルに変更する、という作りをすれば、上記クエリで指定時点の状態を可視化することができそうですね。






