2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Aurora PostgreSQL の DuckDB 統合を試してみた

2
Posted at

1. はじめに

2026 年 9 月 30 日に、Aurora PostgreSQL から Apache Iceberg と Parquet のデータを直接クエリできるようになりました1。Parquet は列ごとにまとめて保存するファイル形式で、Iceberg は Parquet などのファイルを 1 つの表として管理する形式です。拡張機能 aurora_analytics を入れると、S3・S3 Tables・Glue Data Catalog 上のこれらのデータを PostgreSQL の外部表として扱えます。外部表は、データを Aurora の外に置いたまま SELECT できる読み取り専用の表です。外部表の読み取りや集計は、PostgreSQL のプロセスに組み込まれた分析向けのエンジン DuckDB で実行されます2。DuckDB は PostgreSQL と同じプロセスで動くので、使う CPU とメモリは Aurora の DB インスタンス(Serverless v2 なら ACU)のものです。クエリごとの作業メモリはインスタンスのメモリから割り当てられ、並列に動くスレッドの数は Aurora が自動で決めます3。本記事では、この DuckDB 統合を試しました。

公式ドキュメントのユースケースに、次の一文がありました4。

Keep hot operational data in Aurora PostgreSQL and move cold historical data into Iceberg or Parquet in your data lake

よく使う直近のデータは Aurora に置き、古いデータは Iceberg や Parquet に移して、外部表として読み続ける使い方です。古いデータを Aurora の外に移せば、Aurora のストレージは減ります。そのうえで、アプリの SQL を変えずに同じ結果が得られるかを確かめます。

紹介ブログ5には、直近の表と Parquet の外部表を、クエリに UNION ALL を直接書いてつなぐ例があります。アプリの SQL を書き換えずに済ませるなら、元の表名でビューを作ってつなぐ方法が考えられます。そこで本記事では、TPC-H(データベースの性能評価で使われる標準のデータセット)の 1997 年以前の行(以下、古い行)を S3 Tables の Iceberg に移しました。1998 年以降の行(以下、直近の行)は Aurora に置いたまま、元の表名のビューに切り替えて、同じ SQL の結果と所要時間を試してみました。S3 Tables は、Iceberg の表を保存・管理する S3 の機能です6。

1.1. 結論(先出し)

  • 古い行の外部表と直近の行の表を UNION ALL のビューで元の表名につなぐと、アプリの SQL を変えずに読めた。ただし SELECT * でつないだだけのビューでは、文字列の末尾の空白と ORDER BY の並び順が移す前と変わった。外部表側の型と照合順序をそろえたビューでは、4 本のクエリとも移す前と同じ結果が返った
  • 型をそろえたビューの所要時間は、顧客の履歴以外の 3 本で移す前の 1.4〜2.5 倍だった(SELECT * のビューでは 2.1〜5.5 倍)。顧客の履歴は 0.164 ミリ秒から 0.70 秒になった
  • 遅くなる主な原因は、外部表の 5,314 万行を PostgreSQL に返してから集計していることだと考えられる。ビューを通さず外部表だけを集計する SQL にすると、集計まで DuckDB で実行され 2.47 秒だった
  • 実行計画の Pushdown SQL、Database Insights の待機イベント、CloudWatch の AuroraAnalytics メトリクスで、外部表の処理を確かめられた

1.2. 検証ゴール

# 確かめること 確認できれば OK の条件
1 古い行を Iceberg に移しても、アプリの SQL を変えずに同じ結果が返るか 移す前と移した後で、代表的な 4 本のクエリの結果が値まで一致する。一致しない場合は原因が分かっている
2 移した後、クエリはどれくらい遅くなるか 同じ 4 本の所要時間を、移す前と同じ容量(16 ACU 前後)で比べられている
3 いつもの監視ツールで、外部表の処理が見えるか 実行計画・Database Insights・CloudWatch で、外部表の処理がどう表示されるかを確かめられている

2. 検証環境

項目 内容
Aurora PostgreSQL 18.6、Serverless v2、ストレージは Aurora Standard
容量(ACU) 移す前の計測は 2〜16 ACU の設定で、計測中は 15.5〜16 ACU だった。移した後の計測は最小・最大とも 16 ACU に固定(ACU は Serverless v2 の容量の単位)
拡張機能 aurora_analytics 1.0.0
データ TPC-H SF10(SF はデータ量の倍率)。orders 1,500 万行、lineitem 約 6,000 万行、customer 150 万行
古い行の置き場 S3 Tables のテーブルバケット(Iceberg の表専用に作る S3 バケット)
リージョン 東京(ap-northeast-1)
クライアント 手元の PC(WSL2)の psql 16.15

2026 年 10 月時点の Aurora のドキュメントでは、外部表は読み取り専用です7。aws_s3 拡張機能の書き出しは COPY の形式(text・csv・binary)に限られ89、Aurora から Parquet や Iceberg に書き出す方法は見つけられませんでした。そのため Iceberg の表は、Aurora に入れたのと同じ TPC-H のデータから Aurora の外で作りました。作り方は本記事の主題ではないので省略します。置き場には、IMPORT FOREIGN SCHEMA で表をまとめて外部表にできる S3 Tables を選びました10。

3. 構成と準備

図 1: ビューでネイティブ表と S3 Tables の外部表をつなぐ構成

図 1: ビューでネイティブ表と S3 Tables の外部表をつなぐ構成

アプリは元の表名(orders・lineitem)に対して SQL を実行します。この名前はビューです。直近の行はネイティブ表(Aurora の中に保存している通常の表。名前は *_hot)から読み、古い行は aurora_analytics の外部表を経由して S3 Tables から読みます。

3.1. IAM ロール

S3 Tables の表を読むには、s3tables:GetTableBucket・GetNamespace・GetTable・GetTableData の 4 つの権限が必要です11。外部表をまとめて作る IMPORT FOREIGN SCHEMA のために ListTables も付け12、機能名 AuroraAnalytics でクラスタに紐付けます。

aws rds add-role-to-db-cluster \
  --db-cluster-identifier iceberg-tiering \
  --feature-name AuroraAnalytics \
  --role-arn arn:aws:iam::<アカウントID>:role/aurora-iceberg-tiering-role

図 2: クラスタに紐付けた IAM ロール

図 2: クラスタに紐付けた IAM ロール

3.2. DB クラスタパラメータグループ

aurora_analytics.enabled を 1 にします12。このパラメータは変更がすぐに反映されるので、再起動は不要です。

図 3: aurora_analytics のパラメータ(クラスタパラメータグループ)

図 3: aurora_analytics のパラメータ(クラスタパラメータグループ)

3.3. 拡張機能と外部表

IMPORT FOREIGN SCHEMA で S3 Tables のテーブルバケットを指定すると、名前空間(namespace)の中の表をまとめて外部表にできます。列の定義は Iceberg のメタデータから自動で決まります。

CREATE EXTENSION aurora_analytics;

CREATE SCHEMA cold;
IMPORT FOREIGN SCHEMA tpch FROM SERVER aurora_analytics_server INTO cold
  OPTIONS (location 'arn:aws:s3tables:ap-northeast-1:<アカウントID>:bucket/aurora-iceberg-tiering-tb');

4. 古い行を移してビューに切り替える

まず TPC-H SF10 を全部 Aurora に入れて、移す前の基準を取りました。投入には、S3 に置いた Parquet の外部表からの INSERT ... SELECT を使いました。約 6,000 万行の lineitem の投入には約 11 分半かかりました。データレイクから SQL 1 文で取り込む、公式ドキュメントのユースケースのとおりの使い方です4。DDL は TPC-H 標準のもので、char(n) の列を含みます。

この投入用の外部表は、S3 の Parquet を URI で指定して作りました。ファイル名が .parquet で終わっていても、format を指定しないと次のエラーになりました。

ERROR:  format is required for foreign table
HINT:  Valid formats are "parquet" or "iceberg".

公式ドキュメントには「形式を推論できない S3 の URI のときだけ必要」と書かれています10。aurora_analytics 1.0.0 では拡張子から形式は推論されなかったので、S3 の URI で外部表を作るときは format 'parquet' を付けておくと確実です。

次に、古い行を Iceberg に移しました。orders は注文日(o_orderdate)、lineitem は出荷日(l_shipdate)で区切っています。

表 直近の行(Aurora、1998 年以降) 古い行(Iceberg、1997 年以前)
orders 1,333,214 行 13,666,786 行
lineitem 6,849,245 行 53,136,807 行

Iceberg 側の件数が Aurora 側の 1997 年以前の件数と一致することを確かめてから、Aurora の古い行を DELETE しました。続けて VACUUM FULL を実行しました。orders・lineitem・customer の 3 表と索引の合計(pg_total_relation_size)は、14.50 GiB から 1.80 GiB に減りました(orders は 2.68 GiB から 0.24 GiB、lineitem は 11.50 GiB から 1.25 GiB)。最後に、ネイティブ表を orders_hot・lineitem_hot に改名し、元の名前でビューを作りました。以下、このビューを「SELECT * のビュー」と呼びます。

ALTER TABLE orders RENAME TO orders_hot;
CREATE VIEW orders AS
  SELECT * FROM orders_hot
  UNION ALL
  SELECT * FROM cold.orders_cold;
-- lineitem も同じ書き方

アプリが実行する想定のクエリは次の 4 本です。移す前と後で文面は変えていません。これとは別に、並び順を確かめるために、直近の注文を ORDER BY o_comment で並べるクエリを 1 本使いました。

クエリ 中身
直近の集計 1998 年 7 月以降の注文の件数と金額
顧客の履歴 顧客 1 人の全期間の注文を新しい順に 20 件
全期間の集計 明細の全期間を、年と出荷方法(l_shipmode)ごとに集計
顧客との JOIN 顧客の市場区分(c_mktsegment)と年ごとの注文金額。customer はネイティブ表のまま

5. 結果

5.1. 同じ結果が返るか

SELECT * のビューでは、4 本中 1 本(全期間の集計)の結果が移す前と変わりました。並び順を確かめた別の 1 本でも違いが出ました。

クエリ SELECT * のビューでの違い
直近の集計 一致
顧客の履歴 一致
全期間の集計 49 行のうち 1992〜1997 年の 42 行で、l_shipmode の末尾の空白が消えた。件数と金額は同じ
顧客との JOIN 一致
移す前:              1992|AIR       |1083899|39353520474.4601
SELECT * のビュー:   1992|AIR|1083899|39353520474.4601

原因は列の型です。ネイティブ表の l_shipmode は char(10) で、値は 10 文字まで空白で埋まっています。外部表の同じ列は、Iceberg の文字列型から自動で決まった text です。この 2 つを UNION ALL でつなぐと、ビューの列は長さの指定がない bpchar になり、外部表から読んだ値は空白で埋められません。char(n) 同士の比較では末尾の空白が無視されるので13、PostgreSQL の中の GROUP BY では件数が合います。一方で、アプリが受け取る文字列は、1997 年以前の行だけ末尾の空白がありません。数値の列でも型が違い、たとえば o_custkey はネイティブ表で integer、外部表で bigint でした。

並び順の違いは照合順序(文字列を並べたり比べたりするときの規則)によるものです。PostgreSQL の照合順序には、言語ごとの規則を OS の C ライブラリ(libc)から取るもの(en_US.UTF-8 など)と、ICU ライブラリから取るもの(en-US-x-icu など)があります。これとは別に、言語の規則を使わず文字コードの値の順で並べる C という照合順序があります。外部表の列で使えるのは C と ICU のものだけで、libc のものは使えません7。IMPORT FOREIGN SCHEMA で作った外部表の文字列の列は C でした。データベースのデフォルトは libc の en_US.UTF-8 ですが、ビューの列は外部表と同じ C になっていました。直近の注文を ORDER BY o_comment で並べたところ、orders_hot から直接読んだ場合とビュー経由とで、先頭 15 行のうち 10 行が別の行でした。ビュー経由では、先頭が空白の値が先にまとまって並びます。

そこで、ビューの外部表側を、ネイティブ表と同じ型と照合順序にそろえました。以下、このビューを「型をそろえたビュー」と呼びます(照合順序もそろえています)。

CREATE VIEW orders AS
  SELECT * FROM orders_hot
  UNION ALL
  SELECT o_orderkey, o_custkey::integer,
         o_orderstatus::char(1) COLLATE "default",
         o_totalprice, o_orderdate,
         o_orderpriority::char(15) COLLATE "default", o_clerk::char(15) COLLATE "default",
         o_shippriority, o_comment::varchar(79) COLLATE "default"
  FROM cold.orders_cold;
-- lineitem も同じ書き方で、char(n)・integer・COLLATE "default" にそろえる

型をそろえたビューでは、4 本とも移す前と結果が一致し、ORDER BY o_comment の並びも元に戻りました。

ビューに型変換を書く代わりに、CREATE FOREIGN TABLE で外部表の列を明示して型をそろえる方法もあります。外部表の列の型には BPCHAR(char(n))や VARCHAR、INTEGER を指定できます14。ただし、外部表の列に付けられる照合順序は C と ICU だけなので7、libc のデフォルトにそろえる分はビュー側に残ります。今回は IMPORT FOREIGN SCHEMA で作った外部表をそのまま使ったので、この方法は試していません。

5.2. どれくらい遅くなるか

移した後の計測は、移す前の計測中に使われていた 15.5〜16 ACU に合わせて、16 ACU に固定しました。値は、キャッシュに載せるために 1 回実行してから 3 回測った中央値です。

クエリ 移す前(全部 Aurora) SELECT * のビュー 型をそろえたビュー
直近の集計 52 ミリ秒 108 ミリ秒(2.1 倍) 77 ミリ秒(1.5 倍)
顧客の履歴 0.164 ミリ秒 24.0 秒 0.70 秒
全期間の集計 30.8 秒 170.8 秒(5.5 倍) 75.6 秒(2.5 倍)
顧客との JOIN 13.9 秒 38.5 秒(2.8 倍) 20.0 秒(1.4 倍)

型をそろえたビューの所要時間は、SELECT * のビューの 1.4〜34 分の 1 でした。それでも移す前より遅く、顧客の履歴は、索引を使える移す前の約 4,300 倍の時間がかかります。

外部表のデータは、一度 S3 から読むとインスタンスのローカルストレージにキャッシュされます3。キャッシュは再起動で消えるので、再起動の直後に 1 回実行した値とも比べました。

クエリ ビュー 再起動直後 キャッシュに載った後 S3 から読んだ量(再起動直後)
全期間の集計 SELECT * のビュー 183.6 秒 170.8 秒 1,333,958 kB
全期間の集計 型をそろえたビュー 79.7 秒 75.6 秒 236,742 kB
顧客との JOIN SELECT * のビュー 44.5 秒 38.5 秒 347,715 kB
顧客との JOIN 型をそろえたビュー 20.6 秒 20.0 秒 99,395 kB

S3 から読んだ量は、実行計画の Analytics Remote Read Bytes の表示(kB)のままです。

再起動直後とキャッシュに載った後の差は 3〜16% でした。ビューの作り方による差(1.9〜2.3 倍)のほうが大きく出ました。型をそろえたビューで読む量が減ったのは、外部表から読む列がクエリで使う列だけになったためです(5.3 章)。

5.3. いつもの監視ツールで見えるか

EXPLAIN (VERBOSE) を実行すると、外部表の処理のうち、どこまでを DuckDB 側で実行したかが分かります。PostgreSQL ではなく DuckDB 側で処理させることを、プッシュダウンと呼びます。次は、SELECT * のビューで顧客の履歴を読んだときの外部表の部分です(列が長いので一部を省略しています)。

->  Subquery Scan on orders
      Filter: (orders.o_custkey = 7)
      ->  Append
            ->  Subquery Scan on "*SELECT* 1"
                  ->  Seq Scan on public.orders_hot
            ->  Subquery Scan on "*SELECT* 2"
                  ->  Custom Scan on cold.orders_cold
                        Pushdown SQL: SELECT o_orderkey, o_custkey, o_orderstatus, ...(9 列すべて)... FROM system.main.iceberg_scan(...) AS orders_cold) orders_cold
 Unsupported Pushdown Expressions:
 1: Collation
    Description: default
 2: Type
    Description: character(1)
...(中略)...

Pushdown SQL は DuckDB 側で実行した SQL で、WHERE がありません。顧客で絞る条件は、ビューの外側(Subquery Scan の Filter)で処理されています。EXPLAIN ANALYZE で見ると、外部表の部分は 13,666,786 行を PostgreSQL に返すだけで 20.1 秒かかっていました。そのあと PostgreSQL が、ビューの 1,500 万行のうち 14,999,972 行を Filter で除外していました(全体は 24.0 秒)。ネイティブ表の側も、索引を使わない Seq Scan でした。型をそろえたビューでは、同じ部分が次のようになりました。

->  Custom Scan on cold.orders_cold
      Pushdown SQL: SELECT o_orderkey, o_orderstatus, o_totalprice, o_orderdate FROM (...) orders_cold WHERE ((o_custkey)::INTEGER = 7::INTEGER)
      ->  ICEBERG_SCAN
            Filters: o_custkey=7
            Projections: o_orderkey, o_orderstatus, o_totalprice, o_orderdate

Pushdown SQL に顧客の条件が WHERE として入り、読む列も 4 列に減っています。SELECT * のビューでは、o_custkey などの型が両側で違うため、ビューが 1 段の副問い合わせ(Subquery Scan)として残り、条件を外部表側で処理できなかったと考えられます。

CloudWatch Database Insights(DB の負荷を待機イベントや SQL ごとに表示する監視機能)では、DuckDB で処理していた時間が待機イベント Extension:AuroraAnalyticsExecute として表示されます15。待機イベントは、セッションが何に時間を使ったかの分類です。図 4 は計測中の 35 分間で、紫が Extension:AuroraAnalyticsExecute です。再起動直後の計測を実行した 12:03〜12:06(日本時間)に、平均アクティブセッション数で約 8〜12 まで上がっています。

図 4: 計測中の DB 負荷(Database Insights、日本時間 12:00〜12:35)

図 4: 計測中の DB 負荷(Database Insights、日本時間 12:00〜12:35)

負荷の大きい順に SQL を並べた表でも、全期間の集計の負荷が CPU と Extension:AuroraAnalyticsExecute に分かれて表示されます。

図 5: 負荷の大きい順の SQL(Database Insights、日本時間 12:00〜12:35)

図 5: 負荷の大きい順の SQL(Database Insights、日本時間 12:00〜12:35)

CloudWatch には、AuroraAnalytics で始まるメトリクスが 9 種類あります。図 6 は S3 から読んだ量(青)とキャッシュから読んだ量(橙)です。S3 から読んだのは、再起動直後の計測の時間帯だけでした。計測中、メモリ不足でディスクへ書き出した量(AuroraAnalyticsDiskSpillSize)は 0 のままでした。

図 6: S3 から読んだ量とキャッシュから読んだ量(CloudWatch、日本時間 12:00〜12:35)

図 6: S3 から読んだ量とキャッシュから読んだ量(CloudWatch、日本時間 12:00〜12:35)

6. 考察

6.1. ビュー経由の全期間の集計はなぜ遅いか

型をそろえたビューで全期間の集計を実行すると、実行計画の最上位は PostgreSQL の Sort と HashAggregate でした。外部表の部分は Custom Scan として 53,136,807 行を PostgreSQL に返しており、ここまでで約 45 秒かかっています。集計は PostgreSQL 側で、約 6,000 万行を対象に実行していました。

 Sort  (cost=19292169.42..19292269.42 rows=40000 width=112) (actual time=75453.746..75453.750 rows=49.00 loops=1)
   ->  HashAggregate  (cost=17294468.50..19286784.38 rows=40000 width=112) (actual time=75453.649..75453.685 rows=49.00 loops=1)
         ->  Result  (cost=0.00..7228102.11 rows=59985796 width=105) (actual time=0.022..56462.893 rows=59986052.00 loops=1)
               ->  Append  (cost=0.00..6478279.66 rows=59985796 width=77) (actual time=0.019..46493.351 rows=59986052.00 loops=1)
                     ->  Seq Scan on lineitem_hot  (cost=0.00..200359.89 rows=6848989 width=27) (actual time=0.018..844.253 rows=6849245.00 loops=1)
                     ->  Custom Scan on lineitem_cold  (cost=100.00..5977990.79 rows=53136807 width=84) (actual time=16.709..40828.038 rows=53136807.00 loops=1)
                           ->  ICEBERG_SCAN  (cost=0.00..0.00 rows=53136807 width=36) (actual time=13.319..70729.980 rows=53136807.00 loops=1)
...(中略)...
 Execution Time: 75468.494 ms

同じ集計を、ビューを経由せず外部表だけに対して実行すると、最上位は Custom Scan 1 つになり、集計まで DuckDB 側で完了しました。PostgreSQL に返ったのは 42 行だけで、所要時間は 2.47 秒です。ネイティブ表の 685 万行だけを集計すると 3.51 秒でした。2 つの結果を合わせると、移す前の 49 行と値が一致しました。

Custom Scan  (cost=0.00..0.00 rows=33588868 width=48) (actual time=2452.729..2452.955 rows=42.00 loops=1)
  ->  ORDER_BY (actual time=2451.153..2451.153 rows=42.00 loops=1)
        ->  HASH_GROUP_BY  (cost=0.00..0.00 rows=33588868 width=48) (actual time=2450.968..2450.968 rows=42.00 loops=1)
              ->  ICEBERG_SCAN  (cost=0.00..0.00 rows=53136807 width=36) (actual time=7.131..2450.588 rows=53136807.00 loops=1)
...(中略)...
Execution Time: 2468.558 ms
 Sort  (cost=320383.09..320388.94 rows=2338 width=83) (actual time=3548.520..3548.521 rows=7.00 loops=1)
   ->  HashAggregate  (cost=320217.20..320252.27 rows=2338 width=83) (actual time=3548.469..3548.476 rows=7.00 loops=1)
         ->  Seq Scan on lineitem_hot  (cost=0.00..217482.36 rows=6848989 width=55) (actual time=0.014..1328.521 rows=6849245.00 loops=1)
...(中略)...
 Execution Time: 3548.620 ms

ドキュメントには、外部表とネイティブ表の JOIN も含めて、クエリ全体を DuckDB 側で実行できると書かれています。ただし、使う関数・型・照合順序がすべて対応している場合に限られます。ネイティブ表に character(25) の列があると、外部表の読み取りだけを DuckDB 側で実行する方式に切り替わる例が載っています16。

今回のビューでは、ネイティブ表が char(n) の列とデフォルトの照合順序(libc)の列を持っています。そのため Unsupported Pushdown Expressions に Collation(default)と Type(character(n))が並び、クエリ全体を DuckDB 側で実行する方式にならなかったと考えられます。ビューの外部表側の型をそろえても、ネイティブ表の列は変わらないので、集計は PostgreSQL で実行されます。遅くなる原因は、外部表から読んだ行を PostgreSQL に返して集計していることだと考えられます。

逆に、ネイティブ表の側を外部表に寄せる方法も考えられます。ドキュメントの例では、ネイティブ表の列が text で照合順序も対応していれば、ネイティブ表との JOIN を含むクエリ全体が DuckDB 側で実行されると書かれています。照合順序が原因のときは、比較の式に COLLATE "C" を付ける対処も示されています16。ネイティブ表の char(n) を text か varchar に、照合順序を C か ICU の照合順序(en-US-x-icu など)に変えれば、ビュー経由でも集計を DuckDB 側に渡せると考えられます。ただし、末尾の空白の扱いと ORDER BY の並びがアプリから見て変わるので、SQL を変えないという本記事の範囲からは外れます(今回は試していません)。DuckDB には固定長の文字列型がなく、CHAR と BPCHAR は VARCHAR の別名で、長さの指定は効きません17。データレイクに移す前提の表では、char(n) を避けて text や varchar にしておくと、末尾の空白や並び順の違いを減らせると考えられます。

6.2. クエリの種類による違い

直近の集計は 52 ミリ秒から 77 ミリ秒になり、増えたのは 25 ミリ秒でした。外部表側では日付の条件が Filters に入り、外部表から読んだ量は S3・キャッシュとも 0 kB でした。日付の条件で、Iceberg のファイルを読まずに済ませたと考えられます。直近の行を中心に読むアプリなら、古い行を移しても応答時間の増え方は数十ミリ秒の範囲に収まると考えられます。

顧客の履歴のように、索引で数行を読むクエリは 0.164 ミリ秒から 0.70 秒になりました。外部表の読み取りでは Aurora の索引を使えないため、条件を DuckDB 側で処理しても、1,367 万行の o_custkey 列を読む必要があったと考えられます。古い行を 1 件ずつ頻繁に参照する画面がある場合は、移す範囲を決める前にそのクエリを測っておく必要があります。

全期間を集計するクエリは、型をそろえたビューでも移す前の 2.5 倍かかりました。6.1 章の 2.47 秒と 3.51 秒を足しても移す前の 30.8 秒より短いので、集計を直近の行(ネイティブ表)と古い行(外部表)に分けて書けば、移す前より速くできると考えられます。ただし、これはアプリの SQL を書き換えることになるので、本記事の「同じ SQL」の範囲には入りません。

紹介ブログのようにビューを使わず、UNION ALL をクエリに直接書いて各 SELECT に条件を書く方法なら、各 SELECT の WHERE がそのまま Pushdown SQL に入ると考えられます(今回は測っていません)。

6.3. 今回の測定条件

数値は、TPC-H SF10、Serverless v2 を 16 ACU に固定、ストレージは Aurora Standard、1 セッションで順に実行した条件のものです。外部表の処理も同じインスタンスの CPU とメモリで動くので、ACU を変えれば数値も変わります。公式ドキュメントには、複数の分析クエリを同時に実行するとメモリ不足で取り消される場合や3、Serverless v2 の容量の拡大が追いつかない場合がある7と書かれています。今回は同時実行を試していません。

7. まとめ

  • 古い行の外部表と直近の行の表を UNION ALL のビューで元の表名につなぐと、アプリの SQL を変えずに読めた。SELECT * のビューでは末尾の空白と並び順が変わったが、外部表側の型と照合順序(COLLATE "default")をそろえたビューでは移す前と結果が一致した
  • 型をそろえたビューの所要時間は、顧客の履歴以外の 3 本で移す前の 1.4〜2.5 倍、顧客の履歴は 0.70 秒だった
  • 遅くなる主な原因は、外部表の行を PostgreSQL に返してから集計していることだと考えられる。外部表だけを集計する SQL では、5,314 万行で 2.47 秒だった
  • 実行計画の Pushdown SQL、Database Insights の待機イベント、CloudWatch の AuroraAnalytics メトリクスで、外部表の処理を確かめられた

古い行を移す前に、アプリがよく実行するクエリを、ビュー経由で 1 本ずつ実行しておくことをおすすめします。移す前と結果が一致するか、EXPLAIN (VERBOSE) の Pushdown SQL に WHERE が入っているかを見ると、結果が変わるクエリと遅くなるクエリを、移す前に見つけられます。

参考

  1. Aurora PostgreSQL now supports querying of Apache Iceberg and Parquet data(What's New、2026-09-30。対応は 17.11 以上と 18.6 以上) ↩

  2. How it works(PostgreSQL のサーバーに組み込んだ DuckDB に実行を任せる仕組み) ↩

  3. Resource management(ローカルストレージの読み取りキャッシュ。再起動で消える。メモリ不足時のクエリの取り消し) ↩ ↩2 ↩3

  4. Querying Apache Iceberg and Parquet data directly in Aurora PostgreSQL(Aurora ユーザーガイド。Use cases の Data tiering と、データレイクからの取り込み) ↩ ↩2

  5. Amazon Aurora PostgreSQL now supports direct querying of Apache Iceberg and Parquet data in your data lake(AWS News Blog。直近の表と Parquet の外部表を UNION ALL でつなぐ例) ↩

  6. Working with Amazon S3 Tables and table buckets(Amazon S3 ユーザーガイド。Iceberg の表を保存するテーブルバケット) ↩

  7. Limitations(読み取り専用、照合順序は C と ICU のみ、Serverless v2 の注意) ↩ ↩2 ↩3 ↩4

  8. Exporting query data using the aws_s3.query_export_to_s3 function(options は PostgreSQL の COPY の引数) ↩

  9. COPY(PostgreSQL 18 ドキュメント。FORMAT は text・csv・binary) ↩

  10. Working with foreign tables(外部表の OPTIONS と format の説明、IMPORT FOREIGN SCHEMA はカタログ(Glue・S3 Tables)が対象) ↩ ↩2

  11. IAM policies reference(S3 Tables を読むのに必要な 4 つのアクション) ↩

  12. Prerequisites(IAM ロールの紐付け、IMPORT FOREIGN SCHEMA に追加で必要な権限、aurora_analytics.enabled の設定) ↩ ↩2

  13. Character Types(PostgreSQL 18 ドキュメント。character 型の比較では末尾の空白が意味を持たない) ↩

  14. Data formats and type mapping(Aurora ユーザーガイド。Iceberg の型から PostgreSQL の型への対応表と、外部表の列に指定できる型。BPCHAR・VARCHAR は Partially supported types) ↩

  15. Monitoring and troubleshooting(CloudWatch のメトリクス、待機イベント Extension:AuroraAnalyticsExecute) ↩

  16. Execution plan(クエリ全体を DuckDB 側で実行する方式と読み取りだけを実行する方式、character(25) の例、Unsupported Pushdown Expressions) ↩ ↩2

  17. Text Types(DuckDB ドキュメント。CHAR・BPCHAR・STRING・TEXT は VARCHAR の別名で、長さ n は互換性のためだけにある) ↩

2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?