[アップデート]DSQLのEXPLAIN ANALYZE VERBOSEでDPUが確認できるようになりました

[アップデート]DSQLのEXPLAIN ANALYZE VERBOSEでDPUが確認できるようになりました

EXPLAIN ANALYZE VERBOSEの出力結果にDPUに関連する情報が出力されるようにになりました! CloudWatchのメトリクスや実際の課金とは差異が発生し得ますが、利用料金の見積もりにうまく活用していきましょう。
2025.11.21

リテールアプリ共創部@大阪の岩田です。

2025/11/20付けのアップデートでDSQLでステートメントレベルのコスト見積もりを提供するようになりました。

https://aws.amazon.com/about-aws/whats-new/2025/11/aurora-dsql-statement-level-cost-estimates-query-plans/

これによってEXPLAIN ANALYZE VERBOSEの出力から消費DPUが見積もりやすくなりました!従来も消費DPUはCloudWatchのメトリクスから確認可能でしたが、CloudWatchのメトリクスは最低でも1分単位の集計のため、どのSQLがどの程度DPUを消費するのかを直接的に確認することはできませんでした。以前DPU消費について検証したブログでもSQLを実行→しばらく何もせずに待ってメトリクスを確認 という作業を繰り返して消費DPUを確認していました。

https://dev.classmethod.jp/articles/investigate-dsql-dpu-with-heavy-query/

今回のアップデートによって今後はDPU消費を確認したいSQLの先頭にEXPLAIN ANALYZE VERBOSEを付与するだけで、簡単にDPU消費が見積もれるようになります。

やってみる

それではさっそくやっていきましょう。

以前のブログで利用したSQLを使って確認していきます。

CTEで100万レコード生成&ハッシュ値を計算して最大値を集計するSQL

まずCTEで100万レコード生成&ハッシュ値を計算して最大値を集計するSQLです。

EXPLAIN ANALYZE VERBOSE
WITH RECURSIVE v1 (n) AS (
    SELECT
        1
UNION ALL
    SELECT
        n + 1
    FROM
        v1
    WHERE
        n < 1000000
),
v2 AS (
    SELECT
        n,
        MD5(n::text) AS m
    FROM
        v1
)
SELECT
    max(m)
FROM
    v2;

結果は以下のとおりでした。

                                                        QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=3.58..3.58 rows=1 width=32) (actual time=1659.868..1659.870 rows=1 loops=1)
   Output: max(md5((v1.n)::text))
   CTE v1
     ->  Recursive Union  (cost=0.00..2.65 rows=31 width=4) (actual time=0.003..499.096 rows=1000000 loops=1)
           ->  Result  (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=1)
                 Output: 1
           ->  WorkTable Scan on v1 v1_1  (cost=0.00..0.23 rows=3 width=4) (actual time=0.000..0.000 rows=1 loops=1000000)
                 Output: (v1_1.n + 1)
                 Filter: (v1_1.n < 1000000)
                 Rows Removed by Filter: 0
   ->  CTE Scan on v1  (cost=0.00..0.62 rows=31 width=4) (actual time=0.004..702.343 rows=1000000 loops=1)
         Output: v1.n
 Query Identifier: et4rfg53geauw
 Planning Time: 0.379 ms
 Execution Time: 1664.285 ms
 Statement DPU Estimate:
   Compute: 1.75541 DPU
   Read: 0.00401 DPU
   Write: 0.00000 DPU
   Total: 1.75941 DPU
(20 rows)

Statement DPU Estimateとして以下の情報が出力されていました。

  • Compute: 1.75541 DPU
  • Read: 0.00401 DPU
  • Write: 0.00000 DPU
  • Total: 1.75941 DPU

前回のブログでCloudWatchメトリクスから確認した値にかなり近い値が出力されています。

https://dev.classmethod.jp/articles/investigate-dsql-dpu-with-heavy-query/

CROSS JOINで100万レコード生成後にハッシュ値を計算して最大値を集計するSQL

続いてはこちらです。pgbenchのテスト用テーブルを使ってコストの高そうなSQLを実行します。

EXPLAIN ANALYZE VERBOSE
SELECT max_aid FROM
(
    SELECT
        MAX(MD5((c.aid + 1)::text)) as max_aid
    FROM
        (SELECT * FROM pgbench_accounts LIMIT 100) a
    CROSS JOIN
        (SELECT * FROM pgbench_accounts LIMIT 100) b
    CROSS JOIN
        (SELECT * FROM pgbench_accounts LIMIT 100) c
) d

結果は以下の通りでした。

                                                                                                        QUERY PLAN                                                                                                         
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=56438.92..56438.93 rows=1 width=32) (actual time=1105.336..1105.340 rows=1 loops=1)
   Output: max(md5(((c.aid + 1))::text))
   ->  Nested Loop  (cost=31303.63..43938.92 rows=1000000 width=4) (actual time=3.988..185.652 rows=1000000 loops=1)
         Output: c.aid
         ->  Nested Loop  (cost=20869.08..21000.86 rows=10000 width=0) (actual time=2.763..5.926 rows=10000 loops=1)
               ->  Limit  (cost=10434.54..10436.81 rows=100 width=352) (actual time=1.444..2.479 rows=100 loops=1)
                     Output: NULL::integer, NULL::integer, NULL::integer, NULL::character(84)
                     ->  Index Only Scan using pgbench_accounts_pkey on public.pgbench_accounts  (cost=10434.54..12306.60 rows=82676 width=352) (actual time=1.443..2.467 rows=100 loops=1)
                           Output: NULL::integer, NULL::integer, NULL::integer, NULL::character(84)
                           -> Storage Scan on pgbench_accounts_pkey (cost=10434.54..12306.60 rows=82676 width=352) (actual rows=100 loops=1)
                               Projections: 
                               -> B-Tree Scan on pgbench_accounts_pkey (cost=10434.54..12306.60 rows=82676 width=352) (actual rows=196 loops=1)
               ->  Materialize  (cost=10434.54..10438.31 rows=100 width=0) (actual time=0.013..0.025 rows=100 loops=100)
                     ->  Subquery Scan on b  (cost=10434.54..10437.81 rows=100 width=0) (actual time=1.314..1.841 rows=100 loops=1)
                           ->  Limit  (cost=10434.54..10436.81 rows=100 width=352) (actual time=1.314..1.829 rows=100 loops=1)
                                 Output: NULL::integer, NULL::integer, NULL::integer, NULL::character(84)
                                 ->  Index Only Scan using pgbench_accounts_pkey on public.pgbench_accounts pgbench_accounts_1  (cost=10434.54..12306.60 rows=82676 width=352) (actual time=1.313..1.818 rows=100 loops=1)
                                       Output: NULL::integer, NULL::integer, NULL::integer, NULL::character(84)
                                       -> Storage Scan on pgbench_accounts_pkey (cost=10434.54..12306.60 rows=82676 width=352) (actual rows=100 loops=1)
                                           Projections: 
                                           -> B-Tree Scan on pgbench_accounts_pkey (cost=10434.54..12306.60 rows=82676 width=352) (actual rows=196 loops=1)
         ->  Materialize  (cost=10434.54..10438.31 rows=100 width=4) (actual time=0.000..0.006 rows=100 loops=10000)
               Output: c.aid
               ->  Subquery Scan on c  (cost=10434.54..10437.81 rows=100 width=4) (actual time=1.219..1.738 rows=100 loops=1)
                     Output: c.aid
                     ->  Limit  (cost=10434.54..10436.81 rows=100 width=352) (actual time=1.218..1.727 rows=100 loops=1)
                           Output: pgbench_accounts_2.aid, NULL::integer, NULL::integer, NULL::character(84)
                           ->  Index Only Scan using pgbench_accounts_pkey on public.pgbench_accounts pgbench_accounts_2  (cost=10434.54..12306.60 rows=82676 width=352) (actual time=1.217..1.717 rows=100 loops=1)
                                 Output: pgbench_accounts_2.aid, NULL::integer, NULL::integer, NULL::character(84)
                                 -> Storage Scan on pgbench_accounts_pkey (cost=10434.54..12306.60 rows=82676 width=352) (actual rows=100 loops=1)
                                     Projections: aid
                                     -> B-Tree Scan on pgbench_accounts_pkey (cost=10434.54..12306.60 rows=82676 width=352) (actual rows=196 loops=1)
 Query Identifier: c8uzj5jezt3o7
 Planning Time: 0.135 ms
 Execution Time: 1105.408 ms
 Statement DPU Estimate:
   Compute: 1.10590 DPU
   Read: 0.00000 DPU
   Write: 0.00000 DPU
   Total: 1.10590 DPU
(40 rows)

DPU Estimateは以下の通りでした。

  • Compute: 1.10590 DPU

  • Read: 0.00000 DPU

  • Write: 0.00000 DPU

  • Total: 1.10590 DPU

Read DPUが0なのはなぜでしょうね??何度か繰り返したのですが、Compute DPUの微妙なブレは発生したもののRead DPUはずっと0が続きました。

INSERT文/UPDATE文

これまでSELECT文を実行してきたのでINSERT文とUPDATE文も実行しておきましょう。

EXPLAIN ANALYZE VERBOSE
INSERT INTO public.pgbench_accounts VALUES (100001, 1, 0, '                                                                                    ');

結果は以下の通りでした。

                                                             QUERY PLAN                                                              
-------------------------------------------------------------------------------------------------------------------------------------
 Insert on public.pgbench_accounts  (cost=0.00..0.01 rows=0 width=0) (actual time=0.076..0.076 rows=0 loops=1)
   ->  Result  (cost=0.00..0.01 rows=1 width=352) (actual time=0.001..0.002 rows=1 loops=1)
         Output: 100001, 1, 0, '                                                                                    '::character(84)
 Query Identifier: nnjodipidhhuf
 Planning Time: 0.035 ms
 Execution Time: 1.197 ms
 Statement DPU Estimate:
   Compute: 0.00136 DPU
   Read: 0.00023 DPU (Transaction minimum: 0.00375)
   Write: 0.00625 DPU (Transaction minimum: 0.05000)
   Total: 0.00785 DPU
(11 rows)

続いてUPDATE文

EXPLAIN ANALYZE VERBOSE
UPDATE public.pgbench_accounts SET filler='abc' WHERE aid =100001;

結果は以下の通りでした。

                                                                        QUERY PLAN                                                                         
-----------------------------------------------------------------------------------------------------------------------------------------------------------
 Update on public.pgbench_accounts  (cost=100.29..208.29 rows=0 width=0) (actual time=2.871..2.872 rows=0 loops=1)
   ->  Index Scan using pgbench_accounts_pkey on public.pgbench_accounts  (cost=100.29..208.29 rows=1 width=346) (actual time=1.984..1.987 rows=1 loops=1)
         Output: 'abc                                                                                 '::character(84), ctid
         Index Cond: (pgbench_accounts.aid = 100001)
         -> Storage Scan on pgbench_accounts_pkey (cost=100.29..208.29 rows=1 width=346) (actual rows=1 loops=1)
             -> B-Tree Scan on pgbench_accounts_pkey (cost=100.29..208.29 rows=1 width=346) (actual rows=1 loops=1)
                 Index Cond: (pgbench_accounts.aid = 100001)
         -> Storage Lookup on pgbench_accounts (cost=100.29..208.29 rows=1 width=346) (actual rows=1 loops=1)
             Projections: 
             -> B-Tree Lookup on pgbench_accounts (cost=100.29..208.29 rows=1 width=346) (actual rows=1 loops=1)
 Query Identifier: rh8w8r5hmkr8f
 Planning Time: 10.654 ms
 Execution Time: 5.068 ms
 Statement DPU Estimate:
   Compute: 0.01725 DPU
   Read: 0.00234 DPU (Transaction minimum: 0.00375)
   Write: 0.00625 DPU (Transaction minimum: 0.05000)
   Total: 0.02585 DPU
(18 rows)

さて、INSERT文とUPDATE文のEXPAIN ANALYZE VERBOSE結果を見るとRead DPUとWrite DPUの出力に(Transaction minimum: 0.00375)のような出力が追加されているのが分かります。この表示についてドキュメントには以下のように記載されています。

Notice the Transaction minimums shown next to Read and Write DPUs. These indicate the baseline per-transaction costs that apply only when the operation includes reads or writes. They do not mean that every transaction automatically incurs a 0.00375 Read DPU or 0.05 Write DPU charge. Instead, these minimums are applied at the transaction level during cost aggregation and only if reads or writes occur within that transaction. Because of this difference in scope, statement-level estimates in EXPLAIN ANALYZE VERBOSE may not exactly match the transaction-level metrics reported in CloudWatch or billing data.

https://docs.aws.amazon.com/ja_jp/aurora-dsql/latest/userguide/understanding-dpus-explain-analyze.html#dpu-usage-explain-analyze

この値はトランザクションごとのベースラインコストで、対象トランザクション内で読み取りまたは書き込みが発生した場合に適用されるとのことです。

EXPLAIN ANALYZE VERBOSEによって表示されるDPUの消費はステートメントレベルのDPUですが、ベースラインコストはトランザクション単位のため、トランザクション内で複数のDMLを発行する場合はEXPLAIN ANALYZE VERBOSEの結果とCloudWatchのメトリクスや請求に差異が発生するようです。この挙動も確認しておきましょう。

検証のため以下のトランザクションを実行してみます。

BEGIN;

EXPLAIN ANALYZE VERBOSE
INSERT INTO public.pgbench_accounts VALUES (100002, 1, 0, '');
   Compute: 0.07460 DPU
   Read: 0.01010 DPU
   Write: 0.00625 DPU (Transaction minimum: 0.05000)
   Total: 0.09095 DPU
   
EXPLAIN ANALYZE VERBOSE
UPDATE public.pgbench_accounts SET filler='1' WHERE aid =100002;

EXPLAIN ANALYZE VERBOSE
UPDATE public.pgbench_accounts SET filler='2' WHERE aid =100002;
   
EXPLAIN ANALYZE VERBOSE
UPDATE public.pgbench_accounts SET filler='3' WHERE aid =100002;
  
EXPLAIN ANALYZE VERBOSE
UPDATE public.pgbench_accounts SET filler='4' WHERE aid =100002;

EXPLAIN ANALYZE VERBOSE
UPDATE public.pgbench_accounts SET filler='5' WHERE aid =100001;

COMMIT;

出力結果をまとめると以下のようになります。

実行したSQL Compute DPU Read DPU Write DPU Total DPU
INSERT 0.07460 DPU 0.01010 DPU 0.00625 DPU (Transaction minimum: 0.05000) 0.09095 DPU
1回目のUPDATE 0.04240 DPU 0.01510 DPU 0.00000 DPU 0.05750 DPU
2回目のUPDATE 0.00229 DPU 0.00047 DPU (Transaction minimum: 0.00375) 0.00000 DPU 0.00275 DPU
3回目のUPDATE 0.00221 DPU 0.00047 DPU (Transaction minimum: 0.00375) 0.00000 DPU 0.00268 DPU
4回目のUPDATE 0.00219 DPU 0.00047 DPU (Transaction minimum: 0.00375) 0.00000 DPU 0.00266 DPU
5回目のUPDATE 0.00375 DPU 0.00094 DPU (Transaction minimum: 0.00375) 0.00625 DPU (Transaction minimum: 0.05000) 0.01094 DPU
合計
Transaction minimum:...は除いて計算
0.12744 DPU 0.02755 DPU 0.0125 DPU 0.16748 DPU

Read DPU/Write DPUともに Transaction minimum: ...が出力されるとき、されないときがあるようですね。

初回のINSERT文ではWrite DPUにだけTransaction minimum:..が出力されています。Total DPUは0.09095なので

0.07460(Compute DPU) + 0.01010(Read DPU) + 0.00625 (Write DPU) = 0.09095(Total DPU)という計算式が成立し、このうち0.05000がトランザクションのベースラインコストのようです。Transaction minimum: ...の出力はWrite DPUの欄に出力されていましたが、Write DPUは0.00625 DPUとベースラインよりも小さな値になっていました。これはどう解釈すれば良いのでしょうかね?

UPDATE文に関してはTransaction minimum: ...が表示されたりされなかったりですがCompute DPU + Read DPU + Write DPU = Total DPUという計算式は成立しています。
うーん。。。。結局ベースラインコストの考え方はイマイチ理解できませんでした。

上記トランザクションを実行した時間帯のメトリクスをCloudWatchから確認するとメトリクスは以下の通りでした。

CloudWatchから確認したDPU関連メトリクス

EXPLAIN ANALYZE VERBOSEの出力値(Transaction minimum: ...の値を除く)合計とCloudWatchのメトリクスを比較すると以下のようになります。

項目 EXPLAIN ANALYZE VERBOSEの出力値合計 CloudWatchのメトリクス
Compute DPU 0.12744 39.483219
Read DPU 0.02755 0.02755004883
Write DPU 0.0125 0.05
Total DPU 0.16748 39.5607690488

全体的にCloudWatchメトリクスの方が大きいですね。

これはドキュメントにも記載されているようにバックグラウンドのオペレーションやBEGIN/COMMITによるDPU消費も計上されるためです。

CloudWatch also includes background operations (such as ANALYZE or compactions) and transaction overhead (BEGIN/COMMIT) that EXPLAIN ANALYZE VERBOSE intentionally excludes.

今回実行したトランザクションは簡易なものでありANALYZE等のオペレーションが実行されるレベルでは無いはずです。そのためRead DPUについては差異が小さく、COMMIT処理に消費した分だけCompute DPUやWrite DPUの差異が発生していると読み取りました。これ以上の詳細については分からないですが、EXPLAIN ANALYZE VERBOSEの出力値はあくまでステートメントレベルのDPU消費の目安であると認識しておかないと、実際の課金との差異にビックリしてしまうかもしれません。しっかりと認識しておきましょう。

まとめ

DSQLの利用料金を見積もるにあたって、これまではDPUに対する利用料金が読みづらいという問題がありましたが、今回のアップデートによって大きく改善されました。DSQL採用を検討する際に有効活用していきましょう!

参考

この記事をシェアする

FacebookHatena blogX

関連記事