# 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 ```bash # 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 ``` ```bash # Dependencies npm install @planetscale/database ``` --- ## Database Connection ```typescript // 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( sql: string, args?: unknown[] ): Promise { const results = await conn.execute(sql, args); return results.rows as T[]; } ``` ```typescript // 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) ```typescript // 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 ```typescript 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) { 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`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 ```bash # 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: ``` ```typescript // 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) ) `); ``` ```bash # 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-comments ``` --- ## Non-Blocking Schema Changes ```sql -- 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 Integrity ``` --- ## PlanetScale Insights ```typescript // 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 ```typescript // 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 ```typescript // 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: 1. **Vitess-Powered**: YouTube-Scale für MySQL 2. **Database Branching**: Git-Workflow für Datenbanken 3. **Zero-Downtime Migrations**: Non-Blocking Schema Changes 4. **Edge-Ready**: Fetch-basierter Driver für Serverless Enterprise MySQL-Performance mit Developer-Friendly Workflow. --- ## Bildprompts 1. "Database branches merging like Git, version control for databases" 2. "MySQL scaling horizontally across servers, Vitess sharding concept" 3. "Schema migration without downtime, zero-interruption deployment" --- ## Quellen - [PlanetScale Documentation](https://planetscale.com/docs) - [PlanetScale Branching](https://planetscale.com/docs/vitess/schema-changes/branching) - [Vitess Overview](https://vitess.io/) - [PlanetScale Database Driver](https://github.com/planetscale/database-js)