対策:SQLite データベースのパフォーマンスの最適化 - Data Storage - 開発ガイド - BlackBerry Java SDK - 6.0 に、「行のサイズを最小限にする: 幅の広い列がある場合、個別のテーブルにすることを検討します。」 なんて改定あるので、実際のところ行サイズはどれぐらいパフォーマンスに影響するのか気になってきた。
せっかくなので、レコードのデータをすべてフィールドにしてしまった場合と、すべて別テーブルにしてしまっている場合と、どちらがよいパフォーマンスを示すのかについて、 RAM メモリ, SSD, HDD でそれぞれ読み書き速度を比較してみた。
あくまで、私の環境においてなので、参考ぐらいにしておいていただければ幸いだ。
検証コード
一応、以下の様な配置で、bundle 版 System.Data.SQLite.dll を配置していることが前提。
- sqlite_performance_check.ps1
- [x86]
- System.Data.SQLite.dll
- [x64]
- System.Data.SQLite.dll
どちらかの環境だけでしか動かさないのであれば、動かす環境用の System.Data.SQLite.dll だけ置いておけば問題ない。
<###############################################################
System.Net.SQLite のパフォーマンスを見る
###############################################################>
param (
$Count = 1KB,
[switch]$Do01,
[switch]$Do10,
[switch]$Do99,
[switch]$Read,
[switch]$Write
)
$mydir = Split-Path $myInvocation.MyCommand.Path -Parent #スクリプトのフォルダを取得
Add-Type -Path (Join-Path (Join-Path $mydir (&{if ([IntPtr]::Size -eq 8) { 'x64' } else { 'x86' }})) 'System.Data.SQLite.dll')
# -Read も -Write も設定なければ、両方実行
if (!$Read -and !$Write) {
$Read = $Write = $true;
}
# シャッフルデータ作成
[string[]]$shuffleStr = 0..(16KB - 1) | %{ [Guid]::NewGuid().ToString() }
function Execute-Sql ([Data.SQLite.SQLiteCommand]$cmm, [string]$sql) {
$cmm.CommandText = $sql;
$cmm.ExecuteNonQuery() > $null;
}
$cnn = New-Object Data.SQLite.SQLiteConnection("Data Source=$(Join-Path $mydir 'test.sqlite3')")
$cnn.Open();
$cmm = $cnn.CreateCommand();
Execute-Sql $cmm 'CREATE TABLE IF NOT EXISTS tableid01 ([Id] INTEGER PRIMARY KEY, [TableRowId] INTEGER, [DataId] INTEGER)'
Execute-Sql $cmm 'CREATE TABLE IF NOT EXISTS tabledata01 ([Id] INTEGER PRIMARY KEY, [DataId] INTEGER, [DataTypeId] INTEGER , [TEXT1] TEXT)'
Execute-Sql $cmm 'CREATE INDEX IF NOT EXISTS tableid01index on tableid01(TableRowId)'
Execute-Sql $cmm 'CREATE INDEX IF NOT EXISTS tabledata01index on tabledata01(DataId)'
Execute-Sql $cmm 'CREATE TABLE IF NOT EXISTS tableid10 ([Id] INTEGER PRIMARY KEY, [TableRowId] INTEGER, [DataId] INTEGER)'
Execute-Sql $cmm "CREATE TABLE IF NOT EXISTS tabledata10 ([Id] INTEGER PRIMARY KEY, [DataId] INTEGER, [DataTypeId] INTEGER, $((1..10 | %{ `"[TEXT$_] TEXT`" }) -join ', '))"
Execute-Sql $cmm 'CREATE INDEX IF NOT EXISTS tableid10index on tableid10(TableRowId)'
Execute-Sql $cmm 'CREATE INDEX IF NOT EXISTS tabledata10index on tabledata10(DataId)'
Execute-Sql $cmm "CREATE TABLE IF NOT EXISTS table99 ([Id] INTEGER PRIMARY KEY, [TableRowId] INTEGER, $((1..99 | %{ `"[TEXT$_] TEXT`" }) -join ', '))"
Execute-Sql $cmm 'CREATE INDEX IF NOT EXISTS table99index on table99(TableRowId)'
$cmm.Dispose();
$cnn.Dispose();
$cnn = New-Object Data.SQLite.SQLiteConnection("Data Source=$(Join-Path $mydir 'test.sqlite3')")
try {
$cnn.Open()
$cmm = $cnn.CreateCommand();
$cmm.Parameters.Add('Id', [Data.DbType]::Int64) > $null;
$cmm.Parameters.Add('Type', [Data.DbType]::Int64) > $null;
foreach ($i in 1..99) {
$cmm.Parameters.Add("TEXT$i", [Data.DbType]::String) > $null;
}
try {
if ($Do99) {
Write-Host '::DO99:: '
if ($Write) {
Write-Host 'Insert: '
Measure-Command {
$strsql = "INSERT INTO table99 ([TableRowId], $((1..99 | %{ `"[TEXT$_]`" }) -join ', ')) VALUES(:Id, $((1..99 | %{ `":TEXT$_`" }) -join ', '))"
Execute-Sql $cmm 'BEGIN TRANSACTION'
foreach ($i in 0..($Count - 1)) {
$cmm.Parameters['Id'].Value = $i; # TableRowId
foreach ($j in 1..99) {
$cmm.Parameters["TEXT$j"].Value = $shuffleStr[($i * $Count + $j) % 16KB];
}
Execute-Sql $cmm $strsql
# 1K 毎に トランザクションを切る
if ($i % 1KB -eq 0) {
Execute-Sql $cmm 'END TRANSACTION'
Execute-Sql $cmm 'BEGIN TRANSACTION'
}
}
Execute-Sql $cmm 'END TRANSACTION'
} | Tee-Object -Variable Func99InsertTime | Select-Object TotalSeconds
}
if ($Read) {
Write-Host 'Select: '
$shuffleIds = 0..($Count - 1) | sort -prop @{Exp={[Guid]::NewGuid()}}
Measure-Command {
foreach ($i in $shuffleIds) {
$cmm.Parameters['Id'].Value = $i;
Execute-Sql $cmm 'SELECT * FROM table99 WHERE [TableRowId] = :Id';
}
} | Tee-Object -Variable Func99InsertTime | Select-Object TotalSeconds
}
}
if ($Do10) {
Write-Host '::DO10*10:: '
if ($Write) {
Write-Host 'Insert: '
Measure-Command {
$strsqlA = 'INSERT INTO tableid10 ([TableRowId], [DataId]) VALUES(:Id, :Type)'
$strsqlB = "INSERT INTO tabledata10 ([DataId], [DataTypeId], $((1..10 | %{ `"[TEXT$_]`" }) -join ', ')) VALUES(:Id, :Type, $((1..10 | %{ `":TEXT$_`" }) -join ', '))"
Execute-Sql $cmm 'BEGIN TRANSACTION'
foreach ($i in 0..($Count - 1)) {
$cmm.Parameters['Id'].Value = $i; # TableRowId
$cmm.Parameters['Type'].Value = $i; # DataId
Execute-Sql $cmm $strsqlA
foreach ($j in 0..9) {
$cmm.Parameters['Id'].Value = $i; # DataId
$cmm.Parameters['Type'].Value = $j; # DataTypeId
foreach ($k in 1..10) {
$cmm.Parameters["TEXT$k"].Value = $shuffleStr[(10 * $i * $Count + $j * $i + $k) % 16KB];
}
Execute-Sql $cmm $strsqlB
}
# 1K 毎に トランザクションを切る
if ($i % 1KB -eq 0) {
Execute-Sql $cmm 'END TRANSACTION'
Execute-Sql $cmm 'BEGIN TRANSACTION'
}
}
Execute-Sql $cmm 'END TRANSACTION'
} | Tee-Object -Variable Func99InsertTime | Select-Object TotalSeconds
}
if ($Read) {
Write-Host 'Select: '
$shuffleIds = 0..($Count - 1) | sort -prop @{Exp={[Guid]::NewGuid()}}
Measure-Command {
foreach ($i in $shuffleIds) {
$cmm.Parameters['Id'].Value = $i;
Execute-Sql $cmm 'SELECT * FROM tabledata10 WHERE DataId IN (SELECT DataId FROM tableid10 WHERE [TableRowId] = :Id)';
}
} | Tee-Object -Variable Func99InsertTime | Select-Object TotalSeconds
}
}
if ($Do01) {
Write-Host '::DO1*100:: '
if ($Write) {
Write-Host 'Insert: '
Measure-Command {
$strsqlA = 'INSERT INTO tableid01 ([TableRowId], [DataId]) VALUES(:Id, :Type)'
$strsqlB = 'INSERT INTO tabledata01 ([DataId], [DataTypeId], [TEXT1]) VALUES(:Id, :Type, :TEXT1)'
Execute-Sql $cmm 'BEGIN TRANSACTION'
foreach ($i in 0..($Count - 1)) {
$cmm.Parameters['Id'].Value = $i; # TableRowId
$cmm.Parameters['Type'].Value = $i; # DataId
Execute-Sql $cmm $strsqlA
foreach ($j in 0..99) {
$cmm.Parameters['Id'].Value = $i; # DataId
$cmm.Parameters['Type'].Value = $j; # DataTypeId
$cmm.Parameters['TEXT1'].Value = $shuffleStr[(100 * $i * $Count + $j) % 16KB];
Execute-Sql $cmm $strsqlB
}
# 1K 毎に トランザクションを切る
if ($i % 1KB -eq 0) {
Execute-Sql $cmm 'END TRANSACTION'
Execute-Sql $cmm 'BEGIN TRANSACTION'
}
}
Execute-Sql $cmm 'END TRANSACTION'
} | Tee-Object -Variable Func99InsertTime | Select-Object TotalSeconds
}
if ($Read) {
Write-Host 'Select: '
$shuffleIds = 0..($Count - 1) | sort -prop @{Exp={[Guid]::NewGuid()}}
Measure-Command {
foreach ($i in $shuffleIds) {
$cmm.Parameters['Id'].Value = $i;
Execute-Sql $cmm 'SELECT * FROM tabledata01 WHERE DataId IN (SELECT DataId FROM tableid01 WHERE [TableRowId] = :Id)';
}
} | Tee-Object -Variable Func99InsertTime | Select-Object TotalSeconds
}
}
} finally {
$cmm.Dispose()
}
} finally {
$cnn.Dispose()
}
99フィールドを1レコードに仕舞ってしまっているのが -Do99 で、
ひとつのレコードに データフィールドを10個持つレコードを、10レコード関連づけているのが -Do10
ひとつのレコードに データフィールドを1つだけ持つレコードを、100レコード関連づけているのが -Do01
それぞれの Insert した時間と ランダムに Select した時間を測定するものだ。
1024 レコード、4096 レコード、16384 レコードと、レコード数による変化と、
RAM ディスク上で動かした場合、HDD 上で動かした場合、SSD 上で動かした場合、一度ファイルにアクセスして RAM にキャッシュされた場合など、ストレージによる変化を確認すると、以下の様になる。
1KB
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 1KB TotalSeconds ------------ ::DO99:: Insert: 3.400592 Select: 0.3267183 ::DO10*10:: Insert: 6.8167525 Select: 0.3121771 ::DO1*100:: Insert: 28.8467136 Select: 0.3149239
4KB
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 4KB TotalSeconds ------------ ::DO99:: Insert: 13.5436556 Select: 1.316919 ::DO10*10:: Insert: 27.1984719 Select: 1.2525614 ::DO1*100:: Insert: 115.3996579 Select: 1.2619968
16KB
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 16KB TotalSeconds ------------ ::DO99:: Insert: 53.7108137 Select: 5.2602945 ::DO10*10:: Insert: 146.2867899 Select: 5.0426413 ::DO1*100:: Insert: 639.7420941 Select: 5.0647618
RAM
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 16KB -Read TotalSeconds ------------ ::DO99:: Select: 5.2987277 ::DO10*10:: Select: 5.0834981 ::DO1*100:: Select: 5.0632528
SSD (1回目, キャッシュ無し)
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 16KB -Read TotalSeconds ------------ ::DO99:: Select: 9.3642307 ::DO10*10:: Select: 9.8006135 ::DO1*100:: Select: 10.8584984
SSD (2回目, キャッシュ有り)
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 16KB -Read TotalSeconds ------------ ::DO99:: Select: 5.4711732 ::DO10*10:: Select: 5.261745 ::DO1*100:: Select: 5.2114789
HDD (1回目, キャッシュ無し)
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 16KB -Read TotalSeconds ------------ Select: 39.365767 ::DO10*10:: Select: 53.9342967 ::DO1*100:: Select: 67.7335061
HDD (2回目, キャッシュ有り)
> .\sqlite_performance_check.ps1 -Do01 -Do10 -Do99 -Count 16KB -Read TotalSeconds ------------ Select: 5.1556883 ::DO10*10:: Select: 4.9654451 ::DO1*100:: Select: 4.9695433
レコード数ごとのパフォーマンスは以下の通り。
レコード数に線形に増えているようだ。
さすがに Insert は レコードの数が増えるに従ってどんどん長くなるが、Select のほうはほとんど変わらず、逆にフィールド数が少ない方が早いくらいだ。
レコード数 | 1024 | 4096 | 16384 | |
---|---|---|---|---|
Insert | 99*1 | 3.401 | 13.544 | 53.711 |
10*10 | 6.817 | 27.198 | 146.287 | |
1*100 | 28.847 | 115.400 | 639.742 | |
Select | 99*1 | 0.327 | 1.317 | 5.260 |
10*10 | 0.312 | 1.253 | 5.043 | |
1*100 | 0.315 | 1.262 | 5.065 |
ストレージごとに違いは以下の通り。
さすがに RAM は早いが、数百MB で 余っている RAM にキャッシュされてしまえば、SSD や HDD でも同じ速度が出るようだ。
ストレージが遅い場合は、レコード数が少なくフィールドが多い方が早いが、RAM に読み込んでしまうと、むしろ レコード数が多くてもフィールドが小さい方が早くなるようだ。
ストレージ | RAM | SSD (1回目, キャッシュ無し) | SSD (2回目, キャッシュ有り) | HDD (1回目, キャッシュ無し) | HDD (2回目, キャッシュ有り) | |
---|---|---|---|---|---|---|
Select | 99*1 | 5.299 | 9.364 | 5.471 | 39.366 | 5.156 |
10*10 | 5.083 | 9.801 | 5.262 | 53.934 | 4.965 | |
1*100 | 5.063 | 10.858 | 5.211 | 67.734 | 4.970 |
多フィールド少レコードでも、少フィールド多レコードでも、 Select の速度はそこまで大きく変わらないようだ。
データのフィールドが大きくなってしまう場合は、テーブルを分けたりしてみるても良さそうだ。