アプリで大量レコードのCSV出力は重すぎる
僕は常々思ってるんですが、
CSV出力って面倒じゃないですか
本来アプリがやるには重すぎる仕事なんじゃないかと思うんです
大量レコードを出力する際、
- DBから1万件ずつデータ引っ張ってきて
- CSVファイルに書き込んで
- 数GBのCSVをS3にアップロードして
- メモリにもCPU負荷にも気を遣って
など、いろいろ配慮する必要がある
こうした重たいCSV出力を、
アプリを介さずDB側で完結させることができます
そう、SELECT INTO OUTFILE S3 ならね
SELECT INTO OUTFILE S3ってなに?
Aurora MySQLからクエリ結果を直接S3に書き出すための構文
通常の SELECT 文の後ろに INTO OUTFILE S3 という魔法の言葉をつけることで実行できます
SELECT *
FROM users
INTO OUTFILE S3 's3://mybucket/exports/users_20260114'
FIELDS TERMINATED BY ','
LINES TERMINATED BY '\n';
どんな時に使うと嬉しいの?
代表的なユースケースは以下あたりです
システム連携・分析用途
-
分析基盤へのデータ連携
Aurora のデータを S3 に吐き出し、Athena や Redshift などで集計・分析する -
大規模データのスナップショット取得
本番DBのある時点の状態を、アプリを介さず退避する
アプリ負荷を切り離したいケース
- 重いSELECTの逃がし先
全件スキャンや長時間クエリを、アプリ処理から切り離す
今回、使用したケース
後者のアプリ側の負荷軽減で、ユーザ向けのCSVダウンロードのために使いました
ただ、本来の使用想定は分析用のデータ連携などのシステム連携であって、
「日本で人間がExcelでデータを見たい」といったケースだと痒いところに手は届きません ※後述
使い方
下記のユーザーガイドにすべてまとまっています
要点をまとめると、
- Aurora MySQL に S3 アクセス許可
- Aurora MySQL のユーザに権限付与
- 構文を使って実行
構文は以下です
ハマった点として、ちゃんとオプションを順番通りに指定しないと構文エラーになります
SELECT
[ALL | DISTINCT | DISTINCTROW ]
[HIGH_PRIORITY]
[STRAIGHT_JOIN]
[SQL_SMALL_RESULT] [SQL_BIG_RESULT] [SQL_BUFFER_RESULT]
[SQL_CACHE | SQL_NO_CACHE] [SQL_CALC_FOUND_ROWS]
select_expr [, select_expr ...]
[FROM table_references
[PARTITION partition_list]
[WHERE where_condition]
[GROUP BY {col_name | expr | position}
[ASC | DESC], ... [WITH ROLLUP]]
[HAVING where_condition]
[ORDER BY {col_name | expr | position}
[ASC | DESC], ...]
[LIMIT {[offset,] row_count | row_count OFFSET offset}]
INTO OUTFILE S3 's3_uri'
[CHARACTER SET charset_name]
[export_options]
[MANIFEST {ON | OFF}]
[OVERWRITE {ON | OFF}]
[ENCRYPTION {ON | OFF | SSE_S3 | SSE_KMS ['cmk_id']}]
export_options:
[FORMAT {CSV|TEXT} [HEADER]]
[{FIELDS | COLUMNS}
[TERMINATED BY 'string']
[[OPTIONALLY] ENCLOSED BY 'char']
[ESCAPED BY 'char']
]
[LINES
[STARTING BY 'string']
[TERMINATED BY 'string']
]
注意点
BOMはつけられない
BOMは「Byte Order Mark」の略で「このファイルの文字コードはコレです」を表すテキストの先頭に付与される数バイトのデータのこと
UTF-8 の CSV ファイルをダブルクリックして Excel で開くと文字化けする可能性あり
→デフォルトで Shift_JIS で開こうとするため
これって日本だけの話っぽいですね、不思議な国ですね
圧縮できない
出力した数GBのCSVをそのままユーザがダウンロードする、といったケースだとダウンロードに時間かかりすぎます
そういう使い方をするのであれば、別途 圧縮処理する必要ありです
ファイルが分割される、ファイル名にpart_NNNNNがつく
約6GBごとにファイルが分割される仕様
ファイル名の末尾に.part_00000が付き、分割されるごとに採番されていく
ファイルが1つしか出力されない前提で実装すると事故ります
オプションでマニフェストファイルを出力するようにし、出力結果を確認して後処理を書くのが吉
DB負荷
アプリの負荷をDBが担うことになるため、DB負荷にも配慮する
さいごに
「CSV 出力はアプリでごちゃごちゃやる」だけの世界線で生きてきましたが、今回新しい世界を知りました
アプリ、DBのどちらに責務を置くか、みたいな視点もあるんですね