Yappli Tech Blog

株式会社ヤプリの開発メンバーによるブログです。最新の技術情報からチーム・働き方に関するテーマまで、日々の熱い想いを持って発信していきます。

SQLite3入門

サーバーサイドエンジニアの田実です!

YappliはSQLite3を使ったマルチテナントアーキテクチャを採用しています。
今回は、SQLite3の運用でよく使うコマンドやちょっとマニアックな仕様についてまとめてみました!

CLIでクエリ結果を縦に表示したい

MySQLでいうところの \G 、PostgreSQLでいうところの \x をしたい場合、 .mode line を使って表示モードを切り替えれます。

sqlite> .mode line 
sqlite> select * from hoge;
         id = test
       code = hoge
description = fuga

ちなみに指定できるモードは line 以外にも column list csv などがあります。

.mode MODE ?TABLE?       Set output mode
   MODE is one of:
     ascii    Columns/rows delimited by 0x1F and 0x1E
     csv      Comma-separated values
     column   Left-aligned columns.  (See .width)
     html     HTML <table> code
     insert   SQL insert statements for TABLE
     line     One value per line
     list     Values delimited by "|"
     quote    Escape answers as for SQL
     tabs     Tab-separated values
     tcl      TCL list elements

例えば、CSVエクスポートをしたい場合は以下のようなコマンドを実行します。

$ sqlite3 -header -csv {dbファイル} "SELECT * FROM xxx"

スキーマやテーブル一覧を見たい

CLIの .schema.table コマンドを使ってテーブル情報を確認できます。

# 全テーブルのCREATE TABLE文を出力
.schema

# テーブル指定
.schema hoge

# テーブル一覧
.table

JSON関数を使いたい

JSON1拡張を組み込むとJSON関数が使えるようになります。

SELECT json_extract('{"foo":"bar"}', '$.foo'); 
-- => bar

PostgreSQLの jsonb_array_elements のような配列操作も可能です。

SELECT distinct t.id, t.body 
FROM json_table AS t, json_each(t.body, '$.items') AS v
WHERE json_extract(v.value, '$.foo') = 'bar';

独自関数を使いたい

SQLite3はアプリケーション側でDBファイルの操作を行うため、呼び出し元のプログラミング言語を使った関数を簡単に定義することができます。

PHP(PDO)だと以下のようにして独自関数を定義することができます。

<?php

$db = new \PDO('sqlite::memory:');
$db->sqliteCreateFunction('hello', function($v) {
  return 'hello '.$v;
});
$stmt = $db->query('SELECT hello("world");');
echo $stmt->fetch()[0]; // => hello world

Go(mattn/go-sqlite3)だとこんな感じ。

package main

import (
    "database/sql"
    "fmt"

    sqlite "github.com/mattn/go-sqlite3"
)

func main() {
    sql.Register("sqlite3_custom", &sqlite.SQLiteDriver{
        ConnectHook: func(conn *sqlite.SQLiteConn) error {
            f := func(v string) string { return "hello " + v }
            if err := conn.RegisterFunc("hello", f, true); err != nil {
                return err
            }
            return nil
        },
    })
    db, err := sql.Open("sqlite3_custom", ":memory:")
    if err != nil {
        panic(err)
    }
    defer db.Close()

    var r string
    err = db.QueryRow("SELECT hello('world')").Scan(&r)
    if err != nil {
        panic(err)
    }
    fmt.Println(r) // => hello world
}

比較演算子に正規表現を使いたい

SQLite3には比較演算子のREGEXPがありますが、REGEXPを利用するには事前にユーザ側でregexp()の独自関数を定義する必要があります。

<?php

$db = new \PDO('sqlite::memory:');
$db->sqliteCreateFunction('regexp', function($pattern, $value) {
  mb_regex_encoding('UTF-8');
  return (false !== mb_ereg($pattern, $value)) ? 1 : 0;
});
$stmt = $db->query("SELECT 'hello world' REGEXP '^hello.*'");
echo $stmt->fetch()[0]; // => 1

空き領域を開放したい

他のRDBMSと同様、大量のINSERT&DELETEを繰り返すと空き領域ができてしまいレコード数よりも多くの領域を取ってしまいます。 そこで、SQLite3でもバキュームの仕組みであるvacuumコマンドを使って空き領域を開放することができます。

$ wc -c test.db
38989824 test.db

$ sqlite3 test.db 'vacuum'

$ wc -c test.db
8192 test.db

ジャーナルモード

障害復旧時に利用されるロールバックジャーナルに関するジャーナルモードを選択できます。

モード 説明
DELETE ロールバックジャーナルである -journal ファイルを作成し
コミット後に -journal ファイルを削除します。
TRUNCATE DELETEモードとほぼ同じですが、コミット後は -journal ファイルを削除せず
サイズ0のファイルにします。*1
WAL -shm -wal ファイルを作成し、コミット内容は -wal ファイルに反映されます。

WALの場合は実行速度が早くコンカレントな実行ができるのがメリットですが、本体+ -wal ファイルでDBの状態になるので扱い方に注意が必要です。*2

上記以外にもインメモリにロールバックジャーナルを保存する MEMORY やロールバックジャーナルを作成しない=アトミックな処理が行えない OFF というモードがあったりします(OFFモードの利用用途はあるんだろうか…w)

ちなみに、YappliではSQLite3ファイルのコピーを簡単にするため、 WALではなくDELETE/TRUNCATEを採用しています。

PRAGMA journal_mode=TRUNATE;

読み込みモードについて

SQLite3のファイルオープン時にクエリパラメータを指定することで、読み込みモードを指定することができます。

example.sqlite?mode=ro

以下のEFS移行では、SQLite3がReadOnly(mode=ro)でもファイルロックを行うため、DBの読み取りのパフォーマンスが落ちてしまう問題が発生していました。 tech.yappli.io

こちらの場合、nolockパラメータを設定することでファイルロックを行わないようにしてパフォーマンスの改善を行いました。

ファイルオープン時に指定できるパラメータは以下に記載されています。

www.sqlite.org

ジャーナルモードのようにPRAGMAで設定できるパラメータもあります。

www.sqlite.org

まとめ

SQLite3の基本的なものからちょっとマニアックな情報までご紹介しました! YappliではSQLite3をプロダクションに導入しており、今のところ大きな問題なく(?)運用できています。*3

この記事が皆様のSQLite3ライフに役立てば幸いです!w

*1:DELETEモードのファイル削除よりは高速らしい

*2:例えばバックアップを取る際に -wal ファイルも取っておく必要がある

*3:入社する前はSQLite3?って思ったけど結構いけます