🌊

ターミナルだけで完結するデータ分析環境の構築検討:Codex CLI + awk, q, duckdb

に公開

要約(TL;DR)

  • awk:単一CSVの軽量集計・整形向き。依存ほぼゼロでどこでも動く。JOIN/複雑統計は苦手

  • q:CSVをSQLで叩ける。単一ファイルstdin + FROM - が安定。複数JOINはファイル名のクォートが罠

  • DuckDB:CSV/Parquet/JSONをRDB級の表現力で爆速処理。JOIN/ウィンドウ/型推定/関数が揃ってる。

  • 運用指針(Codex 連携)

    • 単一ファイル(軽整形・一次統計) → awk

      • 目安: 行数 < 5万・列数 < 50、JOIN/日時関数なし
    • 単一ファイル(SQLで書きたい) → q(stdin + FROM -

      • 目安: WHERE/GROUP BY/ORDER BY を素直にSQLで
    • 複数ファイルJOIN / 複雑集計 / 型推定・日時関数あり / 100MB超 → DuckDB

      • 基本は DuckDB 一択。read_csv_auto + ウィンドウ/集計で完結
    • どれにも当てはまらない軽い用途 → awk

    • (個人的には、duckdb + awk のみで十分だと思う)

ツールの特徴と優位性

awk

  • 強み:軽い・標準搭載・行処理が速い。一次統計、フィルタ、整形が得意。

  • 苦手:JOIN/複雑集計/日時関数。

  • (基本統計量・ヘッダ1行スキップ):

    awk -F, 'NR>1{n++; s+=$2; s2+=$2*$2; if(n==1){min=$2;max=$2} else{min=($2<min)?$2:min; max=($2>max)?$2:max}} 
             END{m=s/n; sd=sqrt(s2/n - m*m); printf "n=%d mean=%.4f sd=%.4f min=%.3f max=%.3f\n", n,m,sd,min,max}' file.csv
    

q(text-as-data)

  • 強み:SQLで直感的に。WHERE/GROUP/ORDER が気持ちよく書ける。

  • 落とし穴:ファイル名・列名のクォート

  • q -H -d, "SELECT COUNT(*) n, AVG(CAST(eruptions AS REAL)) mean FROM - " < faithful.csv
    

DuckDB

  • 強みJOIN/ウィンドウ/複雑統計/型推定が楽。read_csv_auto が賢い。Parquet/JSONもOK。

  • (CSVの一次統計):

    duckdb -c "SELECT COUNT(*) n,
                      AVG(eruptions) mean_e, STDDEV_SAMP(eruptions) sd_e, MIN(eruptions) min_e, MAX(eruptions) max_e,
                      AVG(waiting)   mean_w, STDDEV_SAMP(waiting)   sd_w, MIN(waiting)   min_w, MAX(waiting)   max_w
               FROM read_csv_auto('faithful.csv')"
    

ベンチマーク設計

タスク & データセット

  • 指示: 「2025-04-01の統計値を算出して
    1. 3つのテーブルを Date で JOIN
    2. 2025-04-01 に絞り込み
    3. 各品質(QualityA/B/C)ごとに件数/平均/合計などを算出, となる想定
  • データ:
    • production_101.csv: Date, Line101, QualityA, QualityB, QualityC, Amount
    • production_121.csv: Date, Line121, QualityA, QualityB, QualityC, Amount
    • weather.csv: Date, Temperature, Humidity, Wind Speed, Wind Direction, Weather

実験条件

  • パータン: awk / q / DuckDB の単独3 + 順列コンビ4 = 計7パターン(手順は AGENTS.md に定義)
  • 計測:
    • 実行時間 / 常駐メモリ:

      /usr/bin/time -f '%E %M' <command>
      
      • %E: 経過時間 (min:sec)
      • %M: 最大常駐集合サイズ (KB)
    • トークン消費: Codex 実行ログの tokens used を採用(モデル別に記録)

パターン 中央値(実行時間) ベスト ワースト 中央値(最大メモリKB) 中央値(消費トークン)
duckdbのみ 2:24.76 1:55.38 2:54.14 118,572 7,285
awkのみ 4:28.99 2:47.40 6:10.57 118,234 7,366
awk → q → DuckDB 4:15.94 2:12.97 6:18.90 117,988 13,408
DuckDB → q → awk 5:50.99 3:57.87 7:44.10 118,486 14,676
q(stdin) → awk → DuckDB 8:17.32 6:05.04 10:29.60 118,316 10,594
qのみ 9:15.41 7:43.15 10:47.67 118,374 11,252
q → DuckDB → awk 10:55.73 10:08.53 11:42.93 118,290 14,838

所感(このデータセット・手順に限る):

  • 速度・トークンともに DuckDB 単独が最良(中央値 2:24.76 / 7,285 tok)。

  • ワークフローを跨ぐほど(q→DuckDB→awk 等)遅く・トークンも増えるOrchestration コストが無視できない。

  • awk 単独は軽作業なら悪くないが、JOIN/統計を入れた途端に表現力の差で手数が増える。

  • メモリは計測系のオーバーヘッドでほぼ横並び(この規模では差が出にくい)。

AGENTS.md 全文
  1. awk のみ
AGENTS_S1.md
## データアクセス & 分析ルール (S1: awkのみ)

- 常に **awk** を使う。q や DuckDB は利用しない。
- 最初の行は列名なので `head -n1 file.csv` で確認してから処理を書く。
- 基本例:
  ```bash
  awk -F, 'NR>1{n++; s+=$2} END{print "mean:",s/n}' file.csv
  ```
  1. q のみ
AGENTS_S2.md
## データアクセス & 分析ルール (S2: qのみ)

- 常に **q** を使う。awk や DuckDB は利用しない。
- 列名は `head -n1 file.csv` で確認して SQL を組み立てる。
- 単一ファイルは必ず stdin + `FROM -` を徹底:
  ```bash
  q -H -d, "SELECT * FROM - LIMIT 5" < file.csv
  ```
- 複数ファイルを扱う場合は `-f` とダブルクォート付きファイル名を利用:
  ```bash
  q -H -d, -f a.csv -f b.csv -- 'SELECT * FROM "a.csv" JOIN "b.csv" USING(id)'
  ```
  1. DuckDB のみ
AGENTS_S3.md
## データアクセス & 分析ルール (S3: DuckDBのみ)

- 常に **DuckDB** を使う。awk や q は利用しない。
- 列名は `head -n1 file.csv` で確認してクエリを作成。
- 単一・複数を問わず `read_csv_auto` を利用:
  ```sql
  SELECT COUNT(*), AVG(col1)
  FROM read_csv_auto('file.csv', HEADER=TRUE);
  ```
- 複数ファイル JOIN も DuckDB で記述する。
  1. q → DuckDB → awk
AGENTS_C1.md
## データアクセス & 分析ルール (C1: q → DuckDB → awk)

- 単一ファイル: **q (stdin + FROM -)**- 複数ファイルや複雑な集計: **DuckDB**- どちらも使えない場合: **awk**- 列名は必ず `head -n1 file.csv` で確認してクエリを生成。
  1. awk → q → DuckDB
AGENTS_C2.md
## データアクセス & 分析ルール (C2: awk → q → DuckDB)

- 簡単な集計や整形: **awk**- SQL 的な条件や集計: **q (stdin + FROM -)**- JOIN や高度な分析: **DuckDB**- 列名確認は `head -n1` を必須とする。
  1. DuckDB → q → awk
AGENTS_C3.md
## データアクセス & 分析ルール (C3: DuckDB → q → awk)

- 既定は **DuckDB** (`read_csv_auto`)。
- 軽い処理なら **q (stdin)**- DuckDB/q が無ければ **awk**- クエリ作成前に必ず `head -n1` で列名を確認。
  1. q(stdin) → awk → DuckDB
AGENTS_C4.md
## データアクセス & 分析ルール (C4: q(stdin) → awk → DuckDB)

- 単一ファイル: **q (stdin + FROM -)** を必ず使用。
- q が不在/失敗した場合: **awk**- JOIN や複雑集計: **DuckDB**- 列名は `head -n1 file.csv` で確認してから処理。

Tips & Tricks

  • CSVのクォート対応(awk)-v FPAT='([^,]+)|(\"([^\"]|\"\")*\")' でカンマ入りフィールドに強くなる。出力は -v OFS=, で安定化。

  • q の鉄則:単一ファイルはstdin + FROM -。複数は -f とダブルクォート付きファイル名、スペースを含む列名もダブルクォート。

  • DuckDB のお作法read_csv_auto('file.csv') は賢いが、日時文字列は STRPTIME() で明示的に型変換すると後段が安定。重い時は PRAGMA threads=auto; PRAGMA memory_limit='4GB';

  • BOM 除去sed -i '1s/^\xEF\xBB\xBF//' *.csv

  • 失敗時リトライ禁止:同一条件で2回連続失敗したら、状況整理→仮説構築→手を打つ(無目的リトライはしない)。

まとめ(結論)

  • まず DuckDB で完結できないかを考える

  • 軽い単発は awk

  • ワークフロー分割はコスト(遅く・高トークンになりがち)→ 単独完結の設計を優先。

  • Codex には明確な分岐規則を実装し、失敗2連続で状況整理→仮説構築に即切替

最後に、AGENTS.mdに追記する用のルールの例を記しておきます。

AGENTS.md
## DuckDB-first Strategy

* **Always try DuckDB first**

  * Handles joins, window functions, type inference, large files, and complex queries.
  * Use `read_csv_auto('file.csv')` as default.
  * For date/time strings, cast explicitly with `STRPTIME()`.

* **Fallback to awk only if**

  * Single CSV, no joins/date ops, trivial filtering or first-order stats.
  * Dataset size < 50k rows and < 50 columns.
  * Use robust parsing:

    ```bash
    awk -F, -v OFS=, -v FPAT='([^,]+)|(\"([^\"]|\"\")*\")' '...'
    ```

* **Failure policy**

  * If the same approach fails twice, stop retrying.
  * Summarize error + hypothesis, then switch strategy.

* **Operational tips**

  * Remove BOM: `sed -i '1s/^\xEF\xBB\xBF//' *.csv`

Discussion