Usamos cookies para mejorar tu experiencia en el sitio
CodeWorlds
Volver a colecciones
Guide30 min read

PlanetScale

PlanetScale is a serverless MySQL database built on Vitess with Git-like branching, zero-downtime schema changes, and automatic scaling for modern applications.

PlanetScale - Kompletny Przewodnik po Serverless MySQL

Czym jest PlanetScale?

PlanetScale to serverless MySQL database zbudowana na Vitess - tej samej technologii, która zasila YouTube i obsługuje miliardy zapytań dziennie. PlanetScale wprowadza rewolucyjne podejście do zarządzania bazami danych poprzez branching (jak w Git), zero-downtime schema changes i automatyczne skalowanie.

Tradycyjne bazy danych wymagają ręcznego zarządzania schematami, planowania downtime na migracje i złożonej infrastruktury do skalowania. PlanetScale eliminuje te problemy, oferując doświadczenie developera znane z nowoczesnych narzędzi jak GitHub - tworzysz branch, wprowadzasz zmiany, otwierasz deploy request i mergeujesz bez przestojów.

Dlaczego PlanetScale?

Kluczowe zalety PlanetScale

  1. Database Branching - Izolowane środowiska do developmentu i testów
  2. Zero-downtime schema changes - Migracje bez przestojów produkcji
  3. Automatyczne skalowanie - Od hobby do enterprise bez konfiguracji
  4. Vitess pod spodem - Sprawdzona technologia YouTube
  5. Serverless - Płać za użycie, nie za idle
  6. Edge-ready - Niskie latencje globalnie
  7. Schema insights - AI-powered rekomendacje
  8. Connection pooling - Wbudowany PlanetScale Proxy

PlanetScale vs Tradycyjne MySQL

CechaPlanetScaleMySQL RDSMySQL self-hosted
BranchingTakNieNie
Schema changesZero downtimeDowntimeDowntime
SkalowanieAutomatyczneManualneManualne
ReplikiAutomatyczneManualneManualne
Foreign keysPrisma relationsNativeNative
Connection poolingWbudowanyDodatkowyDodatkowy
BackupyAutomatyczneManualneManualne
CenaUsage-basedInstance-basedHardware

PlanetScale vs Inne Serverless DB

CechaPlanetScaleNeonSupabaseCockroachDB
EngineMySQL/VitessPostgreSQLPostgreSQLCustom
BranchingTakTakNieNie
Free tier5GB3GB500MB5GB
EdgeTakTakNieTak
Schema changesZero downtimeDowntimeDowntimeZero downtime

Kiedy wybrać PlanetScale?

  • Potrzebujesz MySQL - Nie PostgreSQL
  • Częste schema changes - CI/CD dla bazy danych
  • Globalna aplikacja - Edge locations
  • Team workflow - Branching i review
  • Zero-downtime - Krytyczna produkcja

Instalacja i Konfiguracja

PlanetScale CLI

Code
Bash
# macOS
brew install planetscale/tap/pscale

# Linux
curl -sSL https://github.com/planetscale/cli/releases/latest/download/pscale_linux_amd64.tar.gz | tar xz
sudo mv pscale /usr/local/bin/

# Windows (scoop)
scoop bucket add pscale https://github.com/planetscale/scoop-bucket.git
scoop install pscale

# Weryfikacja
pscale version

Logowanie i tworzenie bazy

Code
Bash
# Zaloguj się
pscale auth login

# Stwórz nową bazę
pscale database create my-app --region us-east

# Lista dostępnych regionów
pscale region list

# Lista baz
pscale database list

Dostępne regiony

RegionKodLokalizacja
US Eastus-eastN. Virginia
US Westus-westOregon
EU Westeu-westDublin
Asia Pacificap-southMumbai
Asia Pacificap-northeastTokyo
Australiaap-southeastSydney
South Americasa-eastSão Paulo

Database Branching

Koncepcja branchingu

PlanetScale traktuje bazę danych jak kod - możesz tworzyć izolowane branche do pracy nad nowymi features bez wpływu na produkcję:

Code
Bash
# Stwórz branch do developmentu
pscale branch create my-app feature-auth

# Lista branchy
pscale branch list my-app

# Przełącz się na branch
pscale branch show my-app feature-auth

# Usuń branch
pscale branch delete my-app feature-auth

Workflow z branchami

Code
Bash
# 1. Stwórz branch dla feature
pscale branch create my-app add-user-profile

# 2. Połącz się z branchem lokalnie
pscale connect my-app add-user-profile --port 3309

# 3. Wprowadź zmiany w schemacie
mysql -h 127.0.0.1 -P 3309 -u root < migrations/add_profile.sql

# 4. Stwórz deploy request (jak PR)
pscale deploy-request create my-app add-user-profile

# 5. Przeglądaj zmiany
pscale deploy-request diff my-app 1

# 6. Deploy do main (merge)
pscale deploy-request deploy my-app 1

Branch Types

Code
Bash
# Development branch - do pracy
pscale branch create my-app dev

# Staging branch - do testów
pscale branch create my-app staging

# Production branch - main (domyślny)
# Chroniony, wymaga deploy request

# Feature branch - krótkotrwały
pscale branch create my-app feature-xyz --from staging

Safe Migrations

PlanetScale automatycznie wykrywa niebezpieczne migracje:

Code
SQL
-- ✅ Bezpieczne - dodanie kolumny
ALTER TABLE users ADD COLUMN bio TEXT;

-- ✅ Bezpieczne - dodanie indeksu
CREATE INDEX idx_users_email ON users(email);

-- ⚠️ Wymaga uwagi - zmiana typu kolumny
ALTER TABLE users MODIFY COLUMN age BIGINT;

-- ❌ Niebezpieczne - usunięcie kolumny z danymi
ALTER TABLE users DROP COLUMN legacy_field;

Zero-Downtime Schema Changes

Jak to działa?

PlanetScale używa Online Schema Change (OSC) inspirowanego gh-ost:

  1. Shadow table - Tworzy kopię tabeli z nowym schematem
  2. Data copy - Kopiuje dane w tle
  3. Changelog - Śledzi zmiany podczas kopiowania
  4. Atomic swap - Przełącza tabele atomicznie
  5. Cleanup - Usuwa starą tabelę
Code
SQL
-- Twój DDL
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);

-- PlanetScale wykonuje:
-- 1. CREATE TABLE _users_new LIKE users;
-- 2. ALTER TABLE _users_new ADD COLUMN avatar_url...
-- 3. INSERT INTO _users_new SELECT * FROM users (chunked)
-- 4. RENAME TABLE users TO _users_old, _users_new TO users;
-- 5. DROP TABLE _users_old;

-- Zero downtime dla aplikacji!

Deploy Requests

Code
Bash
# Stwórz deploy request
pscale deploy-request create my-app feature-auth

# Pokaż diff
pscale deploy-request diff my-app 1

# Output:
# -- users table
# +  avatar_url VARCHAR(500)
# +  bio TEXT
# -- INDEX changes
# +  idx_users_email

# Dodaj komentarz
pscale deploy-request comment my-app 1 "Reviewed, LGTM"

# Deploy
pscale deploy-request deploy my-app 1

# Śledź postęp
pscale deploy-request show my-app 1

Schema w różnych środowiskach

Code
Bash
# Compare branches
pscale branch diff my-app main feature-auth

# Promote staging to production
pscale deploy-request create my-app staging --into main

# Revert (stwórz nowy deploy request z odwrotnymi zmianami)
pscale deploy-request revert my-app 1

Połączenie z Aplikacją

Connection Strings

Code
Bash
# Wygeneruj connection string
pscale password create my-app main production-password

# Output:
# HOST: aws.connect.psdb.cloud
# USERNAME: xxxxx
# PASSWORD: pscale_pw_xxxxx
# DATABASE: my-app

# Format connection string
# mysql://USERNAME:PASSWORD@HOST/DATABASE?ssl={"rejectUnauthorized":true}

Lokalne połączenie (development)

Code
Bash
# Otwórz tunel do brancha
pscale connect my-app main --port 3306

# Teraz możesz połączyć się lokalnie
mysql -h 127.0.0.1 -P 3306 -u root

# Lub z aplikacji
DATABASE_URL="mysql://root@127.0.0.1:3306/my-app"

Prisma ORM

Code
Bash
npm install prisma @prisma/client
npx prisma init
prisma/schema.prisma
Prisma
// prisma/schema.prisma
datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"  // Wymagane dla PlanetScale!
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?  @db.Text
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
}
Code
ENV
# .env
DATABASE_URL='mysql://xxxxx:pscale_pw_xxxxx@aws.connect.psdb.cloud/my-app?sslaccept=strict'
Code
Bash
# Push schema do PlanetScale
npx prisma db push

# Generuj client
npx prisma generate
TSlib/prisma.ts
TypeScript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}
Code
TypeScript
// Użycie
import { prisma } from '@/lib/prisma'

// Create
const user = await prisma.user.create({
  data: {
    email: 'user@example.com',
    name: 'John Doe'
  }
})

// Read with relations
const posts = await prisma.post.findMany({
  where: { published: true },
  include: { author: true }
})

// Update
await prisma.user.update({
  where: { id: user.id },
  data: { name: 'Jane Doe' }
})

// Delete
await prisma.post.delete({
  where: { id: postId }
})

Drizzle ORM

Code
Bash
npm install drizzle-orm @planetscale/database
npm install -D drizzle-kit
TSdb/schema.ts
TypeScript
// db/schema.ts
import { mysqlTable, varchar, text, boolean, timestamp, index } from 'drizzle-orm/mysql-core'

export const users = mysqlTable('users', {
  id: varchar('id', { length: 128 }).primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
  emailIdx: index('email_idx').on(table.email)
}))

export const posts = mysqlTable('posts', {
  id: varchar('id', { length: 128 }).primaryKey(),
  title: varchar('title', { length: 255 }).notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  authorId: varchar('author_id', { length: 128 }).notNull(),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
  authorIdx: index('author_idx').on(table.authorId)
}))
TSdb/index.ts
TypeScript
// db/index.ts
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 })
Code
TypeScript
// Użycie
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and } from 'drizzle-orm'

// Create
const [user] = await db.insert(users).values({
  id: crypto.randomUUID(),
  email: 'user@example.com',
  name: 'John'
}).$returningId()

// Read
const allUsers = await db.select().from(users)

// Read with conditions
const publishedPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.published, true))

// Join
const postsWithAuthors = await db
  .select({
    post: posts,
    author: users
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))

// Update
await db
  .update(users)
  .set({ name: 'Jane' })
  .where(eq(users.id, userId))

// Delete
await db.delete(posts).where(eq(posts.id, postId))

PlanetScale Serverless Driver

Code
Bash
npm install @planetscale/database
Code
TypeScript
// Bezpośrednie użycie driver'a
import { connect } from '@planetscale/database'

const config = {
  host: process.env.DATABASE_HOST,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD
}

const conn = connect(config)

// Simple query
const results = await conn.execute('SELECT * FROM users WHERE id = ?', [userId])
console.log(results.rows)

// Insert
await conn.execute(
  'INSERT INTO users (id, email, name) VALUES (?, ?, ?)',
  [crypto.randomUUID(), 'user@example.com', 'John']
)

// Transaction (pseudo - PlanetScale nie wspiera natywnych transactions)
// Użyj Prisma lub Drizzle dla transaction-like behavior

// Batch queries
const [users, posts] = await Promise.all([
  conn.execute('SELECT * FROM users'),
  conn.execute('SELECT * FROM posts WHERE published = 1')
])

Foreign Keys w PlanetScale

Dlaczego brak natywnych FK?

PlanetScale używa Vitess, który nie wspiera foreign key constraints dla lepszego skalowania. Zamiast tego używaj:

  1. Application-level validation - Waliduj w kodzie
  2. Prisma relationMode - Emulowane relacje
  3. Database triggers (ograniczone wsparcie)

Prisma Relations

schema.prisma
Prisma
// schema.prisma
datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"  // Emuluje FK w aplikacji
}

model User {
  id    String @id
  posts Post[]
}

model Post {
  id       String @id
  author   User   @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId String

  @@index([authorId])  // WAŻNE: Dodaj indeks ręcznie!
}

Manualna walidacja

Code
TypeScript
// Bez ORM - ręczna walidacja
async function createPost(authorId: string, title: string) {
  // Sprawdź czy autor istnieje
  const author = await conn.execute(
    'SELECT id FROM users WHERE id = ?',
    [authorId]
  )

  if (author.rows.length === 0) {
    throw new Error('Author not found')
  }

  // Teraz stwórz post
  await conn.execute(
    'INSERT INTO posts (id, author_id, title) VALUES (?, ?, ?)',
    [crypto.randomUUID(), authorId, title]
  )
}

Insights i Analytics

Query Insights

PlanetScale dashboard pokazuje:

  • Slow queries - Zapytania powyżej progu
  • Query patterns - Najczęstsze zapytania
  • Index suggestions - Rekomendacje indeksów
  • Connection stats - Aktywne połączenia
Code
Bash
# CLI insights
pscale database show my-app --web
# Otwiera dashboard w przeglądarce

Schema Recommendations

Code
SQL
-- PlanetScale sugeruje:

-- ⚠️ Missing index detected
-- Query: SELECT * FROM posts WHERE author_id = ?
-- Recommendation:
CREATE INDEX idx_posts_author_id ON posts(author_id);

-- ⚠️ Unused index detected
-- Index: idx_legacy_field
-- Recommendation:
DROP INDEX idx_legacy_field ON users;

Performance Schema

Code
SQL
-- Włącz w dashboard lub:
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

Backup i Recovery

Automatyczne backupy

Code
Bash
# Lista backupów
pscale backup list my-app

# Restore do nowego brancha
pscale backup restore my-app backup-id --branch restored-data

# Eksport backup
pscale backup download my-app backup-id

Point-in-time Recovery

Code
Bash
# Przywróć do konkretnego momentu (Enterprise)
pscale backup restore my-app \
  --timestamp "2024-01-15T10:30:00Z" \
  --branch pitr-restore

Manual Export

Code
Bash
# Eksport przez mysqldump
pscale connect my-app main --port 3306 &
mysqldump -h 127.0.0.1 -P 3306 -u root my-app > backup.sql

# Import
mysql -h 127.0.0.1 -P 3306 -u root my-app < backup.sql

Integracje

Vercel

Code
Bash
# Zainstaluj integrację w Vercel Dashboard
# Lub ręcznie:
vercel env add DATABASE_URL production
vercel env add DATABASE_URL preview

Next.js API Route

TSapp/api/users/route.ts
TypeScript
// app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function GET() {
  const users = await prisma.user.findMany({
    take: 10,
    orderBy: { createdAt: 'desc' }
  })

  return NextResponse.json(users)
}

export async function POST(request: Request) {
  const data = await request.json()

  const user = await prisma.user.create({
    data: {
      email: data.email,
      name: data.name
    }
  })

  return NextResponse.json(user, { status: 201 })
}

Edge Functions

TSapp/api/edge/route.ts
TypeScript
// app/api/edge/route.ts
import { connect } from '@planetscale/database'

export const runtime = 'edge'

const conn = connect({
  host: process.env.DATABASE_HOST!,
  username: process.env.DATABASE_USERNAME!,
  password: process.env.DATABASE_PASSWORD!
})

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const id = searchParams.get('id')

  const result = await conn.execute(
    'SELECT * FROM users WHERE id = ?',
    [id]
  )

  return Response.json(result.rows[0])
}

GitHub Actions CI/CD

.github/workflows/db-migrate.yml
YAML
# .github/workflows/db-migrate.yml
name: Database Migration

on:
  push:
    branches: [main]
    paths: ['prisma/schema.prisma']

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: planetscale/setup-pscale-action@v1
        with:
          version: latest

      - name: Authenticate
        run: pscale auth login --service-token ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}

      - name: Create branch
        run: pscale branch create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }}

      - name: Push schema
        run: |
          npx prisma db push --accept-data-loss
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL_BRANCH }}

      - name: Create deploy request
        run: |
          pscale deploy-request create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }} \
            --org ${{ secrets.PLANETSCALE_ORG }}

Skalowanie i Performance

Read Replicas

Code
Bash
# PlanetScale automatycznie routuje:
# - Writes -> Primary
# - Reads -> Replicas (gdy włączone)

# Włącz read replicas w dashboard
# Settings -> Read Replicas -> Enable

Connection Pooling

Code
TypeScript
// PlanetScale Proxy automatycznie pooluje połączenia
// Dla aplikacji serverless to krytyczne!

// Bez poolingu: każdy request = nowe połączenie
// Z PlanetScale: połączenia są reused

const config = {
  host: process.env.DATABASE_HOST,  // aws.connect.psdb.cloud
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  // Proxy automatycznie zarządza pool
}

Query Optimization

Code
SQL
-- Używaj EXPLAIN
EXPLAIN SELECT * FROM posts WHERE author_id = 'xxx';

-- Dodawaj indeksy
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);

-- Unikaj SELECT *
SELECT id, title, created_at FROM posts WHERE author_id = ?;

-- Używaj LIMIT
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20;

-- Batch operations
INSERT INTO posts (id, title, author_id)
VALUES
  (?, ?, ?),
  (?, ?, ?),
  (?, ?, ?);

Cennik

Hobby (Free)

  • Storage: 5GB
  • Row reads: 1 miliard/miesiąc
  • Row writes: 10 milionów/miesiąc
  • Branches: 1 produkcyjny + 1 dev
  • Backupy: Codzienne

Scaler ($29/miesiąc)

  • Storage: 10GB (+ $2.50/GB)
  • Row reads: 100 miliardów/miesiąc
  • Row writes: 50 milionów/miesiąc
  • Branches: Unlimited
  • Backupy: Co godzinę
  • Read replicas: 1 region

Team ($39/użytkownik/miesiąc)

  • Wszystko z Scaler
  • Audit logs
  • SSO/SAML
  • SOC 2 Type 2
  • Priority support

Enterprise (Custom)

  • Multi-region
  • PITR
  • SLA 99.99%
  • Dedicated support

Best Practices

Naming Conventions

Code
Bash
# Bazy danych
my-app-production
my-app-staging

# Branche
main              # Production
staging           # Pre-production
feature-auth      # Feature branch
hotfix-bug-123    # Hotfix branch

Migration Workflow

Code
Bash
# 1. Local development
pscale connect my-app dev --port 3306
npx prisma db push

# 2. Create feature branch
pscale branch create my-app feature-xyz

# 3. Test on branch
pscale connect my-app feature-xyz --port 3306
# Run tests...

# 4. Create deploy request
pscale deploy-request create my-app feature-xyz

# 5. Review and deploy
pscale deploy-request deploy my-app 1

# 6. Cleanup
pscale branch delete my-app feature-xyz

Security

Code
Bash
# Rotuj hasła regularnie
pscale password delete my-app main old-password
pscale password create my-app main new-password

# Używaj różnych credentials per environment
# production-creds, staging-creds, dev-creds

# Service tokens dla CI/CD
pscale service-token create

FAQ - Najczęściej Zadawane Pytania

Czy mogę używać foreign keys?

Nie natywnie. PlanetScale używa Vitess, który nie wspiera FK constraints. Używaj relationMode = "prisma" w Prisma lub waliduj w aplikacji.

Jak migrować z MySQL RDS?

  1. Export z RDS: mysqldump
  2. Stwórz bazę w PlanetScale
  3. Import: pscale database import
  4. Zaktualizuj connection string
  5. Usuń FK constraints z schematu

Czy PlanetScale wspiera transactions?

Tak, ale z ograniczeniami Vitess. Single-shard transactions działają normalnie. Cross-shard wymagają aplikacyjnej koordynacji.

Jak debugować wolne zapytania?

  1. Sprawdź Insights w dashboard
  2. Użyj EXPLAIN na problematycznych queries
  3. Dodaj sugerowane indeksy
  4. Monitoruj Query Insights

Czy mogę hostować PlanetScale self-hosted?

Nie. PlanetScale to managed service. Dla self-hosted użyj Vitess bezpośrednio.

Jaka jest różnica między branch a database?

Database to cała baza (jak repo). Branch to izolowana kopia schematu (jak git branch). Branche współdzielą dane z parent branch do momentu rozgałęzienia.

Podsumowanie

PlanetScale rewolucjonizuje zarządzanie bazami MySQL:

  • Database branching - workflow jak w Git
  • Zero-downtime migrations - bez przestojów
  • Serverless - automatyczne skalowanie
  • Developer experience - nowoczesne CLI i dashboard
  • Enterprise-ready - Vitess pod spodem

Idealny dla zespołów chcących traktować bazę danych jak kod, z pełnym CI/CD workflow i bez obawy o downtime podczas migracji.


PlanetScale - a complete guide to serverless MySQL

What is PlanetScale?

PlanetScale is a serverless MySQL database built on Vitess - the same technology that powers YouTube and handles billions of queries per day. PlanetScale introduces a revolutionary approach to database management through branching (like Git), zero-downtime schema changes, and automatic scaling.

Traditional databases require manual schema management, planned downtime for migrations, and complex infrastructure for scaling. PlanetScale eliminates these problems by offering a developer experience familiar from modern tools like GitHub - you create a branch, make changes, open a deploy request, and merge without any downtime.

Why PlanetScale?

Key advantages of PlanetScale

  1. Database Branching - Isolated environments for development and testing
  2. Zero-downtime schema changes - Migrations without production downtime
  3. Automatic scaling - From hobby to enterprise without configuration
  4. Vitess under the hood - Battle-tested YouTube technology
  5. Serverless - Pay for usage, not for idle time
  6. Edge-ready - Low latencies globally
  7. Schema insights - AI-powered recommendations
  8. Connection pooling - Built-in PlanetScale Proxy

PlanetScale vs traditional MySQL

FeaturePlanetScaleMySQL RDSMySQL self-hosted
BranchingYesNoNo
Schema changesZero downtimeDowntimeDowntime
ScalingAutomaticManualManual
ReplicasAutomaticManualManual
Foreign keysPrisma relationsNativeNative
Connection poolingBuilt-inAdditionalAdditional
BackupsAutomaticManualManual
PricingUsage-basedInstance-basedHardware

PlanetScale vs other serverless databases

FeaturePlanetScaleNeonSupabaseCockroachDB
EngineMySQL/VitessPostgreSQLPostgreSQLCustom
BranchingYesYesNoNo
Free tier5GB3GB500MB5GB
EdgeYesYesNoYes
Schema changesZero downtimeDowntimeDowntimeZero downtime

When to choose PlanetScale?

  • You need MySQL - Not PostgreSQL
  • Frequent schema changes - CI/CD for your database
  • Global application - Edge locations
  • Team workflow - Branching and review
  • Zero-downtime - Critical production environments

Installation and configuration

PlanetScale CLI

Code
Bash
# macOS
brew install planetscale/tap/pscale

# Linux
curl -sSL https://github.com/planetscale/cli/releases/latest/download/pscale_linux_amd64.tar.gz | tar xz
sudo mv pscale /usr/local/bin/

# Windows (scoop)
scoop bucket add pscale https://github.com/planetscale/scoop-bucket.git
scoop install pscale

# Verification
pscale version

Logging in and creating a database

Code
Bash
# Log in
pscale auth login

# Create a new database
pscale database create my-app --region us-east

# List available regions
pscale region list

# List databases
pscale database list

Available regions

RegionCodeLocation
US Eastus-eastN. Virginia
US Westus-westOregon
EU Westeu-westDublin
Asia Pacificap-southMumbai
Asia Pacificap-northeastTokyo
Australiaap-southeastSydney
South Americasa-eastSão Paulo

Database branching

The branching concept

PlanetScale treats your database like code - you can create isolated branches to work on new features without affecting production:

Code
Bash
# Create a development branch
pscale branch create my-app feature-auth

# List branches
pscale branch list my-app

# Show branch details
pscale branch show my-app feature-auth

# Delete a branch
pscale branch delete my-app feature-auth

Branch workflow

Code
Bash
# 1. Create a branch for the feature
pscale branch create my-app add-user-profile

# 2. Connect to the branch locally
pscale connect my-app add-user-profile --port 3309

# 3. Apply schema changes
mysql -h 127.0.0.1 -P 3309 -u root < migrations/add_profile.sql

# 4. Create a deploy request (like a PR)
pscale deploy-request create my-app add-user-profile

# 5. Review the changes
pscale deploy-request diff my-app 1

# 6. Deploy to main (merge)
pscale deploy-request deploy my-app 1

Branch types

Code
Bash
# Development branch - for daily work
pscale branch create my-app dev

# Staging branch - for testing
pscale branch create my-app staging

# Production branch - main (default)
# Protected, requires a deploy request

# Feature branch - short-lived
pscale branch create my-app feature-xyz --from staging

Safe migrations

PlanetScale automatically detects dangerous migrations:

Code
SQL
-- ✅ Safe - adding a column
ALTER TABLE users ADD COLUMN bio TEXT;

-- ✅ Safe - adding an index
CREATE INDEX idx_users_email ON users(email);

-- ⚠️ Requires attention - changing column type
ALTER TABLE users MODIFY COLUMN age BIGINT;

-- ❌ Dangerous - dropping a column with data
ALTER TABLE users DROP COLUMN legacy_field;

Zero-downtime schema changes

How does it work?

PlanetScale uses Online Schema Change (OSC) inspired by gh-ost:

  1. Shadow table - Creates a copy of the table with the new schema
  2. Data copy - Copies data in the background
  3. Changelog - Tracks changes during copying
  4. Atomic swap - Switches tables atomically
  5. Cleanup - Removes the old table
Code
SQL
-- Your DDL
ALTER TABLE users ADD COLUMN avatar_url VARCHAR(500);

-- PlanetScale executes:
-- 1. CREATE TABLE _users_new LIKE users;
-- 2. ALTER TABLE _users_new ADD COLUMN avatar_url...
-- 3. INSERT INTO _users_new SELECT * FROM users (chunked)
-- 4. RENAME TABLE users TO _users_old, _users_new TO users;
-- 5. DROP TABLE _users_old;

-- Zero downtime for your application!

Deploy requests

Code
Bash
# Create a deploy request
pscale deploy-request create my-app feature-auth

# Show diff
pscale deploy-request diff my-app 1

# Output:
# -- users table
# +  avatar_url VARCHAR(500)
# +  bio TEXT
# -- INDEX changes
# +  idx_users_email

# Add a comment
pscale deploy-request comment my-app 1 "Reviewed, LGTM"

# Deploy
pscale deploy-request deploy my-app 1

# Track progress
pscale deploy-request show my-app 1

Schema across environments

Code
Bash
# Compare branches
pscale branch diff my-app main feature-auth

# Promote staging to production
pscale deploy-request create my-app staging --into main

# Revert (create a new deploy request with reversed changes)
pscale deploy-request revert my-app 1

Connecting to your application

Connection strings

Code
Bash
# Generate a connection string
pscale password create my-app main production-password

# Output:
# HOST: aws.connect.psdb.cloud
# USERNAME: xxxxx
# PASSWORD: pscale_pw_xxxxx
# DATABASE: my-app

# Connection string format
# mysql://USERNAME:PASSWORD@HOST/DATABASE?ssl={"rejectUnauthorized":true}

Local connection (development)

Code
Bash
# Open a tunnel to the branch
pscale connect my-app main --port 3306

# Now you can connect locally
mysql -h 127.0.0.1 -P 3306 -u root

# Or from your application
DATABASE_URL="mysql://root@127.0.0.1:3306/my-app"

Prisma ORM

Code
Bash
npm install prisma @prisma/client
npx prisma init
prisma/schema.prisma
Prisma
// prisma/schema.prisma
datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"  // Required for PlanetScale!
}

generator client {
  provider = "prisma-client-js"
}

model User {
  id        String   @id @default(cuid())
  email     String   @unique
  name      String?
  posts     Post[]
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([email])
}

model Post {
  id        String   @id @default(cuid())
  title     String
  content   String?  @db.Text
  published Boolean  @default(false)
  author    User     @relation(fields: [authorId], references: [id])
  authorId  String
  createdAt DateTime @default(now())
  updatedAt DateTime @updatedAt

  @@index([authorId])
}
Code
ENV
# .env
DATABASE_URL='mysql://xxxxx:pscale_pw_xxxxx@aws.connect.psdb.cloud/my-app?sslaccept=strict'
Code
Bash
# Push schema to PlanetScale
npx prisma db push

# Generate client
npx prisma generate
TSlib/prisma.ts
TypeScript
// lib/prisma.ts
import { PrismaClient } from '@prisma/client'

const globalForPrisma = globalThis as unknown as {
  prisma: PrismaClient | undefined
}

export const prisma = globalForPrisma.prisma ?? new PrismaClient()

if (process.env.NODE_ENV !== 'production') {
  globalForPrisma.prisma = prisma
}
Code
TypeScript
// Usage
import { prisma } from '@/lib/prisma'

// Create
const user = await prisma.user.create({
  data: {
    email: 'user@example.com',
    name: 'John Doe'
  }
})

// Read with relations
const posts = await prisma.post.findMany({
  where: { published: true },
  include: { author: true }
})

// Update
await prisma.user.update({
  where: { id: user.id },
  data: { name: 'Jane Doe' }
})

// Delete
await prisma.post.delete({
  where: { id: postId }
})

Drizzle ORM

Code
Bash
npm install drizzle-orm @planetscale/database
npm install -D drizzle-kit
TSdb/schema.ts
TypeScript
// db/schema.ts
import { mysqlTable, varchar, text, boolean, timestamp, index } from 'drizzle-orm/mysql-core'

export const users = mysqlTable('users', {
  id: varchar('id', { length: 128 }).primaryKey(),
  email: varchar('email', { length: 255 }).notNull().unique(),
  name: varchar('name', { length: 255 }),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
  emailIdx: index('email_idx').on(table.email)
}))

export const posts = mysqlTable('posts', {
  id: varchar('id', { length: 128 }).primaryKey(),
  title: varchar('title', { length: 255 }).notNull(),
  content: text('content'),
  published: boolean('published').default(false),
  authorId: varchar('author_id', { length: 128 }).notNull(),
  createdAt: timestamp('created_at').defaultNow(),
  updatedAt: timestamp('updated_at').defaultNow().onUpdateNow()
}, (table) => ({
  authorIdx: index('author_idx').on(table.authorId)
}))
TSdb/index.ts
TypeScript
// db/index.ts
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 })
Code
TypeScript
// Usage
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and } from 'drizzle-orm'

// Create
const [user] = await db.insert(users).values({
  id: crypto.randomUUID(),
  email: 'user@example.com',
  name: 'John'
}).$returningId()

// Read
const allUsers = await db.select().from(users)

// Read with conditions
const publishedPosts = await db
  .select()
  .from(posts)
  .where(eq(posts.published, true))

// Join
const postsWithAuthors = await db
  .select({
    post: posts,
    author: users
  })
  .from(posts)
  .innerJoin(users, eq(posts.authorId, users.id))

// Update
await db
  .update(users)
  .set({ name: 'Jane' })
  .where(eq(users.id, userId))

// Delete
await db.delete(posts).where(eq(posts.id, postId))

PlanetScale serverless driver

Code
Bash
npm install @planetscale/database
Code
TypeScript
// Direct driver usage
import { connect } from '@planetscale/database'

const config = {
  host: process.env.DATABASE_HOST,
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD
}

const conn = connect(config)

// Simple query
const results = await conn.execute('SELECT * FROM users WHERE id = ?', [userId])
console.log(results.rows)

// Insert
await conn.execute(
  'INSERT INTO users (id, email, name) VALUES (?, ?, ?)',
  [crypto.randomUUID(), 'user@example.com', 'John']
)

// Transaction (pseudo - PlanetScale does not support native transactions)
// Use Prisma or Drizzle for transaction-like behavior

// Batch queries
const [users, posts] = await Promise.all([
  conn.execute('SELECT * FROM users'),
  conn.execute('SELECT * FROM posts WHERE published = 1')
])

Foreign keys in PlanetScale

Why no native foreign keys?

PlanetScale uses Vitess, which does not support foreign key constraints for better scalability. Instead, you should use:

  1. Application-level validation - Validate in your code
  2. Prisma relationMode - Emulated relations
  3. Database triggers (limited support)

Prisma relations

schema.prisma
Prisma
// schema.prisma
datasource db {
  provider     = "mysql"
  url          = env("DATABASE_URL")
  relationMode = "prisma"  // Emulates FK at the application level
}

model User {
  id    String @id
  posts Post[]
}

model Post {
  id       String @id
  author   User   @relation(fields: [authorId], references: [id], onDelete: Cascade)
  authorId String

  @@index([authorId])  // IMPORTANT: Add the index manually!
}

Manual validation

Code
TypeScript
// Without ORM - manual validation
async function createPost(authorId: string, title: string) {
  // Check if the author exists
  const author = await conn.execute(
    'SELECT id FROM users WHERE id = ?',
    [authorId]
  )

  if (author.rows.length === 0) {
    throw new Error('Author not found')
  }

  // Now create the post
  await conn.execute(
    'INSERT INTO posts (id, author_id, title) VALUES (?, ?, ?)',
    [crypto.randomUUID(), authorId, title]
  )
}

Insights and analytics

Query insights

The PlanetScale dashboard shows:

  • Slow queries - Queries above the threshold
  • Query patterns - Most frequent queries
  • Index suggestions - Index recommendations
  • Connection stats - Active connections
Code
Bash
# CLI insights
pscale database show my-app --web
# Opens the dashboard in your browser

Schema recommendations

Code
SQL
-- PlanetScale suggests:

-- ⚠️ Missing index detected
-- Query: SELECT * FROM posts WHERE author_id = ?
-- Recommendation:
CREATE INDEX idx_posts_author_id ON posts(author_id);

-- ⚠️ Unused index detected
-- Index: idx_legacy_field
-- Recommendation:
DROP INDEX idx_legacy_field ON users;

Performance schema

Code
SQL
-- Enable in the dashboard or:
SELECT * FROM performance_schema.events_statements_summary_by_digest
ORDER BY SUM_TIMER_WAIT DESC
LIMIT 10;

Backup and recovery

Automatic backups

Code
Bash
# List backups
pscale backup list my-app

# Restore to a new branch
pscale backup restore my-app backup-id --branch restored-data

# Export backup
pscale backup download my-app backup-id

Point-in-time recovery

Code
Bash
# Restore to a specific moment (Enterprise)
pscale backup restore my-app \
  --timestamp "2024-01-15T10:30:00Z" \
  --branch pitr-restore

Manual export

Code
Bash
# Export via mysqldump
pscale connect my-app main --port 3306 &
mysqldump -h 127.0.0.1 -P 3306 -u root my-app > backup.sql

# Import
mysql -h 127.0.0.1 -P 3306 -u root my-app < backup.sql

Integrations

Vercel

Code
Bash
# Install the integration from the Vercel Dashboard
# Or manually:
vercel env add DATABASE_URL production
vercel env add DATABASE_URL preview

Next.js API route

TSapp/api/users/route.ts
TypeScript
// app/api/users/route.ts
import { prisma } from '@/lib/prisma'
import { NextResponse } from 'next/server'

export async function GET() {
  const users = await prisma.user.findMany({
    take: 10,
    orderBy: { createdAt: 'desc' }
  })

  return NextResponse.json(users)
}

export async function POST(request: Request) {
  const data = await request.json()

  const user = await prisma.user.create({
    data: {
      email: data.email,
      name: data.name
    }
  })

  return NextResponse.json(user, { status: 201 })
}

Edge functions

TSapp/api/edge/route.ts
TypeScript
// app/api/edge/route.ts
import { connect } from '@planetscale/database'

export const runtime = 'edge'

const conn = connect({
  host: process.env.DATABASE_HOST!,
  username: process.env.DATABASE_USERNAME!,
  password: process.env.DATABASE_PASSWORD!
})

export async function GET(request: Request) {
  const { searchParams } = new URL(request.url)
  const id = searchParams.get('id')

  const result = await conn.execute(
    'SELECT * FROM users WHERE id = ?',
    [id]
  )

  return Response.json(result.rows[0])
}

GitHub Actions CI/CD

.github/workflows/db-migrate.yml
YAML
# .github/workflows/db-migrate.yml
name: Database Migration

on:
  push:
    branches: [main]
    paths: ['prisma/schema.prisma']

jobs:
  migrate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - uses: planetscale/setup-pscale-action@v1
        with:
          version: latest

      - name: Authenticate
        run: pscale auth login --service-token ${{ secrets.PLANETSCALE_SERVICE_TOKEN }}

      - name: Create branch
        run: pscale branch create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }}

      - name: Push schema
        run: |
          npx prisma db push --accept-data-loss
        env:
          DATABASE_URL: ${{ secrets.DATABASE_URL_BRANCH }}

      - name: Create deploy request
        run: |
          pscale deploy-request create ${{ secrets.PLANETSCALE_DB }} pr-${{ github.run_id }} \
            --org ${{ secrets.PLANETSCALE_ORG }}

Scaling and performance

Read replicas

Code
Bash
# PlanetScale automatically routes:
# - Writes -> Primary
# - Reads -> Replicas (when enabled)

# Enable read replicas in the dashboard
# Settings -> Read Replicas -> Enable

Connection pooling

Code
TypeScript
// PlanetScale Proxy automatically pools connections
// For serverless applications this is critical!

// Without pooling: each request = new connection
// With PlanetScale: connections are reused

const config = {
  host: process.env.DATABASE_HOST,  // aws.connect.psdb.cloud
  username: process.env.DATABASE_USERNAME,
  password: process.env.DATABASE_PASSWORD,
  // Proxy automatically manages the pool
}

Query optimization

Code
SQL
-- Use EXPLAIN
EXPLAIN SELECT * FROM posts WHERE author_id = 'xxx';

-- Add indexes
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);

-- Avoid SELECT *
SELECT id, title, created_at FROM posts WHERE author_id = ?;

-- Use LIMIT
SELECT * FROM posts ORDER BY created_at DESC LIMIT 20;

-- Batch operations
INSERT INTO posts (id, title, author_id)
VALUES
  (?, ?, ?),
  (?, ?, ?),
  (?, ?, ?);

Pricing

Hobby (free)

  • Storage: 5GB
  • Row reads: 1 billion/month
  • Row writes: 10 million/month
  • Branches: 1 production + 1 dev
  • Backups: Daily

Scaler ($29/month)

  • Storage: 10GB (+ $2.50/GB)
  • Row reads: 100 billion/month
  • Row writes: 50 million/month
  • Branches: Unlimited
  • Backups: Hourly
  • Read replicas: 1 region

Team ($39/user/month)

  • Everything from Scaler
  • Audit logs
  • SSO/SAML
  • SOC 2 Type 2
  • Priority support

Enterprise (custom)

  • Multi-region
  • PITR
  • SLA 99.99%
  • Dedicated support

Best practices

Naming conventions

Code
Bash
# Databases
my-app-production
my-app-staging

# Branches
main              # Production
staging           # Pre-production
feature-auth      # Feature branch
hotfix-bug-123    # Hotfix branch

Migration workflow

Code
Bash
# 1. Local development
pscale connect my-app dev --port 3306
npx prisma db push

# 2. Create feature branch
pscale branch create my-app feature-xyz

# 3. Test on branch
pscale connect my-app feature-xyz --port 3306
# Run tests...

# 4. Create deploy request
pscale deploy-request create my-app feature-xyz

# 5. Review and deploy
pscale deploy-request deploy my-app 1

# 6. Cleanup
pscale branch delete my-app feature-xyz

Security

Code
Bash
# Rotate passwords regularly
pscale password delete my-app main old-password
pscale password create my-app main new-password

# Use different credentials per environment
# production-creds, staging-creds, dev-creds

# Service tokens for CI/CD
pscale service-token create

FAQ - frequently asked questions

Can I use foreign keys?

Not natively. PlanetScale uses Vitess, which does not support FK constraints. Use relationMode = "prisma" in Prisma or validate in your application.

How do I migrate from MySQL RDS?

  1. Export from RDS: mysqldump
  2. Create a database in PlanetScale
  3. Import: pscale database import
  4. Update the connection string
  5. Remove FK constraints from the schema

Does PlanetScale support transactions?

Yes, but with Vitess limitations. Single-shard transactions work normally. Cross-shard transactions require application-level coordination.

How do I debug slow queries?

  1. Check Insights in the dashboard
  2. Use EXPLAIN on problematic queries
  3. Add suggested indexes
  4. Monitor Query Insights

Can I self-host PlanetScale?

No. PlanetScale is a managed service. For self-hosted, use Vitess directly.

What is the difference between a branch and a database?

A database is the entire database (like a repo). A branch is an isolated copy of the schema (like a git branch). Branches share data with the parent branch up until the point of divergence.

Summary

PlanetScale revolutionizes MySQL database management:

  • Database branching - Git-like workflow
  • Zero-downtime migrations - No downtime
  • Serverless - Automatic scaling
  • Developer experience - Modern CLI and dashboard
  • Enterprise-ready - Vitess under the hood

Ideal for teams that want to treat their database like code, with a full CI/CD workflow and no fear of downtime during migrations.