1 min read
BackendPlanetScale: Serverless MySQL mit Vitess
PlanetScale für skalierbares MySQL. Database Branching, Non-Blocking Schema Changes und Vitess-powered Performance.
PlanetScaleMySQLVitessServerless DatabaseDatabase BranchingSchema Migrations

PlanetScale: Serverless MySQL mit Vitess
Meta-Description: PlanetScale für skalierbares MySQL. Database Branching, Non-Blocking Schema Changes und Vitess-powered Performance.
Keywords: PlanetScale, MySQL, Vitess, Serverless Database, Database Branching, Schema Migrations, Horizontal Scaling
Einführung
PlanetScale bringt YouTube-Scale zu MySQL. Powered by Vitess (entwickelt für YouTube) bietet es Horizontal Sharding, Non-Blocking Schema Changes und Database Branching – ohne die Komplexität selbst zu managen.
PlanetScale Features
┌─────────────────────────────────────────────────────────────┐
│ PLANETSCALE │
├─────────────────────────────────────────────────────────────┤
│ │
│ Vitess Foundation: │
│ ├── Horizontal Sharding (Auto) │
│ ├── Connection Pooling │
│ ├── Query Routing │
│ └── Petabyte-Scale Proven │
│ │
│ Developer Experience: │
│ ├── Database Branching (wie Git) │
│ ├── Non-Blocking Schema Changes │
│ ├── Schema Revert (1-Click Rollback) │
│ └── PlanetScale Insights (Query Analytics) │
│ │
│ Serverless Benefits: │
│ ├── No Cold Starts │
│ ├── Auto-Scaling │
│ ├── Per-Query Pricing │
│ └── Edge-Compatible Driver │
│ │
│ Security: │
│ ├── No Foreign Keys (by Design - Performance) │
│ ├── Branch Permissions │
│ └── Audit Logs │
│ │
└─────────────────────────────────────────────────────────────┘Setup
# PlanetScale CLI installieren
brew install planetscale/tap/pscale
# Login
pscale auth login
# Database erstellen
pscale database create my-app --region eu-west
# Branch erstellen
pscale branch create my-app main
# Local Proxy (Development)
pscale connect my-app main --port 3309# Dependencies
npm install @planetscale/databaseDatabase Connection
// lib/planetscale.ts
import { connect } from '@planetscale/database';
const config = {
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
};
export const conn = connect(config);
// Für Edge (Fetch-basiert)
export async function query<T>(
sql: string,
args?: unknown[]
): Promise<T[]> {
const results = await conn.execute(sql, args);
return results.rows as T[];
}// Mit Drizzle ORM
import { drizzle } from 'drizzle-orm/planetscale-serverless';
import { connect } from '@planetscale/database';
import * as schema from './schema';
const connection = connect({
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
});
export const db = drizzle(connection, { schema });Schema Definition (Drizzle)
// src/db/schema.ts
import {
mysqlTable,
varchar,
text,
boolean,
timestamp,
int,
mysqlEnum
} from 'drizzle-orm/mysql-core';
export const users = mysqlTable('users', {
id: varchar('id', { length: 36 }).primaryKey(),
email: varchar('email', { length: 255 }).notNull().unique(),
name: varchar('name', { length: 255 }),
role: mysqlEnum('role', ['user', 'admin']).default('user'),
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
});
export const posts = mysqlTable('posts', {
id: varchar('id', { length: 36 }).primaryKey(),
title: varchar('title', { length: 255 }).notNull(),
content: text('content'),
slug: varchar('slug', { length: 255 }).notNull().unique(),
published: boolean('published').default(false),
authorId: varchar('author_id', { length: 36 }).notNull(),
// Kein FOREIGN KEY - PlanetScale Design Decision
createdAt: timestamp('created_at').defaultNow(),
updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
});
// Index für Performance
export const postsAuthorIdx = mysqlTable('posts', {
authorId: varchar('author_id', { length: 36 })
}).indexes((t) => ({
authorIdx: index('author_idx').on(t.authorId)
}));CRUD Operations
import { db } from '@/db';
import { users, posts } from '@/db/schema';
import { eq, and, desc, like, sql } from 'drizzle-orm';
import { nanoid } from 'nanoid';
// CREATE
async function createUser(email: string, name: string) {
const id = nanoid();
await db.insert(users).values({
id,
email,
name
});
return { id, email, name };
}
// READ
async function getUserById(id: string) {
const [user] = await db
.select()
.from(users)
.where(eq(users.id, id));
return user;
}
// Manueller JOIN (ohne Foreign Keys)
async function getPostsWithAuthors() {
return await db
.select({
post: posts,
author: {
id: users.id,
name: users.name,
email: users.email
}
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt));
}
// UPDATE
async function updatePost(id: string, data: Partial<typeof posts.$inferInsert>) {
await db
.update(posts)
.set(data)
.where(eq(posts.id, id));
}
// DELETE
async function deletePost(id: string) {
await db.delete(posts).where(eq(posts.id, id));
}
// Pagination
async function getPostsPaginated(page: number, pageSize: number = 20) {
const offset = (page - 1) * pageSize;
const [results, countResult] = await Promise.all([
db
.select()
.from(posts)
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
.limit(pageSize)
.offset(offset),
db
.select({ count: sql<number>`count(*)` })
.from(posts)
.where(eq(posts.published, true))
]);
return {
posts: results,
total: countResult[0].count,
page,
pageSize,
totalPages: Math.ceil(countResult[0].count / pageSize)
};
}Database Branching Workflow
# 1. Feature Branch erstellen
pscale branch create my-app add-comments
# 2. Lokal verbinden
pscale connect my-app add-comments --port 3310
# 3. Schema ändern (auf Branch)
# In Code oder SQL:// Schema-Migration auf Branch
await db.execute(sql`
CREATE TABLE comments (
id VARCHAR(36) PRIMARY KEY,
content TEXT NOT NULL,
post_id VARCHAR(36) NOT NULL,
author_id VARCHAR(36) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
INDEX post_idx (post_id),
INDEX author_idx (author_id)
)
`);# 4. Deploy Request erstellen
pscale deploy-request create my-app add-comments
# 5. Review in Dashboard
# Schema Diff wird angezeigt
# 6. Deploy (Non-Blocking!)
pscale deploy-request deploy my-app 1
# 7. Branch löschen
pscale branch delete my-app add-commentsNon-Blocking Schema Changes
-- Diese Änderungen blockieren NICHT die Tabelle:
-- Column hinzufügen
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(255);
-- Index hinzufügen (online)
ALTER TABLE posts ADD INDEX slug_idx (slug);
-- Column umbenennen (via Ghost Tables)
-- PlanetScale kopiert Daten im Hintergrund
-- Achtung: Foreign Keys nicht unterstützt!
-- Stattdessen: Application-Level Referential IntegrityPlanetScale Insights
// Query Performance analysieren
// In PlanetScale Dashboard: Insights Tab
// Slow Queries finden
// - Queries > 100ms
// - Table Scans
// - Missing Indexes
// Beispiel: Index für häufige Query
await db.execute(sql`
CREATE INDEX posts_published_created_idx
ON posts (published, created_at DESC)
`);
// Composite Index für Filter + Sort
await db.execute(sql`
CREATE INDEX posts_author_published_idx
ON posts (author_id, published, created_at DESC)
`);Edge-Compatible Driver
// Cloudflare Workers / Vercel Edge
import { connect } from '@planetscale/database';
export const runtime = 'edge';
export async function GET() {
const conn = connect({
host: process.env.DATABASE_HOST,
username: process.env.DATABASE_USERNAME,
password: process.env.DATABASE_PASSWORD
});
const results = await conn.execute(
'SELECT * FROM posts WHERE published = ? ORDER BY created_at DESC LIMIT 10',
[true]
);
return Response.json(results.rows);
}Referential Integrity ohne Foreign Keys
// Application-Level Constraints
// Bei Insert: Prüfen ob Author existiert
async function createPost(data: NewPost) {
const [author] = await db
.select({ id: users.id })
.from(users)
.where(eq(users.id, data.authorId));
if (!author) {
throw new Error('Author not found');
}
return await db.insert(posts).values(data);
}
// Bei Delete: Cascade manuell
async function deleteUser(userId: string) {
// Erst abhängige Daten löschen
await db.delete(posts).where(eq(posts.authorId, userId));
await db.delete(comments).where(eq(comments.authorId, userId));
// Dann User
await db.delete(users).where(eq(users.id, userId));
}
// Oder: Soft Delete Pattern
const users = mysqlTable('users', {
// ...
deletedAt: timestamp('deleted_at')
});Fazit
PlanetScale bietet:
- Vitess-Powered: YouTube-Scale für MySQL
- Database Branching: Git-Workflow für Datenbanken
- Zero-Downtime Migrations: Non-Blocking Schema Changes
- Edge-Ready: Fetch-basierter Driver für Serverless
Enterprise MySQL-Performance mit Developer-Friendly Workflow.
Bildprompts
- "Database branches merging like Git, version control for databases"
- "MySQL scaling horizontally across servers, Vitess sharding concept"
- "Schema migration without downtime, zero-interruption deployment"