Neon

データベースPostgreSQLサーバーレスブランチングスケールトゥゼロタイムトラベルクラウドネイティブインスタントプロビジョニング

データベースプラットフォーム

Neon

概要

Neonは、ストレージとコンピュートを分離したサーバーレスPostgreSQLプラットフォームです。Git風のデータベースブランチング、自動的なスケールトゥゼロ、タイムトラベルクエリ、インスタントプロビジョニングなどの革新的な機能を提供します。従来のデータベース管理の複雑さを排除し、開発者がアプリケーション構築に専念できる環境を実現します。

詳細

ストレージとコンピュートの分離

Neonは、PostgreSQLのストレージレイヤーとコンピュートレイヤーを完全に分離することで、独立したスケーリングと柔軟なリソース管理を可能にしています。この革新的なアーキテクチャにより、コンピュートリソースは必要な時にのみ使用され、ストレージは独立して管理されます。

データベースブランチング

Neonの最も特徴的な機能の一つは、データベースブランチングです。Git風のコピーオンライト機構により、本番データベースの完全なコピーを瞬時に作成できます。各ブランチはスキーマとデータの両方を含み、開発、テスト、ステージング環境を最小限のオーバーヘッドで作成できます。

スケールトゥゼロ

アプリケーションのアイドル時間に対応して、コンピュートリソースを自動的にゼロまでスケールダウンします。この機能により、使用していない時のコストを大幅に削減でき、競合他社と比較して最大10倍のコスト削減を実現できます。

タイムトラベルとポイントインタイムリカバリ

データベースの任意の時点の状態にアクセスできるタイムトラベル機能を提供します。これにより、データの履歴を確認したり、特定の時点の状態を復元したりすることが可能です。

メリット・デメリット

メリット

  • 真のサーバーレス: 使用した分だけ課金され、アイドル時はコストゼロ
  • インスタントブランチング: データベースの完全なコピーを数秒で作成
  • 自動スケーリング: 負荷に応じて自動的にリソースを調整
  • PostgreSQL完全互換: 既存のPostgreSQLアプリケーションとの完全な互換性
  • 開発者フレンドリー: シンプルなAPIと豊富なSDKサポート
  • ブランチリセット機能: 開発ブランチを最新の本番状態に簡単に同期
  • グローバル展開: エッジロケーションでの低レイテンシアクセス

デメリット

  • 比較的新しいサービス: 2024年4月に一般提供開始と歴史が浅い
  • コールドスタート: スケールトゥゼロからの復帰時に若干の遅延
  • ストレージ制限: 無料プランではストレージとコンピュートに制限
  • 単一ライターノード: 書き込みは垂直スケールのみ対応
  • エンタープライズ機能: 一部の高度な機能は有料プランのみ

参考ページ

実装の例

セットアップ

# Neon CLIをインストール
npm install -g neonctl

# アカウントにログイン
neonctl auth

# プロジェクトを作成
neonctl projects create --name my-project

# 接続情報を取得
neonctl connection-string my-project

データベースブランチング

# メインブランチから新しいブランチを作成
neonctl branches create --project-id <project-id> --name development

# 特定の時点からブランチを作成
neonctl branches create \
  --project-id <project-id> \
  --name feature-branch \
  --parent main \
  --timestamp "2024-12-01T10:00:00Z"

# ブランチの一覧を確認
neonctl branches list --project-id <project-id>

# ブランチをリセット(親ブランチの最新状態に同期)
neonctl branches reset --project-id <project-id> --branch development

接続とクエリ実行

import { Pool } from '@neondatabase/serverless';

// 接続プールの作成
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// 基本的なクエリ実行
async function getUsers() {
  const client = await pool.connect();
  try {
    const result = await client.query('SELECT * FROM users');
    return result.rows;
  } finally {
    client.release();
  }
}

// トランザクション処理
async function createUserWithProfile(email, name) {
  const client = await pool.connect();
  try {
    await client.query('BEGIN');
    
    const userResult = await client.query(
      'INSERT INTO users (email) VALUES ($1) RETURNING id',
      [email]
    );
    const userId = userResult.rows[0].id;
    
    await client.query(
      'INSERT INTO profiles (user_id, name) VALUES ($1, $2)',
      [userId, name]
    );
    
    await client.query('COMMIT');
    return userId;
  } catch (error) {
    await client.query('ROLLBACK');
    throw error;
  } finally {
    client.release();
  }
}

// Vercel Edge Functionでの使用例
export default async function handler(req) {
  const pool = new Pool({
    connectionString: process.env.DATABASE_URL,
  });

  try {
    const { rows } = await pool.query('SELECT NOW()');
    return new Response(JSON.stringify({ time: rows[0].now }), {
      headers: { 'Content-Type': 'application/json' },
    });
  } finally {
    await pool.end();
  }
}

スケーリングとパフォーマンス

// 読み取りレプリカを使用した負荷分散
import { neon } from '@neondatabase/serverless';

// プライマリ接続(書き込み用)
const sqlWrite = neon(process.env.DATABASE_URL);

// 読み取りレプリカ接続(読み取り専用)
const sqlRead = neon(process.env.DATABASE_URL_REPLICA);

// 書き込み操作
async function createPost(title, content) {
  const [post] = await sqlWrite`
    INSERT INTO posts (title, content, created_at)
    VALUES (${title}, ${content}, NOW())
    RETURNING *
  `;
  return post;
}

// 読み取り操作(レプリカ使用)
async function getPosts(limit = 10) {
  const posts = await sqlRead`
    SELECT * FROM posts
    ORDER BY created_at DESC
    LIMIT ${limit}
  `;
  return posts;
}

// 接続プーリングの最適化
const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
  max: 20, // 最大接続数
  idleTimeoutMillis: 30000, // アイドルタイムアウト
  connectionTimeoutMillis: 2000, // 接続タイムアウト
});

タイムトラベルクエリ

-- 特定の時点のデータを確認
SELECT * FROM orders AS OF SYSTEM TIME '2024-12-01 10:00:00';

-- 過去24時間の変更を追跡
SELECT 
  o1.*,
  o2.status as previous_status
FROM 
  orders o1
  LEFT JOIN orders AS OF SYSTEM TIME (NOW() - INTERVAL '24 hours') o2
  ON o1.id = o2.id
WHERE 
  o1.updated_at > NOW() - INTERVAL '24 hours'
  AND o1.status != COALESCE(o2.status, '');

-- 削除されたレコードの復元
INSERT INTO orders 
SELECT * FROM orders AS OF SYSTEM TIME '2024-12-01 09:00:00'
WHERE id = 12345;

監視とデバッグ

// クエリパフォーマンスの監視
const { Pool } = require('@neondatabase/serverless');

const pool = new Pool({
  connectionString: process.env.DATABASE_URL,
});

// クエリ実行時間の計測
async function executeWithTiming(query, params = []) {
  const start = Date.now();
  const client = await pool.connect();
  
  try {
    const result = await client.query(query, params);
    const duration = Date.now() - start;
    
    console.log({
      query: query.substring(0, 50) + '...',
      duration: `${duration}ms`,
      rowCount: result.rowCount,
    });
    
    return result;
  } finally {
    client.release();
  }
}

// 接続状態の監視
pool.on('error', (err, client) => {
  console.error('Unexpected error on idle client', err);
});

pool.on('connect', (client) => {
  console.log('New client connected');
});

pool.on('acquire', (client) => {
  console.log('Client acquired from pool');
});

CI/CDパイプラインでの活用

# GitHub Actions での例
name: Database Tests

on:
  pull_request:
    branches: [main]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
      - uses: actions/checkout@v3
      
      - name: Create test branch
        run: |
          npx neonctl branches create \
            --project-id ${{ secrets.NEON_PROJECT_ID }} \
            --name pr-${{ github.event.pull_request.number }} \
            --compute \
            --type read_write
      
      - name: Get connection string
        id: connection
        run: |
          CONNECTION_STRING=$(npx neonctl connection-string \
            ${{ secrets.NEON_PROJECT_ID }} \
            --branch pr-${{ github.event.pull_request.number }})
          echo "DATABASE_URL=$CONNECTION_STRING" >> $GITHUB_OUTPUT
      
      - name: Run migrations
        env:
          DATABASE_URL: ${{ steps.connection.outputs.DATABASE_URL }}
        run: npm run db:migrate
      
      - name: Run tests
        env:
          DATABASE_URL: ${{ steps.connection.outputs.DATABASE_URL }}
        run: npm test
      
      - name: Cleanup branch
        if: always()
        run: |
          npx neonctl branches delete \
            pr-${{ github.event.pull_request.number }} \
            --project-id ${{ secrets.NEON_PROJECT_ID }}