mygems

データベースなしで CSV ファイルに SQL クエリを実行する方法 (2026 guide)

GROUP BY に Postgres は不要です。DuckDB、SQLite、q、スマホで CSV を直接クエリする方法と使い分けを紹介します。

執筆: uos ·

先に結論です。DuckDB にファイルを直接指定してください。

duckdb -c "SELECT region, SUM(amount) FROM 'sales.csv' GROUP BY region"

スキーマもインポートもサーバーも CREATE TABLE も要りません。CSV はそのままの場所に置いたままで、ヘッダー行がそのまま列名になります。今日ひとつだけ入れるなら、これを入れてください。

このページの残りでは、なぜこれで動くのか、DuckDB が使えないときはどうするか、そして誰もがつまずく 2 点、ファイル名の引用符と、列名にスペースが入っている場合の扱いを説明します。

ファイルをどこかに読み込む必要がない理由

「まずデータをデータベースに入れる」という発想は、SQL エンジンが自分のストレージしか読めなかった時代の名残です。最近のエンジンにはその制約がありません。DuckDB、SQLite、いくつかの小さな CLI ツールは、1 回のクエリの間だけ CSV をテーブルとして扱い、サンプリングで型を推定して結果を返し、あとはすべて忘れます。

これで損得の計算がまるで変わります。メールで届いたファイルについての単発の質問に、スキーマも読み込みスクリプトも後片付けも割く価値はありません。1 行で十分です。

本物のデータベースに読み込むのが正解になるのは、次の 2 つの場合だけです。複数人が繰り返しデータを問い合わせる場合と、1 枚のフラットなファイルでは表現できない制約やリレーションが必要な場合です。「このエクスポートの地域別合計は?」はどちらにも当てはまりません。

選択肢を正直に比較

ツールインストール向いている用途苦手なこと
DuckDBバイナリ 1 つ、依存なし数十 GB までのほぼすべてこの用途ではほぼなし
SQLite + .importmacOS と Linux に標準搭載テーブルを後のために残す型。すべて TEXT になる
qpip install q-text-as-dataシェルのパイプラインでの手早いワンライナー大きなファイル。遅い
csvsql (csvkit)pip install csvkit本物の DB 用の CREATE TABLE を生成する速度。ここで一番遅い
Pandas + pandasqlpandas が既にある結果をさらに Python で使うときSQL の方言が Postgres ではなく SQLite
SQL が使えるスマホアプリApp Store / Playファイルがスマホのメールに届いたときスクリプト化したいこと全般

正直なランキングは短く、ほとんどは DuckDB、何も入れられないときは SQLite、パソコンの前にいないときはスマホアプリです。他のツールも悪くなく出番はありますが、この用途で DuckDB に勝るものはありません。

DuckDB の使い方

インストールはバイナリ 1 つです。brew install duckdb か、ダウンロードしてパスの通った場所に置いてください。そのあとは次のようにします。

duckdb
D SELECT * FROM 'orders.csv' LIMIT 10;

最初に知っておくことが 2 つあります。

ファイル名はシングルクォートで囲みます。 FROM orders.csv は、スキーマ orders とテーブル csv として解釈され、エラーメッセージを見ても原因が分かりにくいです。正しくは FROM 'orders.csv' です。

スペースを含む列名はダブルクォートで囲みます。 SELECT "Order Date" FROM … のようにします。ここでシングルクォートを使うと、エラーにならず、全行に同じ文字列がそのまま返るため、クエリが成功したように見えてしまいます。

ディレクトリごとまとめて読むには glob を使います。エクスポートが月ごとに分かれて届いたときに便利です。

SELECT * FROM 'exports/*.csv'

型の推定が外れたときは、たいてい先頭にゼロが付く ID の列や、認識できない形式の日付が原因です。そのときは推定と戦わず、読み込みを自分で制御してください。

SELECT * FROM read_csv('orders.csv',
  header = true,
  columns = {'id': 'VARCHAR', 'ordered_at': 'DATE', 'amount': 'DOUBLE'},
  dateformat = '%d/%m/%Y'
)

read_csv には delim、quote、escape、ignore_errors = true、sample_size = -1 も指定できます。sample_size = -1 は、先頭 20,480 行ではなくファイル全体をスキャンしてから型を決めます。1 万行ほど数値に見えた列が、途中で N/A に当たるときの対処がこれです。

結果をファイルに書き戻すのも SQL のままでできます。

COPY (SELECT * FROM 'orders.csv' WHERE amount > 1000)
TO 'large-orders.csv' (HEADER, DELIMITER ',');

同じファイルを数回以上問い合わせるなら、COPY (SELECT * FROM 'orders.csv') TO 'orders.parquet' で一度 Parquet に変換し、以後は Parquet に問い合わせてください。型が付き、圧縮され、スキャンは通常 5 倍から 20 倍速くなります。

SQLite:何もインストールできないとき

sqlite3 は macOS とほとんどの Linux ディストリビューションに入っているので、制限の厳しいマシンでも使えます。

sqlite3 shop.db
sqlite> .mode csv
sqlite> .import orders.csv orders
sqlite> SELECT region, COUNT(*) FROM orders GROUP BY region;

注意点があり、これは重要です。.import はすべての列を TEXT にします。 そのため並べ替えが辞書順になり、100 が 20 より前に来て、SUM() も意外な結果になります。クエリ時にキャスト(CAST(amount AS REAL))するか、インポート前に正しい型でテーブルを作成してください。

DuckDB と違い、shop.db が残ります。明日もそのテーブルを使いたいなら利点ですが、そうでなければ余計なファイルです。

q:シェルのパイプライン向け

クエリがシェルコマンドの連なりの一段階なら、q は他の Unix ツールと同じように標準入力を読みます。

q -H -d, "SELECT region, SUM(amount) FROM - GROUP BY region" < orders.csv

-H は 1 行目がヘッダーであること、-d, は区切り文字、- は標準入力を表します。中身は SQLite なので、方言も SQLite です。大きなファイルでは、現実的な選択肢の中で一番遅く、数十万行なら問題ありませんが、それ以上は厳しくなります。

ファイルがスマホにあるとき

エクスポートがメールの添付ファイルで、デスクにいない場合、ここまでの方法は役に立ちません。よくある状況で、「自分宛てに送って後で見る」という通常の対処は、あまり良い方法ではありません。

CSV File Viewer - Smart CSViPhone・iPad · Android

iOS と Android でファイルに対して本物の SQL を実行できます。スマホのキーボードでクエリを打ちたくないときのためにビジュアルなフィルタービルダーがあり、列名がまだ分からないときのためにふつうの言葉で聞けるアシスタントもあります。

できること

方言は SQLite なので、この記事のクエリはそのまま移せますが、いつもの注意は必要です。DuckDB の例にあるウィンドウ関数は使えず、CAST の習慣も同じように必要です。

手元に置いておきたいクエリ

このファイルに何が入っているか:

SELECT * FROM 'file.csv' LIMIT 20;
DESCRIBE SELECT * FROM 'file.csv';
SELECT COUNT(*) FROM 'file.csv';

NULL がどこにあるか。 列を 1 つずつ調べるのは面倒なので、こうします。

SELECT COUNT(*) - COUNT(email) AS missing_email,
       COUNT(*) - COUNT(phone) AS missing_phone
FROM 'contacts.csv';

キーの重複:

SELECT id, COUNT(*) AS n
FROM 'orders.csv'
GROUP BY id HAVING n > 1
ORDER BY n DESC;

2 つのエクスポートの結合は、この方法の価値を一番よく示すクエリです。2 つのシートをまたいで VLOOKUP でやるよりずっと楽だからです。

SELECT o.id, o.amount, c.name
FROM 'orders.csv' o
JOIN 'customers.csv' c ON c.id = o.customer_id;

日付列から月別の合計:

SELECT date_trunc('month', ordered_at) AS month, SUM(amount)
FROM 'orders.csv'
GROUP BY month ORDER BY month;

うまくいかないとき

すべてが 1 列にまとまって返ってきた。 区切り文字がカンマではありません。ヨーロッパの多くの地域ではセミコロンが標準です。read_csv('f.csv', delim = ';') を試してください。それ以上に変なファイルなら、単に珍しいのではなく、本当に壊れているのかもしれません。

数値の列が合計できない。 非数値が混ざっているため、テキストとして推定されています。原因は SELECT amount FROM 'f.csv' WHERE TRY_CAST(amount AS DOUBLE) IS NULL LIMIT 10; で見つけられます。通貨記号、桁区切り、N/A という文字列のどれかであることがほとんどです。

先頭のゼロが消えた。 列が数値として読まれています。columns で VARCHAR を指定し、その後は表計算ソフトで開かないでください。

ファイルの途中にヘッダー行が何度も出てくる。 複数のエクスポートが連結されています。WHERE id <> 'id' が荒っぽい対処ですが、たいていはこれで正解です。

メモリが足りなくなった。 DuckDB はストリーミングで処理するので、通常はファイル全体に対する ORDER BY や DISTINCT が原因です。一時ディレクトリ(SET temp_directory = '/tmp/duckdb')を指定すれば、ディスクにあふれさせて処理します。規模ごとに何が使えるかという広い話は、1GB の CSV ファイルを開く方法を参照してください。

よくある質問

CSV ファイルをインポートせずに SQL を実行できますか?
はい。DuckDB なら `SELECT * FROM 'file.csv'` で CSV をその場で問い合わせられ、列名はヘッダー行から、型はサンプリングで推定されます。インポートも、テーブルの作成も行われず、ファイルは変更されません。SQLite でも可能ですが、.import の手順で永続的なテーブルが作られます。
大きな CSV を SQL で問い合わせる最速の方法は何ですか?
セットアップ不要の選択肢の中では、大差で DuckDB です。ファイルを並列に読み、すべてをメモリに載せずにストリーミングで処理するので、RAM よりはるかに大きなファイルも扱えます。同じファイルを繰り返し問い合わせるなら、COPY … TO 'file.parquet' で一度 Parquet に変換すると、スキャンが 5 倍から 20 倍速くなります。
名前にスペースが入っている CSV の列はどう問い合わせますか?
ダブルクォートで囲みます。SELECT "Order Date" FROM 'orders.csv' のようにします。シングルクォートだと文字列リテラルになり、エラーにならずに全行で同じ文字列が返るため、クエリが成功したように見えます。
スマホで CSV ファイルに SQL クエリを実行できますか?
はい。Smart CSV Viewer は iOS と Android で CSV に対して SQL を実行でき、スマホのキーボードでクエリを打つ代わりにビジュアルなフィルタービルダーも使えます。エンジンは SQLite なので、方言は DuckDB ではなく SQLite のものです。
数値の列が合計できないのはなぜですか?
通貨記号、桁区切り、N/A のようなプレースホルダーなど、非数値が列のどこかに含まれているため、テキストとして推定されています。TRY_CAST(col AS DOUBLE) IS NULL で該当の行を探し、修正するか、read_csv の `columns` 引数で列を明示的に指定して読み込んでください。
どんなときは CSV を本物のデータベースに読み込むべきですか?
次の 2 つの場合です。複数人が繰り返しデータを問い合わせる場合と、1 枚のフラットなファイルでは表現できない制約やリレーションが必要な場合です。メールで届いたファイルについての単発の質問に、スキーマや読み込みスクリプトは要りません。

uos — スマホに SQL エンジンを載せた Smart CSV Viewer を作っているので、何をデータベースに入れるべきかという話題によく出会います。