最終更新:

libSQL + Edge Runtime完全ガイド - SQLiteをエッジで活用する実践的な方法


はじめに

libSQLは、SQLiteをベースにした次世代データベースフォークで、エッジコンピューティングに最適化されています。2026年現在、TursoによるホスティングサービスとCloudflare D1、Vercel PostgresなどEdge Runtime統合が進み、グローバル分散アプリケーションの構築が容易になっています。

libSQLとは

libSQLの特徴:
✅ SQLite互換のフォーク
✅ Edge-first設計
✅ レプリケーション対応
✅ マルチリージョン分散
✅ WebSocket接続
✅ HTTP/REST API
✅ 低レイテンシー(<50ms)
✅ 無料枠が充実(Turso)

エッジでSQLiteを使うメリット

従来のアーキテクチャ:
ユーザー → CDN → Origin Server → Database(単一リージョン)
└─ レイテンシー: 200-500ms

libSQL + Edge:
ユーザー → Edge Runtime → Local libSQL Replica
└─ レイテンシー: 10-50ms

メリット:
✅ 低レイテンシー(10倍以上高速)
✅ 読み取りスケール無限
✅ オフライン耐性
✅ コスト削減
✅ グローバル展開が容易

Turso + libSQLのセットアップ

インストールと初期設定

# Turso CLIインストール
curl -sSfL https://get.tur.so/install.sh | bash

# ログイン
turso auth login

# データベース作成(東京リージョン)
turso db create my-app --location nrt

# 接続URL取得
turso db show my-app

# 認証トークン生成
turso db tokens create my-app

クライアントライブラリセットアップ

npm install @libsql/client
// lib/db.ts
import { createClient } from '@libsql/client';

export const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

環境変数設定

# .env.local
TURSO_DATABASE_URL="libsql://my-app-kokiriho.turso.io"
TURSO_AUTH_TOKEN="eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9..."

Edge Runtime統合パターン

パターン1: Cloudflare Workers + D1

// worker.ts
import { D1Database } from '@cloudflare/workers-types';

export interface Env {
  DB: D1Database;
}

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    // SQLite互換クエリ
    const { results } = await env.DB.prepare(
      'SELECT * FROM users WHERE email = ?'
    )
      .bind('user@example.com')
      .all();

    return Response.json({ users: results });
  },
};
# wrangler.toml
name = "my-worker"
main = "src/worker.ts"
compatibility_date = "2024-01-01"

[[d1_databases]]
binding = "DB"
database_name = "my-database"
database_id = "xxxx-xxxx-xxxx"
// Drizzle ORM統合
import { drizzle } from 'drizzle-orm/d1';
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core';

export const users = sqliteTable('users', {
  id: integer('id').primaryKey(),
  email: text('email').notNull(),
  name: text('name').notNull(),
});

export default {
  async fetch(request: Request, env: Env): Promise<Response> {
    const db = drizzle(env.DB);

    const allUsers = await db.select().from(users);

    return Response.json(allUsers);
  },
};

パターン2: Vercel Edge Functions + Turso

// app/api/users/route.ts
import { createClient } from '@libsql/client';
import { NextRequest, NextResponse } from 'next/server';

export const runtime = 'edge';

const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

export async function GET(request: NextRequest) {
  const { rows } = await db.execute('SELECT * FROM users LIMIT 10');

  return NextResponse.json({ users: rows });
}

export async function POST(request: NextRequest) {
  const { name, email } = await request.json();

  const result = await db.execute({
    sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
    args: [name, email],
  });

  return NextResponse.json({ id: result.lastInsertRowid }, { status: 201 });
}

パターン3: Deno Deploy + Turso

// main.ts
import { Client } from 'https://esm.sh/@libsql/client@0.4.0';

const db = new Client({
  url: Deno.env.get('TURSO_DATABASE_URL')!,
  authToken: Deno.env.get('TURSO_AUTH_TOKEN')!,
});

Deno.serve(async (req) => {
  const url = new URL(req.url);

  if (url.pathname === '/api/users') {
    const { rows } = await db.execute('SELECT * FROM users');
    return Response.json({ users: rows });
  }

  return new Response('Not Found', { status: 404 });
});

レプリケーション戦略

組み込みレプリケーション(Embedded Replicas)

// lib/db-replica.ts
import { createClient } from '@libsql/client';

// プライマリデータベース
export const primaryDB = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// ローカルレプリカ(読み取り専用)
export const replicaDB = createClient({
  url: 'file:./local-replica.db',
  syncUrl: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
  syncInterval: 60, // 60秒ごとに同期
});

// 使い方
async function getUser(id: string) {
  // 読み取りはレプリカから(高速)
  const result = await replicaDB.execute({
    sql: 'SELECT * FROM users WHERE id = ?',
    args: [id],
  });

  return result.rows[0];
}

async function createUser(name: string, email: string) {
  // 書き込みはプライマリへ(一貫性保証)
  const result = await primaryDB.execute({
    sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
    args: [name, email],
  });

  // レプリカを手動同期
  await replicaDB.sync();

  return result.lastInsertRowid;
}

マルチリージョン配置

# 複数リージョンに配置
turso db create my-app \
  --location nrt \
  --location lax \
  --location fra

# レプリカ追加
turso db replicate my-app --location syd

# 現在の配置確認
turso db locations list
// lib/multi-region.ts
import { createClient } from '@libsql/client';

// 自動的に最寄りのリージョンに接続
export const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// 特定リージョン指定
export const nrtDB = createClient({
  url: 'libsql://my-app-nrt.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

export const laxDB = createClient({
  url: 'libsql://my-app-lax.turso.io',
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

同期モード選択

// 即時同期(強整合性)
const db = createClient({
  url: 'file:./local.db',
  syncUrl: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
  syncMode: 'immediate', // 書き込み後即座に同期
});

// 定期同期(結果整合性)
const db = createClient({
  url: 'file:./local.db',
  syncUrl: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
  syncMode: 'interval',
  syncInterval: 300, // 5分ごと
});

// 手動同期(最大制御)
const db = createClient({
  url: 'file:./local.db',
  syncUrl: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
  syncMode: 'manual',
});

// 明示的に同期実行
await db.sync();

実践的なアーキテクチャパターン

パターン1: グローバルEコマースサイト

// app/api/products/route.ts
import { createClient } from '@libsql/client';

export const runtime = 'edge';

const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// 商品一覧取得(読み取り - レプリカから高速)
export async function GET(request: Request) {
  const { searchParams } = new URL(request.url);
  const category = searchParams.get('category');

  const sql = category
    ? 'SELECT * FROM products WHERE category = ? ORDER BY created_at DESC'
    : 'SELECT * FROM products ORDER BY created_at DESC LIMIT 50';

  const { rows } = await db.execute({
    sql,
    args: category ? [category] : [],
  });

  return Response.json({ products: rows });
}

// 在庫更新(書き込み - プライマリへ)
export async function PATCH(request: Request) {
  const { productId, quantity } = await request.json();

  // トランザクション実行
  await db.batch([
    {
      sql: 'UPDATE products SET stock = stock - ? WHERE id = ?',
      args: [quantity, productId],
    },
    {
      sql: 'INSERT INTO stock_history (product_id, quantity, type) VALUES (?, ?, ?)',
      args: [productId, -quantity, 'sale'],
    },
  ]);

  return Response.json({ success: true });
}

パターン2: リアルタイム分析ダッシュボード

// app/api/analytics/route.ts
export const runtime = 'edge';

import { createClient } from '@libsql/client';

const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

export async function GET() {
  // 並列クエリで複数の集計を実行
  const [dailyStats, topProducts, userGrowth] = await Promise.all([
    db.execute(`
      SELECT
        DATE(created_at) as date,
        COUNT(*) as orders,
        SUM(total) as revenue
      FROM orders
      WHERE created_at >= DATE('now', '-30 days')
      GROUP BY DATE(created_at)
      ORDER BY date DESC
    `),
    db.execute(`
      SELECT
        p.name,
        COUNT(oi.id) as sold_count,
        SUM(oi.quantity * oi.price) as total_revenue
      FROM products p
      JOIN order_items oi ON p.id = oi.product_id
      WHERE oi.created_at >= DATE('now', '-7 days')
      GROUP BY p.id
      ORDER BY total_revenue DESC
      LIMIT 10
    `),
    db.execute(`
      SELECT
        DATE(created_at) as date,
        COUNT(*) as new_users
      FROM users
      WHERE created_at >= DATE('now', '-90 days')
      GROUP BY DATE(created_at)
    `),
  ]);

  return Response.json({
    daily: dailyStats.rows,
    topProducts: topProducts.rows,
    userGrowth: userGrowth.rows,
  });
}

パターン3: セッション管理

// lib/session.ts
import { createClient } from '@libsql/client';

const db = createClient({
  url: process.env.TURSO_DATABASE_URL!,
  authToken: process.env.TURSO_AUTH_TOKEN!,
});

// テーブル初期化
await db.execute(`
  CREATE TABLE IF NOT EXISTS sessions (
    id TEXT PRIMARY KEY,
    user_id INTEGER NOT NULL,
    data TEXT NOT NULL,
    expires_at INTEGER NOT NULL
  )
`);

await db.execute(`
  CREATE INDEX IF NOT EXISTS idx_sessions_expires
  ON sessions(expires_at)
`);

export async function createSession(userId: number, data: Record<string, any>) {
  const sessionId = crypto.randomUUID();
  const expiresAt = Date.now() + 7 * 24 * 60 * 60 * 1000; // 7日後

  await db.execute({
    sql: 'INSERT INTO sessions (id, user_id, data, expires_at) VALUES (?, ?, ?, ?)',
    args: [sessionId, userId, JSON.stringify(data), expiresAt],
  });

  return sessionId;
}

export async function getSession(sessionId: string) {
  const result = await db.execute({
    sql: 'SELECT * FROM sessions WHERE id = ? AND expires_at > ?',
    args: [sessionId, Date.now()],
  });

  if (result.rows.length === 0) return null;

  const session = result.rows[0];
  return {
    id: session.id,
    userId: session.user_id,
    data: JSON.parse(session.data as string),
    expiresAt: session.expires_at,
  };
}

export async function deleteSession(sessionId: string) {
  await db.execute({
    sql: 'DELETE FROM sessions WHERE id = ?',
    args: [sessionId],
  });
}

// 期限切れセッション削除(定期実行)
export async function cleanupExpiredSessions() {
  const result = await db.execute({
    sql: 'DELETE FROM sessions WHERE expires_at < ?',
    args: [Date.now()],
  });

  return result.rowsAffected;
}

パフォーマンス最適化

コネクションプーリング

// lib/db-pool.ts
import { createClient, Client } from '@libsql/client';

class DBPool {
  private clients: Client[] = [];
  private readonly maxSize = 10;

  getClient(): Client {
    if (this.clients.length < this.maxSize) {
      const client = createClient({
        url: process.env.TURSO_DATABASE_URL!,
        authToken: process.env.TURSO_AUTH_TOKEN!,
      });
      this.clients.push(client);
      return client;
    }

    // ラウンドロビンで返す
    const client = this.clients.shift()!;
    this.clients.push(client);
    return client;
  }
}

export const dbPool = new DBPool();

バッチ処理

// ❌ 遅い(複数のHTTPリクエスト)
for (const user of users) {
  await db.execute({
    sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
    args: [user.name, user.email],
  });
}

// ✅ 速い(単一バッチリクエスト)
await db.batch(
  users.map((user) => ({
    sql: 'INSERT INTO users (name, email) VALUES (?, ?)',
    args: [user.name, user.email],
  }))
);

プリペアドステートメント

// プリペアドステートメント使用
const stmt = await db.prepare('SELECT * FROM users WHERE id = ?');

for (const id of userIds) {
  const result = await stmt.execute([id]);
  console.log(result.rows);
}

// クエリキャッシュが効く

インデックス最適化

// 複合インデックス作成
await db.execute(`
  CREATE INDEX IF NOT EXISTS idx_orders_user_date
  ON orders(user_id, created_at DESC)
`);

// カバリングインデックス
await db.execute(`
  CREATE INDEX IF NOT EXISTS idx_products_category_price
  ON products(category, price)
`);

// EXPLAINで確認
const plan = await db.execute('EXPLAIN QUERY PLAN SELECT * FROM users WHERE email = ?');
console.log(plan.rows);

セキュリティベストプラクティス

認証トークン管理

// ❌ クライアントサイドで直接使わない
const db = createClient({
  url: process.env.NEXT_PUBLIC_TURSO_URL, // 危険
  authToken: process.env.NEXT_PUBLIC_TURSO_TOKEN, // 危険
});

// ✅ Edge Functionsで使う
// app/api/secure/route.ts
export const runtime = 'edge';

import { createClient } from '@libsql/client';

const db = createClient({
  url: process.env.TURSO_DATABASE_URL!, // サーバーサイドのみ
  authToken: process.env.TURSO_AUTH_TOKEN!, // サーバーサイドのみ
});

SQL injection対策

// ❌ 危険
const email = request.url.searchParams.get('email');
await db.execute(`SELECT * FROM users WHERE email = '${email}'`);

// ✅ 安全(プレースホルダー使用)
await db.execute({
  sql: 'SELECT * FROM users WHERE email = ?',
  args: [email],
});

Row Level Security(RLS)

-- Tursoではアプリケーションレベルで実装
CREATE TABLE posts (
  id INTEGER PRIMARY KEY,
  user_id INTEGER NOT NULL,
  title TEXT,
  content TEXT
);

CREATE INDEX idx_posts_user ON posts(user_id);
// アプリケーション側でフィルタ
export async function getUserPosts(userId: number) {
  const result = await db.execute({
    sql: 'SELECT * FROM posts WHERE user_id = ?',
    args: [userId],
  });

  return result.rows;
}

モニタリングとデバッグ

クエリログ

// lib/db-logger.ts
import { createClient } from '@libsql/client';

export function createLoggedClient() {
  const baseClient = createClient({
    url: process.env.TURSO_DATABASE_URL!,
    authToken: process.env.TURSO_AUTH_TOKEN!,
  });

  return new Proxy(baseClient, {
    get(target, prop) {
      if (prop === 'execute') {
        return async (...args: any[]) => {
          const start = Date.now();
          const result = await target.execute(...args);
          const duration = Date.now() - start;

          console.log('[DB Query]', {
            sql: args[0]?.sql || args[0],
            duration: `${duration}ms`,
            rows: result.rows.length,
          });

          return result;
        };
      }

      return target[prop as keyof typeof target];
    },
  });
}

エラーハンドリング

import { LibsqlError } from '@libsql/client';

try {
  await db.execute({
    sql: 'INSERT INTO users (email) VALUES (?)',
    args: [email],
  });
} catch (error) {
  if (error instanceof LibsqlError) {
    if (error.code === 'SQLITE_CONSTRAINT') {
      return Response.json(
        { error: 'Email already exists' },
        { status: 409 }
      );
    }
  }

  console.error('Database error:', error);
  return Response.json(
    { error: 'Internal server error' },
    { status: 500 }
  );
}

まとめ

libSQL + Edgeの強み

  1. 超低レイテンシー: 10-50msでグローバルアクセス
  2. スケーラビリティ: 読み取りは無限スケール
  3. コスト効率: 無料枠が充実(Turso: 500MB/3DB無料)
  4. 開発体験: SQLite互換で学習コスト低

ベストプラクティス

  • 読み取りはレプリカ、書き込みはプライマリ
  • バッチ処理でネットワークコスト削減
  • インデックスで検索最適化
  • Edge Functionsでトークン保護

いつ使うべきか

最適な用途:

  • グローバルユーザー向けアプリ
  • 低レイテンシーが必須のサービス
  • Jamstack / Edge-first アーキテクチャ
  • オフライン対応PWA

不向きな用途:

  • 大規模分析(100GB超)
  • 強整合性が必須のトランザクション
  • リアルタイムコラボレーション

次のステップ

libSQLとEdge Runtimeで、次世代のグローバル分散アプリケーションを構築しましょう。