Neon サーバーレスPostgreSQL完全ガイド - 次世代のデータベースプラットフォーム
Neon は、従来のデータベースの概念を覆すサーバーレスPostgreSQLプラットフォームです。自動スケーリング、ブランチング、そして完全なPostgreSQL互換性を提供し、現代のアプリケーション開発に最適化されています。
この記事では、Neonの基本から実践的な活用方法まで詳しく解説します。
Neon とは
Neon は、ストレージとコンピュートを分離したアーキテクチャを採用する、フルマネージドのPostgreSQLサービスです。
主な特徴
- サーバーレスアーキテクチャ: 使用した分だけ課金
- 瞬時のスケーリング: 0〜無限まで自動スケール
- データベースブランチング: Git風のブランチ作成
- 完全なPostgreSQL互換: 標準のPostgreSQLツールが使える
- 高速なコールドスタート: 数百ミリ秒で起動
はじめに
アカウント作成とプロジェクトセットアップ
# Neon CLIのインストール
npm install -g neonctl
# ログイン
neonctl auth login
# プロジェクトの作成
neonctl project create --name my-app
# 接続文字列の取得
neonctl connection-string
Web UIでの作成
- https://neon.tech にアクセス
- GitHubアカウントでサインイン
- “New Project” をクリック
- プロジェクト名とリージョンを選択
- 接続情報をコピー
データベース接続
Node.js での接続
npm install @neondatabase/serverless
// serverless.js - エッジ環境対応
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
async function getUsers() {
const users = await sql`SELECT * FROM users`;
return users;
}
// トランザクション
async function createUser(name, email) {
return await sql.transaction([
sql`INSERT INTO users (name, email) VALUES (${name}, ${email})`,
sql`INSERT INTO audit_log (action) VALUES ('user_created')`
]);
}
プール接続を使う場合
import { Pool } from '@neondatabase/serverless';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
async function queryWithPool() {
const client = await pool.connect();
try {
const result = await client.query('SELECT NOW()');
return result.rows[0];
} finally {
client.release();
}
}
Drizzle ORMとの統合
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
// db/schema.ts
import { pgTable, serial, text, timestamp } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
name: text('name').notNull(),
email: text('email').notNull().unique(),
createdAt: timestamp('created_at').defaultNow(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
userId: serial('user_id').references(() => users.id),
createdAt: timestamp('created_at').defaultNow(),
});
// db/index.ts
import { drizzle } from 'drizzle-orm/neon-http';
import { neon } from '@neondatabase/serverless';
import * as schema from './schema';
const sql = neon(process.env.DATABASE_URL!);
export const db = drizzle(sql, { schema });
// 使用例
import { db } from './db';
import { users, posts } from './db/schema';
import { eq } from 'drizzle-orm';
// ユーザーの作成
async function createUser(name: string, email: string) {
const [user] = await db.insert(users)
.values({ name, email })
.returning();
return user;
}
// ユーザーと投稿を取得
async function getUserWithPosts(userId: number) {
return await db.query.users.findFirst({
where: eq(users.id, userId),
with: {
posts: true,
},
});
}
Prisma との統合
npm install prisma @prisma/client
npx prisma init
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
url = env("DATABASE_URL")
}
model User {
id Int @id @default(autoincrement())
name String
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id Int @id @default(autoincrement())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId Int
createdAt DateTime @default(now())
}
# マイグレーションの実行
npx prisma migrate dev --name init
# Prisma Clientの生成
npx prisma generate
// 使用例
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
// ユーザーと投稿を同時に作成
const user = await prisma.user.create({
data: {
name: 'Alice',
email: 'alice@example.com',
posts: {
create: [
{ title: 'First Post', content: 'Hello World' },
],
},
},
include: {
posts: true,
},
});
console.log(user);
}
ブランチング機能
Neonの最大の特徴の一つが、データベースのブランチング機能です。
ブランチの作成
# 本番環境のブランチを作成
neonctl branch create --name production
# 開発用ブランチを作成(本番のコピー)
neonctl branch create --name dev --parent production
# 機能開発用ブランチ
neonctl branch create --name feature/user-auth --parent dev
プレビュー環境との統合
Vercelなどのプレビュー環境と組み合わせると強力です。
# .github/workflows/preview.yml
name: Deploy Preview
on:
pull_request:
types: [opened, synchronize]
jobs:
preview:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Create Neon Branch
id: neon
run: |
BRANCH_NAME="preview/pr-${{ github.event.pull_request.number }}"
neonctl branch create --name $BRANCH_NAME --parent main
CONNECTION_STRING=$(neonctl connection-string $BRANCH_NAME)
echo "::set-output name=db_url::$CONNECTION_STRING"
- name: Run Migrations
env:
DATABASE_URL: ${{ steps.neon.outputs.db_url }}
run: |
npm install
npx prisma migrate deploy
- name: Deploy to Vercel
env:
DATABASE_URL: ${{ steps.neon.outputs.db_url }}
run: |
vercel deploy --env DATABASE_URL="$DATABASE_URL"
ブランチの管理
# ブランチ一覧
neonctl branch list
# ブランチの削除
neonctl branch delete feature/user-auth
# ブランチのリセット(特定の時点に戻す)
neonctl branch reset --timestamp "2025-02-05 10:00:00"
スケーリングと最適化
自動スケーリングの設定
# コンピュートの設定
neonctl project update --autoscaling-limit-min-cu 0.25 --autoscaling-limit-max-cu 4
Neonは使用状況に応じて自動的にスケールします。
- 最小CU (0.25): アイドル時のコスト削減
- 最大CU (4): トラフィックピーク時の対応
接続プーリング
// 接続プール設定
import { Pool } from '@neondatabase/serverless';
const pool = new Pool({
connectionString: process.env.DATABASE_URL,
max: 20, // 最大接続数
idleTimeoutMillis: 30000,
connectionTimeoutMillis: 2000,
});
クエリの最適化
// ❌ N+1問題
async function getUsersWithPostsWrong() {
const users = await sql`SELECT * FROM users`;
for (const user of users) {
user.posts = await sql`SELECT * FROM posts WHERE user_id = ${user.id}`;
}
return users;
}
// ✅ JOINを使う
async function getUsersWithPosts() {
return await sql`
SELECT
users.*,
json_agg(posts.*) as posts
FROM users
LEFT JOIN posts ON posts.user_id = users.id
GROUP BY users.id
`;
}
// ✅ ORMを使う場合(Drizzle)
import { db } from './db';
import { users } from './db/schema';
const usersWithPosts = await db.query.users.findMany({
with: {
posts: true,
},
});
セキュリティとアクセス制御
環境変数の管理
# .env
DATABASE_URL="postgresql://user:password@ep-xxx.us-east-2.aws.neon.tech/neondb?sslmode=require"
# 本番環境用
DATABASE_URL_PRODUCTION="postgresql://..."
# 開発環境用
DATABASE_URL_DEV="postgresql://..."
Row Level Security (RLS)
-- RLSを有効化
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
-- ポリシーの作成:ユーザーは自分の投稿のみ閲覧可能
CREATE POLICY user_posts_policy ON posts
FOR SELECT
USING (auth.uid() = user_id);
-- ポリシーの作成:ユーザーは自分の投稿のみ編集可能
CREATE POLICY user_posts_update_policy ON posts
FOR UPDATE
USING (auth.uid() = user_id);
IPアドレス制限
# Neon CLIでIP制限を設定
neonctl project update --allowed-ips "203.0.113.0/24,198.51.100.0/24"
バックアップと復元
自動バックアップ
Neonは自動的にポイントインタイムリカバリ(PITR)をサポートしています。
# 特定の時点にブランチを作成
neonctl branch create --name recovery --parent main --timestamp "2025-02-05 10:00:00"
手動バックアップ
# pg_dumpを使用
pg_dump $DATABASE_URL > backup.sql
# リストア
psql $DATABASE_URL < backup.sql
モニタリングとログ
クエリのモニタリング
// クエリのログ記録
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL, {
onQuery: (query, params, duration) => {
console.log({
query,
params,
duration: `${duration}ms`,
});
},
});
パフォーマンスメトリクス
Neon Web UIで確認できる項目:
- クエリ実行時間
- 接続数
- データ転送量
- コンピュート使用量
実践例: Next.js アプリケーション
// app/api/users/route.ts
import { neon } from '@neondatabase/serverless';
import { NextResponse } from 'next/server';
const sql = neon(process.env.DATABASE_URL!);
export async function GET() {
try {
const users = await sql`SELECT * FROM users ORDER BY created_at DESC`;
return NextResponse.json(users);
} catch (error) {
return NextResponse.json(
{ error: 'Failed to fetch users' },
{ status: 500 }
);
}
}
export async function POST(request: Request) {
try {
const { name, email } = await request.json();
const [user] = await sql`
INSERT INTO users (name, email)
VALUES (${name}, ${email})
RETURNING *
`;
return NextResponse.json(user, { status: 201 });
} catch (error) {
return NextResponse.json(
{ error: 'Failed to create user' },
{ status: 500 }
);
}
}
// app/api/users/[id]/route.ts
export async function GET(
request: Request,
{ params }: { params: { id: string } }
) {
const userId = parseInt(params.id);
const [user] = await sql`
SELECT * FROM users WHERE id = ${userId}
`;
if (!user) {
return NextResponse.json(
{ error: 'User not found' },
{ status: 404 }
);
}
return NextResponse.json(user);
}
コスト最適化
無料枠の活用
Neonの無料プランには以下が含まれます(2025年2月時点):
- 0.5 GBのストレージ
- 1つのプロジェクト
- 無制限のブランチ
コスト削減のヒント
# 1. 使用していないブランチの削除
neonctl branch list
neonctl branch delete old-branch
# 2. 自動スケーリングの最小値を下げる
neonctl project update --autoscaling-limit-min-cu 0.25
# 3. アイドルタイムアウトの設定
neonctl project update --suspend-timeout-seconds 300
モニタリング
// コスト監視のための簡易スクリプト
import { neon } from '@neondatabase/serverless';
const sql = neon(process.env.DATABASE_URL);
async function analyzeUsage() {
const stats = await sql`
SELECT
schemaname,
tablename,
pg_size_pretty(pg_total_relation_size(schemaname||'.'||tablename)) AS size
FROM pg_tables
WHERE schemaname NOT IN ('pg_catalog', 'information_schema')
ORDER BY pg_total_relation_size(schemaname||'.'||tablename) DESC;
`;
console.log('Database Usage:');
console.table(stats);
}
トラブルシューティング
接続エラー
// リトライロジックの実装
async function queryWithRetry(maxRetries = 3) {
for (let i = 0; i < maxRetries; i++) {
try {
return await sql`SELECT * FROM users`;
} catch (error) {
if (i === maxRetries - 1) throw error;
await new Promise(resolve => setTimeout(resolve, 1000 * (i + 1)));
}
}
}
パフォーマンス問題
-- スロークエリの特定
SELECT
query,
calls,
total_time,
mean_time,
max_time
FROM pg_stat_statements
ORDER BY mean_time DESC
LIMIT 10;
-- インデックスの作成
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_id ON posts(user_id);
まとめ
Neon は、以下のような場面で特に威力を発揮します。
- プレビュー環境: PRごとに独立したDBブランチ
- スケーラブルなアプリ: 自動スケーリングで急なトラフィック増にも対応
- コスト効率: 使用した分だけの課金
- 開発速度: 瞬時のブランチ作成で開発サイクルを高速化
従来のデータベース管理の煩雑さから解放され、アプリケーション開発に集中できる環境を提供してくれます。