[アップデート] Amazon Redshift Data API の long polling、list session、flexible batch execution を試してみた

[アップデート] Amazon Redshift Data API の long polling、list session、flexible batch execution を試してみた

Amazon Redshift Data API に追加されたロングポーリング、セッション一覧取得、柔軟なバッチ実行の3つの新機能を、実際に試してみました。
2026.07.30

クラウド事業統括本部の石川です。Amazon Redshift Data API に long polling、list session、flexible batch execution が追加されましたので、実際に試してみました。

https://aws.amazon.com/jp/about-aws/whats-new/2026/07/amazon-redshift-data-api-longpolling-listsession-flexiblebatchexecute/

Amazon Redshift Data API は、JDBC/ODBC ドライバーや永続的な接続を管理せずに、HTTPS ベースの API 呼び出しで Amazon Redshift に SQL を実行できる機能です。AWS Lambda や AWS Step Functions から Amazon Redshift を扱う際によく使われます。

この Data API は非同期モデルが基本でした。ExecuteStatement で SQL を投入するとステートメント ID だけが返り、DescribeStatement を繰り返し呼び出して完了を待ち、GetStatementResult で結果を取得する、という流れです。このポーリングループは呼び出し側で実装する必要がありました。

2026 年 7 月 29 日のアップデートで、この定型処理を API 側に寄せる 3 つの機能が追加されました。ロングポーリング(WaitTimeSeconds)、セッション一覧の取得(ListSessions)、柔軟なバッチ実行(ExecutionMode と共通パラメータ)です。Amazon Redshift のプロビジョニングされたクラスターおよび Amazon Redshift Serverless の両方で、Data API が利用可能なすべての AWS 商用リージョンおよび AWS GovCloud (US) リージョンで一般提供されています。

https://docs.aws.amazon.com/redshift/latest/mgmt/data-api.html

3 つの新機能とは

ロングポーリング(Long polling)

ロングポーリングは、WaitTimeSeconds に 1〜30 秒の値を指定すると、ステートメントが終了するか待機時間に達するかのいずれか早い方まで、API がレスポンスの返却を遅延させる仕組みです。対応するオペレーションは ExecuteStatementBatchExecuteStatementDescribeStatementGetStatementResultGetStatementResultV2 の 5 つです。

https://docs.aws.amazon.com/redshift/latest/mgmt/data-api-calling-considerations-long-polling.html

セッション一覧取得・管理(ListSessions)

ListSessions は、呼び出し元が過去 24 時間に作成したセッションを一覧する API です。既定では AVAILABLE または BUSY のセッションが返り、ステータス・コンピュート対象・データベースでフィルタリングできます。

https://docs.aws.amazon.com/redshift-data/latest/APIReference/API_ListSessions.html

柔軟なバッチ実行(Flexible batch execution)

柔軟なバッチ実行は、BatchExecuteStatement に追加された ExecutionMode パラメータです。既定の TRANSACTION はすべての SQL を単一トランザクションとして扱いますが、AUTO_COMMIT を指定すると各 SQL が個別にコミットされます。あわせて ParametersSqlParameter の配列)に対応し、バッチ内の複数ステートメントで同じパラメータを共有できるようになりました。

https://docs.aws.amazon.com/redshift-data/latest/APIReference/API_BatchExecuteStatement.html

やってみた

前提条件

  • リージョン: 東京(ap-northeast-1)
  • AWS CLI: aws-cli/2.36.9
  • 検証環境: Amazon Redshift Serverless(base capacity 4 RPU)
  • 認証: IAM の一時的な認証情報(--workgroup-name--database の指定のみ。DB ユーザーは IAM アイデンティティ)

本記事の実行結果は実際の出力をそのまま掲載していますが、AWS アカウント ID は 123456789012、IAM ロール由来の DB ユーザー名は IAMR:your-role に置き換えています。ステータス・時刻・実行時間・行数・エラーメッセージは実際の値です。

なお本記事では API 呼び出しの経過時間を計測しています。計測は以下の形で行い、以降のステップでは elapsed: の行として結果を併記します。

S=$(python3 -c 'import time;print(time.time())')
aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT 1 AS test" \
  --wait-time-seconds 30
E=$(python3 -c 'import time;print(time.time())')
python3 -c "print(f'elapsed: {$E-$S:.2f}s')"

検証用テーブルの作成

1. 検証用データの作成(注文・商品テーブル)

-- 注文テーブルの作成とテストデータ投入
CREATE TABLE public.blog_orders (
    order_id INT,
    load_date DATE,
    amount DECIMAL(10,2)
);

INSERT INTO public.blog_orders VALUES 
    (1, '2026-07-29', 100.00),
    (2, '2026-07-29', 200.00),
    (3, '2026-07-30', 300.00);

-- 商品テーブルの作成とテストデータ投入
CREATE TABLE public.blog_items (
    item_id INT,
    load_date DATE,
    qty INT
);

INSERT INTO public.blog_items VALUES 
    (1, '2026-07-29', 5),
    (2, '2026-07-29', 3),
    (3, '2026-07-30', 7);

2. 大規模テストデータの作成(100万行)

-- シード用データ(1〜10の数値)の生成
CREATE TABLE public.blog_seed AS 
SELECT 1 AS x 
UNION ALL SELECT 2 
UNION ALL SELECT 3 
UNION ALL SELECT 4 
UNION ALL SELECT 5 
UNION ALL SELECT 6 
UNION ALL SELECT 7 
UNION ALL SELECT 8 
UNION ALL SELECT 9 
UNION ALL SELECT 10;

-- シードテーブルを6回クロス結合(10^6 = 1,000,000行)して100万行のテーブルを作成
CREATE TABLE public.blog_big AS 
SELECT 
    a.x + 
    b.x * 10 + 
    c.x * 100 + 
    d.x * 1000 + 
    e.x * 10000 + 
    f.x * 100000 AS n 
FROM 
    public.blog_seed a, 
    public.blog_seed b, 
    public.blog_seed c, 
    public.blog_seed d, 
    public.blog_seed e, 
    public.blog_seed f;

3. 作成した大規模データの件数確認

-- 作成された行数のカウント(結果: 1,000,000行)
SELECT COUNT(*) FROM public.blog_big;

4. バッチ比較検証用テーブルの作成

-- トランザクション検証用テーブル
CREATE TABLE public.batch_tx_test (
    id INT, 
    note VARCHAR(20)
);

-- オートコミット/個別処理検証用テーブル
CREATE TABLE public.batch_ac_test (
    id INT, 
    note VARCHAR(20)
);

5. クエリキャッシュの無効化

ALTER USER "IAMR:cm-user" SET enable_result_cache_for_session TO off;

従来の非同期実行を確認する

比較のため、まず WaitTimeSeconds を指定しない従来の挙動を確認します。

% time aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT 1 AS test"
{
    "Id": "f3bb2899-af18-4f6d-b21c-9b18a871f390",
    "CreatedAt": "2026-07-30T20:27:45.967000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg"
}
aws redshift-data execute-statement --workgroup-name dataapi-blog-wg  dev    0.21s user 0.10s system 29% cpu 1.058 total

ステートメント ID は返りますが、Status は含まれていません。SELECT 1 のような一瞬で終わるクエリであっても、完了を知るには DescribeStatement を別途呼ぶ必要があります。

ロングポーリングを試す

同じ SQL に --wait-time-seconds 30 を付けて実行します。

% time aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT 1 AS test" \
  --wait-time-seconds 30
{
    "Id": "f1afa90d-c749-417a-83a8-3e91d10f70ab",
    "CreatedAt": "2026-07-30T20:28:12.374000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "FINISHED",
    "RedshiftPid": 1073832146,
    "HasResultSet": true
}
aws redshift-data execute-statement --workgroup-name dataapi-blog-wg  dev      0.23s user 0.09s system 25% cpu 1.232 total

StatusRedshiftPidHasResultSet の 3 つが追加されました。1 回の呼び出しで FINISHED まで確認できています。

ロングポーリングが実際に待つことを確認する

SELECT 1 では速すぎて「待った」のかどうかが分かりません。そこで先ほど作った 100 万行のテーブルを 10 行のシードテーブルと 2 回クロス結合し、1 億行に対して md5 を計算するクエリで測定しました。

% time aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar || c.x::varchar)) FROM public.blog_big a, public.blog_seed b, public.blog_seed c" \
  --wait-time-seconds 30
{
    "Id": "d83406a7-55df-4214-858c-59aff6e35ff4",
    "CreatedAt": "2026-07-30T20:28:42.774000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "FINISHED",
    "RedshiftPid": 1073897683,
    "HasResultSet": true
}
aws redshift-data execute-statement --workgroup-name dataapi-blog-wg  dev      0.23s user 0.09s system 2% cpu 12.368 total

API 呼び出しが 12.368 秒間保留され、FINISHED が返りました。サーバー側の実行時間を DescribeStatement で確認します。

% aws redshift-data describe-statement --id d83406a7-55df-4214-858c-59aff6e35ff4
{
    "Id": "d83406a7-55df-4214-858c-59aff6e35ff4",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "Duration": 10943844591,
    "Status": "FINISHED",
    "CreatedAt": "2026-07-30T20:28:42.774000+09:00",
    "UpdatedAt": "2026-07-30T20:28:54.367000+09:00",
    "RedshiftPid": 1073897683,
    "HasResultSet": true,
    "QueryString": "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar || c.x::varchar)) FROM public.blog_big a, public.blog_seed b, public.blog_seed c",
    "ResultRows": 1,
    "ResultSize": 20,
    "RedshiftQueryId": 1837,
    "WorkgroupName": "dataapi-blog-wg",
    "ResultFormat": "json"
}

Duration は 10943844591 ナノ秒、つまり 10.94 秒です。クエリの実行時間ぶんだけ API が待ち、完了と同時にステータスを返していることが分かります。従来であればこの 11 秒間に DescribeStatement を何度も呼ぶ必要がありました。

ここで一点注意があります。同じクエリをもう一度 --wait-time-seconds 5 で実行したところ、待機時間を待たずに完了しました。Amazon Redshift の結果キャッシュが効いたためで、10.94 秒かかっていたクエリが実質ゼロ秒で返っています。ロングポーリングの挙動を確かめたい場合は、クエリ文字列を変えて計測する必要があります。

待機時間が切れたときの挙動を確認する

WaitTimeSeconds の上限は 30 秒です。それを超える処理ではどうなるかを確認します。結果キャッシュを避けるため、先ほどのクエリに 'v13' という文字列を足した別のクエリを使い、--wait-time-seconds 5 を指定しました。

% time aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar || c.x::varchar || 'v13')) FROM public.blog_big a, public.blog_seed b, public.blog_seed c" \
  --wait-time-seconds 5
{
    "Id": "a4940560-6e1a-4d82-98d1-ff8b52e3c185",
    "CreatedAt": "2026-07-30T20:44:03.030000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "STARTED",
    "RedshiftPid": 1073963131,
    "HasResultSet": false
}
aws redshift-data execute-statement --workgroup-name dataapi-blog-wg  dev      0.21s user 0.11s system 6% cpu 5.038 total

ちょうど 5.038 秒で戻り、実行中を示す STARTED が返りました。続いて DescribeStatement にロングポーリングを指定し、残りを待ちます。

% time aws redshift-data describe-statement \
  --id a4940560-6e1a-4d82-98d1-ff8b52e3c185 \
  --wait-time-seconds 30
{
    "Id": "a4940560-6e1a-4d82-98d1-ff8b52e3c185",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "Duration": 10949178655,
    "Status": "FINISHED",
    "CreatedAt": "2026-07-30T20:44:03.030000+09:00",
    "UpdatedAt": "2026-07-30T20:44:14.590000+09:00",
    "RedshiftPid": 1073963131,
    "HasResultSet": true,
    "QueryString": "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar || c.x::varchar || 'v13')) FROM public.blog_big a, public.blog_seed b, public.blog_seed c",
    "ResultRows": 1,
    "ResultSize": 20,
    "RedshiftQueryId": 203141,
    "WorkgroupName": "dataapi-blog-wg",
    "ResultFormat": "json"
}
aws redshift-data describe-statement --id a4940560-6e1a-4d82-98d1-ff8b52e3c18  0.23s user 0.09s system 52% cpu 0.614 total

5.038 秒 + 0.614 秒 = 約 5.65 秒とコマンド実行する間の5秒を加えると、サーバー側の実行時間の約 11 秒とおおむね一致します。30 秒を超える処理でも、待機時間切れのレスポンスを受けたら同じステートメント ID で再度ロングポーリングすればよい、という流れになります。

--wait-time-seconds をさらに短く 1 秒にした場合も確認しました。こちらは 100 万行と 10 行を 1 回だけ結合する、より軽いクエリです。

% time aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar)) FROM public.blog_big a, public.blog_seed b" \
  --wait-time-seconds 1
{
    "Id": "3d9b375e-0cf7-4de9-8a91-1deda1af19b8",
    "CreatedAt": "2026-07-30T20:52:39.798000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "PICKED",
    "HasResultSet": false
}
aws redshift-data execute-statement --workgroup-name dataapi-blog-wg  dev      0.23s user 0.09s system 31% cpu 1.024 total

PICKED(実行対象として選択された段階)が返りました。このとき RedshiftPid は含まれていません。まだプロセスが割り当てられていない段階のためと考えられます。

このステートメントを DescribeStatement で追いかけると、既に完了していました。

% time aws redshift-data describe-statement --id 3d9b375e-0cf7-4de9-8a91-1deda1af19b8 --wait-time-seconds 30
{
    "Id": "3d9b375e-0cf7-4de9-8a91-1deda1af19b8",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "Duration": 1252004741,
    "Status": "FINISHED",
    "CreatedAt": "2026-07-30T20:52:39.798000+09:00",
    "UpdatedAt": "2026-07-30T20:52:41.648000+09:00",
    "RedshiftPid": 1073971314,
    "HasResultSet": true,
    "QueryString": "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar)) FROM public.blog_big a, public.blog_seed b",
    "ResultRows": 1,
    "ResultSize": 20,
    "RedshiftQueryId": 203348,
    "WorkgroupName": "dataapi-blog-wg",
    "ResultFormat": "json"
}
aws redshift-data describe-statement --id 3d9b375e-0cf7-4de9-8a91-1deda1af19b  0.22s user 0.11s system 46% cpu 0.719 total

実行時間は 1.25 秒でした。1 秒しか待たない設定では、この程度のクエリでも待機時間切れになります。また、既に完了しているステートメントに対する呼び出しは 0.719 秒で即座に返っており、公式ドキュメントの「すでに完了していた場合は即座に終了ステータスを返す」という記述と一致します。

結果取得もロングポーリングできる

GetStatementResult にも WaitTimeSeconds を指定できます。ExecuteStatement で投入した直後に、DescribeStatement を挟まずに結果取得を呼んでみます。

% ID=$(aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT load_date, COUNT(*) AS cnt, SUM(amount) AS total FROM public.blog_orders GROUP BY load_date ORDER BY load_date" \
  --query 'Id' --output text)

time aws redshift-data get-statement-result --id "${ID}" --wait-time-seconds 30
{
    "Records": [
        [
            {
                "stringValue": "2026-07-29"
            },
            {
                "longValue": 2
            },
            {
                "stringValue": "300.00"
            }
        ],
        [
            {
                "stringValue": "2026-07-30"
            },
            {
                "longValue": 1
            },
            {
                "stringValue": "300.00"
            }
        ]
    ],
    "ColumnMetadata": [
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": false,
            "label": "load_date",
            "name": "load_date",
            "nullable": 1,
            "precision": 13,
            "scale": 0,
            "schemaName": "public",
            "tableName": "blog_orders",
            "typeName": "date",
            "length": 0
        },
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": true,
            "label": "cnt",
            "name": "cnt",
            "nullable": 1,
            "precision": 19,
            "scale": 0,
            "schemaName": "",
            "tableName": "",
            "typeName": "int8",
            "length": 0
        },
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": true,
            "label": "total",
            "name": "total",
            "nullable": 1,
            "precision": 38,
            "scale": 2,
            "schemaName": "",
            "tableName": "",
            "typeName": "numeric",
            "length": 0
        }
    ],
    "TotalNumRows": 2
}
aws redshift-data get-statement-result --id "${ID}" --wait-time-seconds 30  0.21s user 0.08s system 32% cpu 0.915 total

投入と結果取得の 2 回の呼び出しだけで結果が得られました。ステータス確認のための呼び出しが不要になっています。

ListSessions を試す

まず --session-keep-alive-seconds でセッションを維持し、一時テーブルを作成します。

% aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "CREATE TEMP TABLE session_scratch (k VARCHAR(20), v INT)" \
  --session-keep-alive-seconds 300 \
  --wait-time-seconds 30
{
    "Id": "3e5c64ed-ab2a-4d24-8b0f-a576b7f2318a",
    "CreatedAt": "2026-07-30T21:47:18.053000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "SessionId": "39012909-75bb-4187-a1ec-b75873f7d593",
    "Status": "FINISHED",
    "RedshiftPid": 1073832059,
    "HasResultSet": false
}

この状態で list-sessions を実行します。

% aws redshift-data list-sessions --workgroup-name dataapi-blog-wg
{
    "Sessions": [
        {
            "SessionId": "39012909-75bb-4187-a1ec-b75873f7d593",
            "Status": "AVAILABLE",
            "CreatedAt": "2026-07-30T21:47:17.859000+09:00",
            "UpdatedAt": "2026-07-30T21:47:18.583000+09:00",
            "Database": "dev",
            "DbUser": "IAMR:cm-user",
            "WorkgroupName": "dataapi-blog-wg",
            "SessionAliveSeconds": 300,
            "SessionTtl": "2026-07-30T21:52:18+09:00"
        }
    ]
}

SessionTtl としてセッションの期限まで返ってきました。これまではアプリケーション側で SessionId を保持しておく必要がありましたが、後から一覧できるようになっています。

取得した SessionId を指定して、一時テーブルが引き継がれているか確認します。

% aws redshift-data execute-statement \
  --session-id 39012909-75bb-4187-a1ec-b75873f7d593 \
  --sql "INSERT INTO session_scratch VALUES ('alpha', 1), ('beta', 2)" \
  --wait-time-seconds 30 --query 'Status' --output text
FINISHED
% RES=$(aws redshift-data execute-statement \
  --session-id 39012909-75bb-4187-a1ec-b75873f7d593 \
  --sql "SELECT k, v FROM session_scratch ORDER BY k" \
  --wait-time-seconds 30 --query 'Id' --output text)

aws redshift-data get-statement-result --id "${RES}" --wait-time-seconds 30
{
    "Records": [
        [
            {
                "stringValue": "alpha"
            },
            {
                "longValue": 1
            }
        ],
        [
            {
                "stringValue": "beta"
            },
            {
                "longValue": 2
            }
        ]
    ],
    "ColumnMetadata": [
        {
            "isCaseSensitive": true,
            "isCurrency": false,
            "isSigned": false,
            "label": "k",
            "name": "k",
            "nullable": 1,
            "precision": 20,
            "scale": 0,
            "schemaName": "pg_temp_8",
            "tableName": "session_scratch",
            "typeName": "varchar",
            "length": 0
        },
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": true,
            "label": "v",
            "name": "v",
            "nullable": 1,
            "precision": 10,
            "scale": 0,
            "schemaName": "pg_temp_8",
            "tableName": "session_scratch",
            "typeName": "int4",
            "length": 0
        }
    ],
    "TotalNumRows": 2
}

セッションを跨いで一時テーブルが参照できました。ColumnMetadataschemaNamepg_temp_14 となっており、セッション固有の一時スキーマに作られていることも分かります。

次に、クエリを実行中の状態で list-sessions を呼んでみます。重いクエリを非同期で投入し、すぐに一覧を取得します。

% aws redshift-data execute-statement \
  --session-id 39012909-75bb-4187-a1ec-b75873f7d593 \
  --sql "SELECT COUNT(DISTINCT md5(a.n::varchar || b.x::varchar || c.x::varchar || 'v17')) FROM public.blog_big a, public.blog_seed b, public.blog_seed c" \
  --query 'Id' --output text

78537eb3-3b95-4217-9893-1015249f7c4f

% aws redshift-data list-sessions --workgroup-name dataapi-blog-wg
{
    "Sessions": [
        {
            "SessionId": "39012909-75bb-4187-a1ec-b75873f7d593",
            "Status": "AVAILABLE",
            "CreatedAt": "2026-07-30T21:47:17.859000+09:00",
            "UpdatedAt": "2026-07-30T21:50:23.151000+09:00",
            "Database": "dev",
            "DbUser": "IAMR:cm-user",
            "WorkgroupName": "dataapi-blog-wg",
            "SessionAliveSeconds": 300,
            "SessionTtl": "2026-07-30T21:55:23+09:00"
        }
    ]
}

StatusBUSY になり、CurrentStatementId に投入したステートメント ID がそのまま入っていました。どのセッションがどのクエリで塞がっているかが分かるため、セッションが枯渇したときの調査に使えそうです。

--session-id を指定すると、そのセッションだけを取得できます。

% aws redshift-data list-sessions --session-id 39012909-75bb-4187-a1ec-b75873f7d593
{
    "Sessions": [
        {
            "SessionId": "39012909-75bb-4187-a1ec-b75873f7d593",
            "Status": "AVAILABLE",
            "CreatedAt": "2026-07-30T21:47:17.859000+09:00",
            "UpdatedAt": "2026-07-30T21:51:09.723000+09:00",
            "Database": "dev",
            "DbUser": "IAMR:cm-user",
            "WorkgroupName": "dataapi-blog-wg",
            "SessionAliveSeconds": 300,
            "SessionTtl": "2026-07-30T21:55:23+09:00"
        }
    ]
}

先ほどのクエリが終わったため AVAILABLE に戻り、CurrentStatementId も消えています。

フィルタ条件には制約があります。--session-id は他のフィルタと併用できません。

% aws redshift-data list-sessions --session-id 39012909-75bb-4187-a1ec-b75873f7d593 --status AVAILABLE

aws: [ERROR]: An error occurred (ValidationException) when calling the ListSessions operation: SessionId cannot be specified with other filter parameters.

次に CLOSED のセッションも取得できるか確認します。Data API にはセッションを明示的に閉じる操作がないため、--session-keep-alive-seconds 1 で短命なセッションを作り、TTL が切れるのを待ちました。

% aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT 'short-lived session' AS note" \
  --session-keep-alive-seconds 1 --wait-time-seconds 30 \
  --query 'SessionId' --output text

d22dd9dc-ffb5-44c5-b0c1-9fb74d260af9
% sleep 25
aws redshift-data list-sessions --workgroup-name dataapi-blog-wg --status CLOSED
{
    "Sessions": [
        {
            "SessionId": "d22dd9dc-ffb5-44c5-b0c1-9fb74d260af9",
            "Status": "CLOSED",
            "CreatedAt": "2026-07-30T21:53:02.796000+09:00",
            "UpdatedAt": "2026-07-30T21:53:17.410000+09:00",
            "Database": "dev",
            "DbUser": "IAMR:cm-user",
            "WorkgroupName": "dataapi-blog-wg",
            "SessionAliveSeconds": 1,
            "SessionTtl": "2026-07-30T21:53:04+09:00"
        }
    ]
}

既定では返らない CLOSED も、明示的に指定すれば取得できました。

柔軟なバッチ実行を試す

ここが今回もっとも挙動の差が大きかった部分です。同じ 3 文のバッチを TRANSACTION(既定)と AUTO_COMMIT で実行して比較します。2 番目の文はゼロ除算で意図的に失敗させています。

まず既定の TRANSACTION です。

% aws redshift-data batch-execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sqls "INSERT INTO public.batch_tx_test VALUES (1, 'first')" \
         "INSERT INTO public.batch_tx_test VALUES (1/0, 'boom')" \
         "INSERT INTO public.batch_tx_test VALUES (3, 'third')" \
  --wait-time-seconds 30
{
    "Id": "b32c3c58-3ffc-4e0b-9faa-74e50721f8b8",
    "CreatedAt": "2026-07-30T21:56:18.770000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "FAILED",
    "RedshiftPid": 1073938530,
    "HasResultSet": false
}

ロングポーリングのおかげで、投入と同じ呼び出しで FAILED が判明しました。サブステートメントの状態を確認します。

% aws redshift-data describe-statement --id b32c3c58-3ffc-4e0b-9faa-74e50721f8b8
{
    "Id": "b32c3c58-3ffc-4e0b-9faa-74e50721f8b8",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "Duration": 233169010,
    "Error": "Query #2 failed with ERROR: division by zero",
    "Status": "FAILED",
    "CreatedAt": "2026-07-30T21:56:18.770000+09:00",
    "UpdatedAt": "2026-07-30T21:56:20.018000+09:00",
    "RedshiftPid": 1073938530,
    "HasResultSet": false,
    "ResultRows": -1,
    "ResultSize": -1,
    "RedshiftQueryId": 0,
    "SubStatements": [
        {
            "Id": "b32c3c58-3ffc-4e0b-9faa-74e50721f8b8:1",
            "Duration": 233169010,
            "Status": "FINISHED",
            "CreatedAt": "2026-07-30T21:56:18.999000+09:00",
            "UpdatedAt": "2026-07-30T21:56:19.977000+09:00",
            "QueryString": "INSERT INTO public.batch_tx_test VALUES (1, 'first')",
            "ResultRows": 1,
            "ResultSize": 0,
            "RedshiftQueryId": 405066,
            "HasResultSet": false
        },
        {
            "Id": "b32c3c58-3ffc-4e0b-9faa-74e50721f8b8:2",
            "Duration": -1,
            "Error": "ERROR: division by zero",
            "Status": "FAILED",
            "CreatedAt": "2026-07-30T21:56:19.005000+09:00",
            "UpdatedAt": "2026-07-30T21:56:19.977000+09:00",
            "QueryString": "INSERT INTO public.batch_tx_test VALUES (1/0, 'boom')",
            "ResultRows": -1,
            "ResultSize": -1,
            "RedshiftQueryId": 405066,
            "HasResultSet": false
        },
        {
            "Id": "b32c3c58-3ffc-4e0b-9faa-74e50721f8b8:3",
            "Duration": -1,
            "Error": "Connection or an prior query failed.",
            "Status": "ABORTED",
            "CreatedAt": "2026-07-30T21:56:19.010000+09:00",
            "UpdatedAt": "2026-07-30T21:56:19.977000+09:00",
            "QueryString": "INSERT INTO public.batch_tx_test VALUES (3, 'third')",
            "ResultRows": -1,
            "ResultSize": -1,
            "RedshiftQueryId": 0,
            "HasResultSet": false
        }
    ],
    "WorkgroupName": "dataapi-blog-wg",
    "ResultFormat": "json"
}

3 番目が ABORTED、エラーは Connection or an prior query failed. となり、実行されていません。1 番目と 2 番目の RedshiftQueryId が同じ 1881 になっている点も、単一トランザクションとして扱われていることを示しています。

続いて AUTO_COMMIT で同じことを行います。

% aws redshift-data batch-execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --execution-mode AUTO_COMMIT \
  --sqls "INSERT INTO public.batch_ac_test VALUES (1, 'first')" \
         "INSERT INTO public.batch_ac_test VALUES (1/0, 'boom')" \
         "INSERT INTO public.batch_ac_test VALUES (3, 'third')" \
  --wait-time-seconds 30
{
    "Id": "128682c9-5fb9-4fb4-99a6-ec5bb54541ec",
    "CreatedAt": "2026-07-30T21:58:02.140000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "FAILED",
    "RedshiftPid": 1073791095,
    "HasResultSet": false
}

親ステータスは TRANSACTION と同じ FAILED です。サブステートメントを確認します。

% aws redshift-data describe-statement --id 128682c9-5fb9-4fb4-99a6-ec5bb54541ec
{
    "Id": "128682c9-5fb9-4fb4-99a6-ec5bb54541ec",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "Duration": 515891245,
    "Error": "Queries failed in AUTO_COMMIT mode: [2]",
    "Status": "FAILED",
    "CreatedAt": "2026-07-30T21:58:02.140000+09:00",
    "UpdatedAt": "2026-07-30T21:58:03.529000+09:00",
    "RedshiftPid": 1073791095,
    "HasResultSet": false,
    "ResultRows": -1,
    "ResultSize": -1,
    "RedshiftQueryId": 0,
    "SubStatements": [
        {
            "Id": "128682c9-5fb9-4fb4-99a6-ec5bb54541ec:1",
            "Duration": 244527325,
            "Status": "FINISHED",
            "CreatedAt": "2026-07-30T21:58:02.361000+09:00",
            "UpdatedAt": "2026-07-30T21:58:03.021000+09:00",
            "QueryString": "INSERT INTO public.batch_ac_test VALUES (1, 'first')",
            "ResultRows": 1,
            "ResultSize": 0,
            "RedshiftQueryId": 405117,
            "HasResultSet": false
        },
        {
            "Id": "128682c9-5fb9-4fb4-99a6-ec5bb54541ec:2",
            "Duration": -1,
            "Error": "ERROR: division by zero",
            "Status": "FAILED",
            "CreatedAt": "2026-07-30T21:58:02.367000+09:00",
            "UpdatedAt": "2026-07-30T21:58:03.109000+09:00",
            "QueryString": "INSERT INTO public.batch_ac_test VALUES (1/0, 'boom')",
            "ResultRows": -1,
            "ResultSize": -1,
            "RedshiftQueryId": 405117,
            "HasResultSet": false
        },
        {
            "Id": "128682c9-5fb9-4fb4-99a6-ec5bb54541ec:3",
            "Duration": 271363920,
            "Status": "FINISHED",
            "CreatedAt": "2026-07-30T21:58:02.372000+09:00",
            "UpdatedAt": "2026-07-30T21:58:03.468000+09:00",
            "QueryString": "INSERT INTO public.batch_ac_test VALUES (3, 'third')",
            "ResultRows": 1,
            "ResultSize": 0,
            "RedshiftQueryId": 405122,
            "HasResultSet": false
        }
    ],
    "WorkgroupName": "dataapi-blog-wg",
    "ResultFormat": "json",
    "ExecutionMode": "AUTO_COMMIT"
}

3 番目が FINISHED になりました。失敗した文の後ろも実行されています。加えて、生の出力を並べると次の違いが見えてきます。

  • 親の ErrorQueries failed in AUTO_COMMIT mode: [2] となり、失敗した文の番号が配列で示されるTRANSACTIONQuery #2 failed with ERROR: division by zero とは表現が異なります
  • レスポンス末尾に "ExecutionMode": "AUTO_COMMIT" が付く。TRANSACTION(既定)で実行した先ほどの出力には、このフィールド自体がありません
  • 3 番目の RedshiftQueryId1897 と、1・2 番目の 1892 から分かれている

実際にテーブルに残った行を比較します。

% aws redshift-data execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sql "SELECT 'TRANSACTION' AS mode, COALESCE(SUM(1),0) AS rows_left, LISTAGG(note, ',') AS notes FROM public.batch_tx_test UNION ALL SELECT 'AUTO_COMMIT', COALESCE(SUM(1),0), LISTAGG(note, ',') FROM public.batch_ac_test" \
  --wait-time-seconds 30 --query 'Id' --output text

ce3a3840-e574-4385-a4e3-fe8950a45a0a
% aws redshift-data get-statement-result --id ce3a3840-e574-4385-a4e3-fe8950a45a0a

{
    "Records": [
        [
            {
                "stringValue": "TRANSACTION"
            },
            {
                "longValue": 0
            },
            {
                "isNull": true
            }
        ],
        [
            {
                "stringValue": "AUTO_COMMIT"
            },
            {
                "longValue": 2
            },
            {
                "stringValue": "first,third"
            }
        ]
    ],
    "ColumnMetadata": [
        {
            "isCaseSensitive": true,
            "isCurrency": false,
            "isSigned": false,
            "label": "mode",
            "name": "mode",
            "nullable": 1,
            "precision": 11,
            "scale": 0,
            "schemaName": "",
            "tableName": "",
            "typeName": "varchar",
            "length": 0
        },
        {
            "isCaseSensitive": false,
            "isCurrency": false,
            "isSigned": true,
            "label": "rows_left",
            "name": "rows_left",
            "nullable": 1,
            "precision": 19,
            "scale": 0,
            "schemaName": "",
            "tableName": "",
            "typeName": "int8",
            "length": 0
        },
        {
            "isCaseSensitive": true,
            "isCurrency": false,
            "isSigned": false,
            "label": "notes",
            "name": "notes",
            "nullable": 1,
            "precision": 65535,
            "scale": 0,
            "schemaName": "",
            "tableName": "",
            "typeName": "varchar",
            "length": 0
        }
    ],
    "TotalNumRows": 2
}

TRANSACTION は 0 行(notesisNull)、AUTO_COMMIT は 2 行で first,third が残りました。TRANSACTION 側は 1 番目のサブステートメントが ResultRows: 1 と記録されていたにもかかわらずテーブルは空であり、ロールバックされたことがはっきり分かります。

バッチ内でパラメータを共有する

--parameters で渡した値がバッチ内の複数文から参照できるかを確認します。

% aws redshift-data batch-execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --execution-mode AUTO_COMMIT \
  --sqls "DELETE FROM public.blog_orders WHERE load_date = :load_date" \
         "DELETE FROM public.blog_items WHERE load_date = :load_date" \
         "INSERT INTO public.blog_orders VALUES (99, :load_date, 999.00)" \
  --parameters name=load_date,value=2026-07-29 \
  --wait-time-seconds 30
{
    "Id": "b88a807d-f848-46f5-a6a4-18b2d880fe1c",
    "CreatedAt": "2026-07-30T22:04:41.517000+09:00",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "WorkgroupName": "dataapi-blog-wg",
    "Status": "FINISHED",
    "RedshiftPid": 1073897582,
    "HasResultSet": false
}
% aws redshift-data describe-statement --id b88a807d-f848-46f5-a6a4-18b2d880fe1c
{
    "Id": "b88a807d-f848-46f5-a6a4-18b2d880fe1c",
    "DbUser": "IAMR:cm-user",
    "Database": "dev",
    "Duration": 1251001472,
    "Status": "FINISHED",
    "CreatedAt": "2026-07-30T22:04:41.517000+09:00",
    "UpdatedAt": "2026-07-30T22:04:43.676000+09:00",
    "RedshiftPid": 1073897582,
    "HasResultSet": false,
    "ResultRows": -1,
    "ResultSize": -1,
    "RedshiftQueryId": 0,
    "QueryParameters": [
        {
            "name": "load_date",
            "value": "2026-07-29"
        }
    ],
    "SubStatements": [
        {
            "Id": "b88a807d-f848-46f5-a6a4-18b2d880fe1c:1",
            "Duration": 616451380,
            "Status": "FINISHED",
            "CreatedAt": "2026-07-30T22:04:41.784000+09:00",
            "UpdatedAt": "2026-07-30T22:04:42.819000+09:00",
            "QueryString": "DELETE FROM public.blog_orders WHERE load_date = :load_date",
            "ResultRows": 2,
            "ResultSize": 0,
            "RedshiftQueryId": 405297,
            "HasResultSet": false
        },
        {
            "Id": "b88a807d-f848-46f5-a6a4-18b2d880fe1c:2",
            "Duration": 294325118,
            "Status": "FINISHED",
            "CreatedAt": "2026-07-30T22:04:41.789000+09:00",
            "UpdatedAt": "2026-07-30T22:04:43.189000+09:00",
            "QueryString": "DELETE FROM public.blog_items WHERE load_date = :load_date",
            "ResultRows": 2,
            "ResultSize": 0,
            "RedshiftQueryId": 405303,
            "HasResultSet": false
        },
        {
            "Id": "b88a807d-f848-46f5-a6a4-18b2d880fe1c:3",
            "Duration": 340224974,
            "Status": "FINISHED",
            "CreatedAt": "2026-07-30T22:04:41.795000+09:00",
            "UpdatedAt": "2026-07-30T22:04:43.609000+09:00",
            "QueryString": "INSERT INTO public.blog_orders VALUES (99, :load_date, 999.00)",
            "ResultRows": 1,
            "ResultSize": 0,
            "RedshiftQueryId": 405309,
            "HasResultSet": false
        }
    ],
    "WorkgroupName": "dataapi-blog-wg",
    "ResultFormat": "json",
    "ExecutionMode": "AUTO_COMMIT"
}

1 つの load_date を 3 文すべてが参照し、それぞれ 2 行・2 行・1 行を処理しました。QueryParameters として渡した値が記録されるため、後から何を指定して実行したのかも追えます。日付やパーティションキーのような共通の値を SQL に埋め込まずに済みます。

公式ドキュメントには「指定したパラメータはバッチ内の少なくとも 1 つの SQL から参照されている必要がある」と記載されています。ダメ元で未参照のパラメータを渡してみたところ、以下のエラーが返されました。

% aws redshift-data batch-execute-statement \
  --workgroup-name dataapi-blog-wg --database dev \
  --sqls "SELECT :used AS a" \
  --parameters name=used,value=1 name=unused,value=2 \
  --wait-time-seconds 30

aws: [ERROR]: An error occurred (ValidationException) when calling the BatchExecuteStatement operation: Parameters list contains unused parameters. Unused parameters: [unused]

未使用のパラメータ名まで示されるため、原因の特定は容易です。共通のパラメータセットを使い回して SQL の組み合わせだけを変える実装では、この制約に引っかかる可能性があります。

考察

実測して分かった点を整理します。

ロングポーリングは、実行時間 11 秒のクエリで 12.368 秒の保留を確認できました。従来のポーリングループを --wait-time-seconds に置き換えられるため、AWS Lambda から Data API を呼んでいる構成では、待機のためのループとスリープを削れます。ただし上限は 30 秒なので、それを超える処理では待機時間切れのレスポンスを受けて再度ロングポーリングする実装が必要です。実際に --wait-time-seconds 5 では 5.038 秒で STARTED が返り、約6秒経過後 FINISHED を返しました。

GetStatementResult のロングポーリングは、待機時間内に結果が揃わないと ResourceNotFoundException になります。エラーコードだけを見て失敗と判定する実装だと誤検知しますので、「まだ結果がない」状態として扱う必要があります。

ListSessions では CurrentStatementId が返るため、BUSY なセッションがどのクエリで塞がっているかを追えます。セッション数の上限はクラスターまたはワークグループあたり 500、SessionKeepAliveSeconds の最大は 24 時間で、同一セッションでのクエリの並列実行はできません。セッションが枯渇したときに、原因のステートメントまで辿れるようになったのは実用的だと感じました。

AUTO_COMMIT は、部分完了を許容してリトライで埋めるワークロードに向いています。一方で運用設計上、押さえておきたい点が 2 つあります。1 つは親ステータスが FAILED になることです。ただし親の Error には Queries failed in AUTO_COMMIT mode: [2] のように失敗した文の番号が入るため、どこで失敗したかはここまでで分かります。成功した文がどれかまで知るには SubStatements を確認する必要があります。もう 1 つは成功した文がロールバックされないことです。冪等性のない INSERT を並べている場合、リトライで二重登録になる可能性がありますので、ClientTokenMERGE の併用を検討したいところです。すべてが成立してほしい処理では、引き続き既定の TRANSACTION を使うのが安全です。

なお DescribeStatement のレスポンスに ExecutionMode フィールドが現れるのは AUTO_COMMIT で実行した場合のみでした。既定の TRANSACTION では出力されないため、実行モードをレスポンスから判定する場合はフィールドの有無で見る必要があります。

今後に期待したい点としては、WaitTimeSeconds の上限(現在 30 秒)の緩和と、セッションを明示的にクローズする API が挙げられます。今回 CLOSED のセッションを確認する際は、SessionKeepAliveSeconds を短く設定して TTL 切れを待つ必要がありました。

最後に

Amazon Redshift Data API のロングポーリング、ListSessions、柔軟なバッチ実行を実際に試してみました。いずれも、これまでアプリケーション側で書いていたコードを API 側に寄せるアップデートです。ポーリングループは --wait-time-seconds に、セッション ID の外部管理は list-sessions に、バッチ全体のロールバック回避は --execution-mode AUTO_COMMIT に置き換えられます。特にロングポーリングは、AWS Lambda や AWS Step Functions から Data API を呼んでいる構成で、実行時間とコード量の両方に効いてきます。

なお、新しいパラメータを使うには AWS CLI や SDK の更新が必要です。手元の環境で --wait-time-seconds が見つからない場合は、まず CLI のバージョンを確認してみてください。既存のポーリング処理を置き換えられないか、確認してみてはいかがでしょうか。

この記事をシェアする

AWSのお困り事はクラスメソッドへ

関連記事