SQLiteでPythonアプリにローカルデータベースを組み込む実践ガイド

Pythonの標準ライブラリsqlite3を使えば、外部インストール不要でローカルデータベースを実装できます。この記事では、基本的なデータベース操作から実務レベルのエラーハンドリングまで、すぐに仕事で使えるコード例を解説します。

SQLiteがPythonアプリに最適な3つの理由

SQLiteは単一ファイルで完結するため、サーバー構築やセットアップが不要です。Pythonのsqlite3モジュールは標準ライブラリに含まれているため、追加インストール不要。小〜中規模のデータ管理、モバイルアプリやデスクトップアプリケーション、プロトタイピング段階で特に活躍します。

ただし、大規模な並行アクセスやネットワーク経由でのアクセスが必要な場合は、PostgreSQLやMySQLの導入を検討してください。

最初のデータベース作成と接続

基本的な接続コード

以下のコードで、新規データベースを作成し、テーブルを定義します。

import sqlite3

# データベースに接続(ファイルが存在しなければ作成)
conn = sqlite3.connect('app.db')
cursor = conn.cursor()

# テーブルを作成
cursor.execute('''
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        email TEXT UNIQUE NOT NULL,
        created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
    )
''')

# 変更をコミット
conn.commit()

# 接続を閉じる
conn.close()

このコードを実行すると、カレントディレクトリにapp.dbというファイルが生成されます。既に存在する場合は、既存のデータベースに接続します。

よくあるハマりポイント:COMMITを忘れてしまう

SQLiteではINSERTUPDATEDELETE操作後にconn.commit()を実行しないと、変更が保存されません。テスト環境で「データが保存されていない」というトラブルの90%がこれです。必ずcommitまたはコンテキストマネージャーを使用してください。

CRUDオペレーションの実装

データの挿入(CREATE)

import sqlite3

conn = sqlite3.connect('app.db')
cursor = conn.cursor()

# 単一レコード挿入
cursor.execute('''
    INSERT INTO users (name, email) VALUES (?, ?)
''', ('田中太郎', 'tanaka@example.com'))

# 複数レコード挿入
users_data = [
    ('佐藤花子', 'sato@example.com'),
    ('鈴木次郎', 'suzuki@example.com'),
]
cursor.executemany('''
    INSERT INTO users (name, email) VALUES (?, ?)
''', users_data)

conn.commit()
conn.close()

セキュリティ注意: プレースホルダー?を使用してください。文字列結合でSQLを組み立てるとSQLインジェクション攻撃に脆弱になります。

データの取得(READ)

conn = sqlite3.connect('app.db')
cursor = conn.cursor()

# すべてのデータを取得
cursor.execute('SELECT * FROM users')
all_users = cursor.fetchall()
for user in all_users:
    print(user)

# 単一レコード取得
cursor.execute('SELECT * FROM users WHERE email = ?', ('tanaka@example.com',))
user = cursor.fetchone()
print(user)

# 条件付き複数取得
cursor.execute('SELECT name, email FROM users WHERE id > ?', (1,))
recent_users = cursor.fetchall()

conn.close()

fetchone()は最初の1件、fetchall()はすべてのレコード、fetchmany(n)は最初のn件を返します。大量データの処理時はメモリ効率の観点からfetchmany()の使用を検討してください。

辞書形式でのデータ取得

デフォルトではタプル形式で返されますが、辞書形式が便利な場合があります:

conn = sqlite3.connect('app.db')
conn.row_factory = sqlite3.Row  # 辞書形式に変更
cursor = conn.cursor()

cursor.execute('SELECT * FROM users WHERE id = ?', (1,))
user = cursor.fetchone()

# 辞書のようにアクセス可能
print(user['name'])
print(user['email'])

conn.close()

データの更新(UPDATE)

conn = sqlite3.connect('app.db')
cursor = conn.cursor()

cursor.execute('''
    UPDATE users SET email = ? WHERE name = ?
''', ('new_tanaka@example.com', '田中太郎'))

print(f"更新行数: {cursor.rowcount}")

conn.commit()
conn.close()

データの削除(DELETE)

conn = sqlite3.connect('app.db')
cursor = conn.cursor()

cursor.execute('DELETE FROM users WHERE id = ?', (1,))

print(f"削除行数: {cursor.rowcount}")

conn.commit()
conn.close()

コンテキストマネージャーでスマートなコード管理

トランザクション管理をシンプルにするには、Pythonのコンテキストマネージャーを活用してください:

import sqlite3

def add_user(name, email):
    # with文を抜ける際に自動的にcommitまたはrollback
    with sqlite3.connect('app.db') as conn:
        cursor = conn.cursor()
        try:
            cursor.execute('''
                INSERT INTO users (name, email) VALUES (?, ?)
            ''', (name, email))
            # with文を抜ける時に自動commit
        except sqlite3.IntegrityError:
            print(f"エラー: {email}は既に登録されています")
            # 例外発生時は自動的にロールバック

# 使用例
add_user('山田太郎', 'yamada@example.com')

この方法により、commit()close()の呼び忘れを防げます。

実務でよく使うクエリパターン

ページネーション

def get_users_paginated(page=1, limit=10):
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    
    offset = (page - 1) * limit
    cursor.execute('''
        SELECT * FROM users ORDER BY id DESC LIMIT ? OFFSET ?
    ''', (limit, offset))
    
    users = cursor.fetchall()
    conn.close()
    return users

# 2ページ目を取得(1ページ10件)
page2_users = get_users_paginated(page=2, limit=10)

集計と検索

conn = sqlite3.connect('app.db')
cursor = conn.cursor()

# 登録ユーザー数をカウント
cursor.execute('SELECT COUNT(*) FROM users')
total_users = cursor.fetchone()[0]
print(f"総ユーザー数: {total_users}")

# 特定パターンのメールアドレスで検索
cursor.execute('''
    SELECT * FROM users WHERE email LIKE ?
''', ('%@example.com',))

example_users = cursor.fetchall()

conn.close()

パフォーマンス最適化のコツ

インデックスの活用

頻繁に検索条件に使うカラムにはインデックスを設定します:

conn = sqlite3.connect('app.db')
cursor = conn.cursor()

# emailカラムにインデックスを作成
cursor.execute('CREATE INDEX IF NOT EXISTS idx_email ON users(email)')

conn.commit()
conn.close()

インデックスを設定することで、大量データの検索速度が大幅に向上します。

トランザクション処理で大量挿入を高速化

import sqlite3
import time

def insert_many_fast(data_list):
    conn = sqlite3.connect('app.db')
    cursor = conn.cursor()
    
    start = time.time()
    
    # トランザクション開始
    cursor.execute('BEGIN TRANSACTION')
    
    try:
        for name, email in data_list:
            cursor.execute('''
                INSERT INTO users (name, email) VALUES (?, ?)
            ''', (name, email))
        
        conn.commit()
        print(f"処理時間: {time.time() - start:.2f}秒")
    except Exception as e:
        conn.rollback()
        print(f"エラー: {e}")
    finally:
        conn.close()

# テストデータ
large_data = [
    (f'ユーザー{i}', f'user{i}@example.com') 
    for i in range(1000)
]

insert_many_fast(large_data)

大量データの場合、ひとつずつコミットするより、すべてをトランザクション内で処理してからまとめてコミットすると5〜10倍高速化できます。

エラーハンドリングのベストプラクティス

import sqlite3
from sqlite3 import IntegrityError, OperationalError

def safe_insert_user(name, email):
    try:
        with sqlite3.connect('app.db') as conn:
            cursor = conn.cursor()
            cursor.execute('''
                INSERT INTO users (name, email) VALUES (?, ?)
            ''', (name, email))
    
    except IntegrityError as e:
        # UNIQUE制約違反や NOT NULL違反
        print(f"データ制約エラー: {e}")
        return False
    
    except OperationalError as e:
        # テーブルが存在しないなど
        print(f"操作エラー(テーブル未作成?): {e}")
        return False
    
    except Exception as e:
        # 予期しないエラー
        print(f"予期しないエラー: {e}")
        return False
    
    return True

# 使用例
result = safe_insert_user('新規ユーザー', 'new@example.com')
if result:
    print("挿入成功")
else:
    print("挿入失敗")

SQLiteの使うべき場面と避けるべき場面

SQLiteが適している場面

  • 個人ツール、デスクトップアプリケーション、モバイルアプリの開発
  • プロトタイプやMVP開発で、データベース構築を素早く始めたい
  • ユーザーごとのデータが分離している(複数ユーザーの並行アクセスが少ない)
  • オフライン対応が必要なアプリケーション

SQLiteを避けるべき場面

  • 数千以上の同時接続が予想される本番Webサービス
  • 複数サーバー間でデータ同期が必要
  • 複雑なロック制御やトランザクション分離が必要

Webアプリケーションの本番環境では、PostgreSQLやMariaDBなどのクライアント・サーバー型DBMSをお勧めします。

よくある質問

ネットワークドライブやクラウドストレージ上に置くことは技術的には可能ですが、ロック機構の関係で避けるべきです。ネットワーク遅延やクラッシュによるデータ破損のリスクがあります。複数マシンからのアクセスが必要な場合は、PostgreSQLなどのクライアント・サーバー型DBMSを採用してください。

理論上は大規模データに対応していますが、実際には同時アクセスのボトルネック、メモリ使用量、クエリパフォーマンスの低下が懸念されます。数百MB〜1GB程度が実用的な上限と考えてください。それ以上の規模が予想される場合は、専用DBMSの導入を検討してください。

K
AWS・Python・生成AIを専門とするソフトウェアエンジニア。AI・クラウド・開発ワークフローの実践ガイドを執筆しています。詳しく見る →