Files
Portfolio/blog-posts/46-planetscale-mysql-serverless.md
damjan_savicandClaude Opus 4.5 43484c5023 Add blog posts, cleanup unused files, update components
- Add 100 blog posts covering AI, development, and tech topics
- Add .env.example for environment configuration
- Add accessibility and lighthouse audit scripts
- Remove obsolete SEO reports and temporary files
- Remove dev-dist build artifacts and backup files
- Remove unused portrait images (moved/consolidated elsewhere)
- Update contact form and component improvements

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-25 11:42:11 +01:00

434 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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<T>(
sql: string,
args?: unknown[]
): Promise<T[]> {
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<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
```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)