SQL 問い合わせの性能確認手順
- 集計(group by)と選択(where)の2種類の SQL 問い合わせを使う.
- データサイズを 250M バイトから 12000M バイトまで変えながら,問い合わせの処理時間を計測する.
- 合わせて,Python からの計測と,csvkit(CSV ファイルを直接 SQL で問い合わせできるツール)も体験する.
処理時間の計測では,計測のばらつきを抑えるため,計測のたびにキャッシュのクリアを行い,3回の計測の平均をとる. 計測中のプロセッサ,ストレージの利用状況の確認は,dstat(システム計測ソフト)のページで説明している.
このページで紹介しているソフトウェア類の利用条件等は,利用者で確認すること.
前準備
- テスト用に使う CSV データの合成: 別ページ »で説明
- 問い合わせ対象の SQLite 3 データベース(T250M_1.db,T500M_1.db,T1000M_1.db など)は,リレーショナルデータベースへの CSV ファイルインポートと性能確認手順のページの手順で作成しておくこと.
SQLite 3 について: 別ページ »にまとめ
SQLite 3 での SQL 問い合わせの性能
SQL 問い合わせのテスト実行(SQLite 3 を使用)
次のように設定して実行すること.
- 「T1000M_1」のところはテーブル名
- 「T1000M_1.db」のところは SQLite 3 データベース名
エラーメッセージが出ないことを確認.
# 集計(group by)の問い合わせ
echo "select region, count(*) from T1000M_1 group by region;" | time sqlite3 T1000M_1.db
# 選択(where)の問い合わせ
echo "select birth_cap from T1000M_1 where birth_cap > 80000 limit 10;" | time sqlite3 T1000M_1.db
SQL 問い合わせでのデータサイズと処理時間の関係(SQLite 3 を使用)
データサイズを 250M バイトから 12000M バイトまで変えながら,SQL 問い合わせの処理時間を計測する. 計測のばらつきを抑えるため,計測のたびにキャッシュのクリアを行い,3回の計測の平均をとる.
#!/bin/bash
function elapsed() {
echo `python3 -c "import sys; print( float(sys.argv[2]) - float(sys.argv[1]) )" $1 $2`
return
}
function avg() {
echo `python3 -c 'import sys; print("%.3f" % ((float(sys.argv[1]) + float(sys.argv[2]) + float(sys.argv[3])) / 3.0) )' $1 $2 $3`
return
}
# キャッシュのクリア(PageCache, dentries and inodes)
function cache_clear() {
/usr/bin/sync
/usr/bin/sync
/usr/bin/sync
/usr/bin/sync
/usr/bin/sync
sleep 3
sudo sysctl -w vm.drop_caches=3 > /dev/null
echo 3 | sudo tee -a /proc/sys/vm/drop_caches > /dev/null
return
}
# $1: SQLite 3 Database name, $2: Table name
function sqlite3_query() {
echo "select region, count(*) from $2 group by region;"
start=`date +%s.%N`
echo "select region, count(*) from $2 group by region;" | sqlite3 $1 > /dev/null
current=`date +%s.%N`
echo `elapsed $start $current`
return
}
# 250M
cache_clear
elapsed1=`sqlite3_query T250M_1.db T250M_1`
cache_clear
elapsed2=`sqlite3_query T250M_1.db T250M_1`
cache_clear
elapsed3=`sqlite3_query T250M_1.db T250M_1`
echo 250M: `avg $elapsed1 $elapsed2 $elapsed3`
# 500M
cache_clear
elapsed1=`sqlite3_query T500M_1.db T500M_1`
cache_clear
elapsed2=`sqlite3_query T500M_1.db T500M_1`
cache_clear
elapsed3=`sqlite3_query T500M_1.db T500M_1`
echo 500M: `avg $elapsed1 $elapsed2 $elapsed3`
# 1000M
cache_clear
elapsed1=`sqlite3_query T1000M_1.db T1000M_1`
cache_clear
elapsed2=`sqlite3_query T1000M_1.db T1000M_1`
cache_clear
elapsed3=`sqlite3_query T1000M_1.db T1000M_1`
echo 1000M: `avg $elapsed1 $elapsed2 $elapsed3`
# 2000M
cache_clear
elapsed1=`sqlite3_query T2000M_1.db T2000M_1`
cache_clear
elapsed2=`sqlite3_query T2000M_1.db T2000M_1`
cache_clear
elapsed3=`sqlite3_query T2000M_1.db T2000M_1`
echo 2000M: `avg $elapsed1 $elapsed2 $elapsed3`
# 4000M
cache_clear
elapsed1=`sqlite3_query T4000M_1.db T4000M_1`
cache_clear
elapsed2=`sqlite3_query T4000M_1.db T4000M_1`
cache_clear
elapsed3=`sqlite3_query T4000M_1.db T4000M_1`
echo 4000M: `avg $elapsed1 $elapsed2 $elapsed3`
# 8000M
cache_clear
elapsed1=`sqlite3_query T8000M_1.db T8000M_1`
cache_clear
elapsed2=`sqlite3_query T8000M_1.db T8000M_1`
cache_clear
elapsed3=`sqlite3_query T8000M_1.db T8000M_1`
echo 8000M: `avg $elapsed1 $elapsed2 $elapsed3`
# 12000M
cache_clear
elapsed1=`sqlite3_query T12000M_1.db T12000M_1`
cache_clear
elapsed2=`sqlite3_query T12000M_1.db T12000M_1`
cache_clear
elapsed3=`sqlite3_query T12000M_1.db T12000M_1`
echo 12000M: `avg $elapsed1 $elapsed2 $elapsed3`
インデックスの効果の確認(SQLite 3 を使用)
選択(where)の問い合わせについて,インデックス作成の前後で処理時間を比べる. 「T1000M_1」のところはテーブル名,「T1000M_1.db」のところは SQLite 3 データベース名に設定して実行すること.
# インデックス作成前の問い合わせ
echo "select count(*) from T1000M_1 where birth_cap > 80000;" | time sqlite3 T1000M_1.db
# インデックスの作成(作成自体にも時間がかかることを確認)
echo "create index idx_birth_cap on T1000M_1(birth_cap);" | time sqlite3 T1000M_1.db
# インデックス作成後の問い合わせ
echo "select count(*) from T1000M_1 where birth_cap > 80000;" | time sqlite3 T1000M_1.db
Python からの SQL 問い合わせと処理時間の計測
Python 標準ライブラリの sqlite3 モジュールを使う. 下のプログラムをファイル名 sqlquery.py で保存し,次のように実行する.
python3 sqlquery.py T1000M_1.db T1000M_1
#!/usr/bin/python3
# SQL 問い合わせの処理時間の計測
# 使い方: python3 sqlquery.py <SQLite 3 データベース名> <テーブル名>
import sys
import time
import sqlite3
dbname = sys.argv[1]
tablename = sys.argv[2]
conn = sqlite3.connect(dbname)
cur = conn.cursor()
# 集計(group by)の問い合わせ
start = time.perf_counter()
cur.execute("select region, count(*) from %s group by region" % tablename)
result = cur.fetchall()
elapsed = time.perf_counter() - start
print(result)
print("group by: %.3f sec" % elapsed)
# 選択(where)の問い合わせ
start = time.perf_counter()
cur.execute("select count(*) from %s where birth_cap > 80000" % tablename)
result = cur.fetchall()
elapsed = time.perf_counter() - start
print(result)
print("where: %.3f sec" % elapsed)
conn.close()
csvkit による CSV ファイルへの直接の SQL 問い合わせ
csvkit の csvsql を使うと,CSV ファイルをデータベースにインポートすることなく,SQL で直接問い合わせできる. 内部では CSV ファイル全体をメモリ上の SQLite データベースに読み込んでから問い合わせを実行する.
csvkit のインストール(Ubuntu 上)
sudo apt update
sudo apt -y install python3-pip
pip3 install -U csvkit
csvsql による SQL 問い合わせ.「T250M_1.csv」のところは CSV ファイル名,「T250M_1」のところはテーブル名(拡張子を除いたファイル名になる)に設定して実行すること.
time csvsql --query "select region, count(*) from T250M_1 group by region" T250M_1.csv
time csvsql --query "select birth_cap from T250M_1 where birth_cap > 80000 limit 10" T250M_1.csv
考察ポイント
- データサイズと問い合わせの処理時間はほぼ比例するか(インデックスがない場合,どちらの問い合わせもテーブル全体の読み取り,つまりフルスキャンになる).
- キャッシュのクリアを行わずに同じ問い合わせを2回続けて実行すると,2回目の処理時間はどうなるか.dstat でストレージのリード量の違いを確認する.
- インデックス作成の前後で,選択(where)の問い合わせの処理時間はどう変わるか.インデックス作成自体の時間と,データベースファイルのサイズの増加も確認する.
- csvsql(CSV ファイルへの直接の問い合わせ)と,インポート済みの SQLite 3 データベースへの問い合わせとで,処理時間を比べる.同じ CSV ファイルに何度も問い合わせるなら,どちらが有利か.