Turso, rozproszona baza SQLite blisko użytkownika
Turso to baza danych zgodna z SQLite, uruchamiana jako usługa i zaprojektowana wokół jednego pomysłu: kopia danych ma leżeć jak najbliżej kodu, który je czyta. W skrajnym wariancie oznacza to replikę wewnątrz procesu aplikacji, więc odczyt nie wychodzi w ogóle na sieć. Projekt jest otwarty na licencji MIT.
Repliki osadzone, czyli o co tu naprawdę chodzi
Większość baz w chmurze rozwiązuje problem opóźnień, stawiając repliki w kilku regionach. Zapytanie nadal idzie po sieci, tylko krócej. Turso idzie o krok dalej i pozwala trzymać całą bazę jako plik w Twojej aplikacji, synchronizowany z instancją główną.
Konsekwencja jest jakościowa, a nie ilościowa. Odczyt przestaje być wywołaniem sieciowym i staje się odczytem z dysku lokalnego, więc mierzy się go w ułamkach milisekundy zamiast w dziesiątkach. Znika też cała klasa problemów: limit połączeń, chwilowe zerwania łącza, opóźnienie rosnące wraz z odległością od centrum danych.
Cena jest taka, że zapisy nadal idą do instancji głównej, a replika dowiaduje się o nich z opóźnieniem. Dostajesz więc model, w którym odczyt jest błyskawiczny, ale może pokazywać stan sprzed chwili. Do katalogu produktów, treści czy konfiguracji to układ idealny. Do licznika miejsc na sali albo stanu magazynowego przy ostatniej sztuce już nie, bo dwóch użytkowników zobaczy tę samą dostępność.
Druga cena to rozmiar. Skoro replika leży w aplikacji, musi się w niej zmieścić. Przy bazie o rozmiarze kilkuset megabajtów jest to bez znaczenia, przy kilkudziesięciu gigabajtach przestaje mieć sens, zwłaszcza w środowisku uruchamianym na żądanie, gdzie każdy zimny start oznacza pobranie pliku.
Przepisanie silnika w Rust
To najważniejsza zmiana ostatnich dwóch lat i zarazem rzecz, przez którą starsze opisy tego projektu wprowadzają w błąd.
Pierwotnie Turso opierało się na libSQL, czyli odgałęzieniu oryginalnego SQLite napisanego w C, do którego dołożono replikację i dostęp po HTTP. Ten projekt nadal istnieje, ma otwarty kod i jest utrzymywany, ostatnie zmiany w repozytorium pochodzą z lipca 2026 roku.
Równolegle zespół doszedł do wniosku, że część celów, przede wszystkim asynchroniczne wejście i wyjście oraz bezpieczeństwo pamięci, łatwiej osiągnąć, pisząc silnik od nowa, niż przerabiając kod sprzed dwóch dekad. Tak powstał projekt Limbo, czyli kompletne przepisanie SQLite w języku Rust, z zachowaniem zgodności dialektu SQL i formatu pliku. Później przemianowano go po prostu na Turso i to on jest dziś głównym kierunkiem rozwoju.
Konkretne zyski z tego przepisania są dwa i oba mają znaczenie praktyczne. Sterowanie współbieżnością oparte na wielu wersjach pozwala kilku piszącym postępować równolegle, co w oryginalnym SQLite było niemożliwe, bo zapis blokował całą bazę. Asynchroniczne wejście i wyjście przez mechanizm jądra Linuksa sprawia, że wątek nie stoi bezczynnie w oczekiwaniu na dysk, co ma znaczenie zwłaszcza w środowiskach rozliczanych za czas działania.
W lipcu 2026 roku projekt poszedł jeszcze dalej i dołożył eksperymentalną obsługę protokołu Postgresa. Opis repozytorium mówi wprost o architekturze, która ma pozwolić na wiele różnych warstw zewnętrznych nad jednym silnikiem.
Turso a inne bazy w chmurze
| Cecha | Turso | Neon | PlanetScale | Supabase |
|---|---|---|---|---|
| Silnik | zgodny z SQLite | PostgreSQL | MySQL | PostgreSQL |
| Replika w aplikacji | tak | nie | nie | nie |
| Odczyt lokalny | ułamki milisekundy | przez sieć | przez sieć | przez sieć |
| Rozgałęzianie bazy | tak | tak, mocna strona | tak | ograniczone |
| Otwarty kod silnika | tak, MIT | częściowo | nie | tak |
| Złożone zapytania | ograniczone | pełny PostgreSQL | pełny MySQL | pełny PostgreSQL |
Wybór sprowadza się do pytania o kształt obciążenia. Jeśli aplikacja głównie czyta, dane da się zmieścić w rozsądnym pliku, a zależy Ci na opóźnieniu, Turso wygrywa i to wyraźnie. Jeśli piszesz dużo, potrzebujesz złożonych zapytań z wieloma złączeniami albo funkcji, które daje dojrzały PostgreSQL, wybierz PostgreSQL w dowolnym wydaniu i nie komplikuj sobie życia.
Kluczowe zalety Turso
- Lokalizacje brzegowe - repliki rozsiane po świecie, blisko użytkownika
- Repliki osadzone - kopia bazy wewnątrz Twojej aplikacji
- libSQL - otwarte odgałęzienie SQLite z dodatkowymi możliwościami
- Bardzo niskie opóźnienia - mikrosekundy przy odczycie lokalnym
- Zgodność z SQLite - istniejące narzędzia i zapytania działają
- Bez zarządzania serwerem - skalowanie po stronie dostawcy
- Rozliczenie za wiersze - zamiast za czas działania serwera
- Prostota - jeden plik to cała baza
Turso a inne bazy w chmurze
| Cecha | Turso | PlanetScale | Neon | D1 (Cloudflare) |
|---|---|---|---|---|
| Silnik | SQLite/libSQL | Vitess i Postgres | PostgreSQL | SQLite |
| Repliki osadzone | Tak | Nie | Nie | Nie |
| Gałęzie bazy | Tak | Tak | Tak | Nie |
| Otwarty silnik | libSQL, tak | Nie | Nie | Nie |
Rozmiarów darmowych progów i liczb opisujących opóźnienia świadomie w tej tabeli nie ma. Zmieniają się kilka razy w roku, a przynajmniej jedna z powtarzanych powszechnie wartości jest po prostu nieprawdziwa: PlanetScale wycofał darmowy plan, więc jego wariant wejściowy zaczyna się od opłaty miesięcznej, a nie od zera. Aktualne limity sprawdź u każdego dostawcy przed decyzją, która się na nich opiera.
Turso vs SQLite
| Cecha | Turso | SQLite |
|---|---|---|
| Replikacja | Automatyczna | Brak |
| Edge deployment | Tak | Nie |
| HTTP access | Tak | Nie |
| Websockets | Tak | Nie |
| Skalowanie | Automatyczne | Manualne |
| Backupy | Automatyczne | Manualne |
| Multi-region | Tak | Nie |
Kiedy wybrać Turso?
- Edge applications - Dane blisko użytkowników
- Read-heavy workloads - Większość operacji to odczyty
- Low latency critical - Mikrosekundy mają znaczenie
- Simple schema - Relacyjne dane bez złożonych JOINów
- Cost-conscious - Tańsza alternatywa dla PostgreSQL
Instalacja i konfiguracja
Turso CLI
# macOS / Linux
curl -sSfL https://get.tur.so/install.sh | bash
# macOS (Homebrew)
brew install tursodatabase/tap/turso
# Weryfikacja
turso --version
# Windows (WSL wymagany)
curl -sSfL https://get.tur.so/install.sh | bashLogowanie i tworzenie bazy
# Zaloguj się (otworzy przeglądarkę)
turso auth login
# Lub z tokenem
turso auth login --headless
# Stwórz nową bazę danych
turso db create my-database
# Z konkretną lokalizacją
turso db create my-database --location waw
# Lista baz
turso db list
# Szczegóły bazy
turso db show my-databaseDostępne lokalizacje (30+)
# Lista wszystkich lokalizacji
turso db locations
# Popularne lokalizacje:
# waw - Warsaw, Poland
# fra - Frankfurt, Germany
# lhr - London, UK
# ams - Amsterdam, Netherlands
# cdg - Paris, France
# iad - N. Virginia, USA
# sfo - San Francisco, USA
# sin - Singapore
# nrt - Tokyo, Japan
# syd - Sydney, Australia
# gru - São Paulo, BrazilTworzenie tokenu
# Token z pełnymi uprawnieniami
turso db tokens create my-database
# Token read-only
turso db tokens create my-database --read-only
# Token z expiration
turso db tokens create my-database --expiration 7d
# Revoke token
turso db tokens revoke my-database <token-name>Podstawowe użycie
Turso CLI Shell
# Połącz się z bazą (interaktywny shell)
turso db shell my-database
# Wykonaj SQL
turso> CREATE TABLE users (
id INTEGER PRIMARY KEY,
email TEXT UNIQUE NOT NULL,
name TEXT,
created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
turso> INSERT INTO users (email, name) VALUES ('user@example.com', 'John');
turso> SELECT * FROM users;
# Wyjście
turso> .quitKlient TypeScript i JavaScript
npm install @libsql/client// db/client.ts
import { createClient } from '@libsql/client'
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
// Lub dla lokalnego developmentu
// export const db = createClient({
// url: 'file:local.db'
// })// Podstawowe operacje
import { db } from './db/client'
// Query
const result = await db.execute('SELECT * FROM users')
console.log(result.rows)
// Z parametrami (named)
const user = await db.execute({
sql: 'SELECT * FROM users WHERE id = :id',
args: { id: 1 }
})
// Z parametrami (positional)
const users = await db.execute({
sql: 'SELECT * FROM users WHERE email = ?',
args: ['user@example.com']
})
// Insert
await db.execute({
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['new@example.com', 'Jane']
})
// Update
await db.execute({
sql: 'UPDATE users SET name = ? WHERE id = ?',
args: ['Updated Name', 1]
})
// Delete
await db.execute({
sql: 'DELETE FROM users WHERE id = ?',
args: [1]
})Transakcje
// Pojedyncza transakcja
const result = await db.transaction(async (tx) => {
await tx.execute({
sql: 'INSERT INTO accounts (user_id, balance) VALUES (?, ?)',
args: [userId, 1000]
})
await tx.execute({
sql: 'INSERT INTO transactions (account_id, amount) VALUES (?, ?)',
args: [accountId, -100]
})
await tx.execute({
sql: 'UPDATE accounts SET balance = balance - ? WHERE id = ?',
args: [100, accountId]
})
return { success: true }
})Zapytania wsadowe
// Wykonaj wiele queries w jednym roundtrip
const results = await db.batch([
{
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['user1@example.com', 'User 1']
},
{
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: ['user2@example.com', 'User 2']
},
{
sql: 'SELECT * FROM users'
}
])
// results[0] - pierwszy INSERT
// results[1] - drugi INSERT
// results[2] - SELECTRepliki brzegowe
Dodawanie replik
# Dodaj replikę w konkretnej lokalizacji
turso db replicate my-database waw
turso db replicate my-database fra
turso db replicate my-database sin
# Lista replik
turso db show my-database
# Usuń replikę
turso db replicate my-database waw --removeKierowanie automatyczne
// Klient automatycznie łączy się z najbliższą repliką
const db = createClient({
url: 'libsql://my-database-username.turso.io',
authToken: process.env.TURSO_AUTH_TOKEN
})
// Reads -> najbliższa replika
// Writes -> primary (propagowane do replik)Kierowanie ręczne
// Połączenie do konkretnej lokalizacji
const db = createClient({
url: 'libsql://my-database-username.turso.io?location=waw',
authToken: process.env.TURSO_AUTH_TOKEN
})Repliki osadzone w praktyce
Repliki osadzone to lokalna kopia bazy trzymana w samej aplikacji.
Konfiguracja
import { createClient } from '@libsql/client'
const db = createClient({
// Lokalna replika
url: 'file:local-replica.db',
// Synchronizacja z remote
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN,
// Opcjonalnie: automatyczna synchronizacja
syncInterval: 60 // sekundy
})Synchronizacja ręczna
// Synchronizuj ręcznie
await db.sync()
// Po INSERT/UPDATE na remote
// Dane pojawią się lokalnie po sync
// Typowy flow:
// 1. Użytkownik czyta z embedded replica (~0.5ms)
// 2. Użytkownik zapisuje do remote
// 3. Aplikacja sync()
// 4. Nowe dane dostępne lokalnieZastosowania replik osadzonych
// 1. Read-heavy aplikacje
// Większość odczytów z lokalnej repliki
const users = await db.execute('SELECT * FROM users LIMIT 100')
// ~0.5ms zamiast ~50ms!
// 2. Offline-first apps
// Dane dostępne bez internetu
const cachedData = await db.execute('SELECT * FROM cache')
// 3. Edge functions
// Lokalna replika w Vercel Edge Function
export const config = { runtime: 'edge' }
export default async function handler() {
const data = await db.execute('SELECT * FROM products')
return Response.json(data.rows)
}Repliki osadzone w Next.js
// lib/db.ts
import { createClient } from '@libsql/client'
// Singleton pattern dla embedded replica
let db: ReturnType<typeof createClient> | null = null
export function getDb() {
if (!db) {
db = createClient({
url: 'file:./local.db',
syncUrl: process.env.TURSO_DATABASE_URL,
authToken: process.env.TURSO_AUTH_TOKEN
})
}
return db
}
// Sync na starcie aplikacji
export async function initDb() {
const db = getDb()
await db.sync()
}// app/api/users/route.ts
import { getDb } from '@/lib/db'
import { NextResponse } from 'next/server'
export async function GET() {
const db = getDb()
// Super szybki odczyt z lokalnej repliki
const result = await db.execute('SELECT * FROM users')
return NextResponse.json(result.rows)
}
export async function POST(request: Request) {
const db = getDb()
const data = await request.json()
// Zapis idzie do remote
await db.execute({
sql: 'INSERT INTO users (email, name) VALUES (?, ?)',
args: [data.email, data.name]
})
// Sync dla natychmiastowej dostępności lokalnie
await db.sync()
return NextResponse.json({ success: true })
}Drizzle ORM
Konfiguracja
npm install drizzle-orm @libsql/client
npm install -D drizzle-kit// db/schema.ts
import { sqliteTable, text, integer } from 'drizzle-orm/sqlite-core'
export const users = sqliteTable('users', {
id: integer('id').primaryKey({ autoIncrement: true }),
email: text('email').notNull().unique(),
name: text('name'),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})
export const posts = sqliteTable('posts', {
id: integer('id').primaryKey({ autoIncrement: true }),
title: text('title').notNull(),
content: text('content'),
authorId: integer('author_id').notNull().references(() => users.id),
published: integer('published', { mode: 'boolean' }).default(false),
createdAt: text('created_at').default('CURRENT_TIMESTAMP')
})// db/index.ts
import { drizzle } from 'drizzle-orm/libsql'
import { createClient } from '@libsql/client'
import * as schema from './schema'
const client = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
export const db = drizzle(client, { schema })// drizzle.config.ts
import type { Config } from 'drizzle-kit'
export default {
schema: './db/schema.ts',
out: './drizzle',
driver: 'turso',
dbCredentials: {
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
}
} satisfies ConfigMigracje
# Generuj migracje
npx drizzle-kit generate:sqlite
# Aplikuj migracje
npx drizzle-kit push:sqlite
# Studio (GUI)
npx drizzle-kit studioZapytania przez Drizzle
import { db } from '@/db'
import { users, posts } from '@/db/schema'
import { eq, and, desc, like } from 'drizzle-orm'
// Select all
const allUsers = await db.select().from(users)
// Select with where
const user = await db
.select()
.from(users)
.where(eq(users.id, 1))
// Select specific columns
const emails = await db
.select({ email: users.email })
.from(users)
// Join
const postsWithAuthors = await db
.select({
post: posts,
author: users
})
.from(posts)
.innerJoin(users, eq(posts.authorId, users.id))
.where(eq(posts.published, true))
.orderBy(desc(posts.createdAt))
// Insert
const [newUser] = await db
.insert(users)
.values({
email: 'new@example.com',
name: 'New User'
})
.returning()
// Update
await db
.update(users)
.set({ name: 'Updated' })
.where(eq(users.id, 1))
// Delete
await db
.delete(users)
.where(eq(users.id, 1))
// Search
const searchResults = await db
.select()
.from(users)
.where(like(users.name, '%john%'))Prisma
Konfiguracja
npm install prisma @prisma/client
npm install @prisma/adapter-libsql @libsql/client
npx prisma init// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
previewFeatures = ["driverAdapters"]
}
datasource db {
provider = "sqlite"
url = "file:./dev.db"
}
model User {
id Int @id @default(autoincrement())
email String @unique
name String?
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())
}// lib/prisma.ts
import { PrismaClient } from '@prisma/client'
import { PrismaLibSQL } from '@prisma/adapter-libsql'
import { createClient } from '@libsql/client'
const libsql = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
const adapter = new PrismaLibSQL(libsql)
export const prisma = new PrismaClient({ adapter })# Push schema do Turso
npx prisma db push
# Generuj client
npx prisma generate// Użycie
import { prisma } from '@/lib/prisma'
// Create
const user = await prisma.user.create({
data: {
email: 'user@example.com',
name: 'John'
}
})
// Read with relations
const posts = await prisma.post.findMany({
where: { published: true },
include: { author: true }
})
// Update
await prisma.user.update({
where: { id: 1 },
data: { name: 'Updated' }
})
// Delete
await prisma.post.delete({
where: { id: 1 }
})Grupy baz danych
Groups pozwalają zarządzać wieloma bazami razem:
# Stwórz grupę
turso group create my-group --location waw
# Dodaj bazę do grupy
turso db create my-db --group my-group
# Wszystkie bazy w grupie dziedziczą lokalizacje
turso group locations add my-group fra
# Lista grup
turso group list
# Tokeny na poziomie grupy
turso group tokens create my-groupArchitektura wielodostępna
// Każdy tenant ma własną bazę w grupie
async function createTenantDb(tenantId: string) {
// Użyj Turso Platform API
const response = await fetch(
`https://api.turso.tech/v1/organizations/${orgId}/databases`,
{
method: 'POST',
headers: {
Authorization: `Bearer ${TURSO_API_TOKEN}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
name: `tenant-${tenantId}`,
group: 'tenants-group'
})
}
)
return response.json()
}
// Routing do właściwej bazy
function getTenantDb(tenantId: string) {
return createClient({
url: `libsql://tenant-${tenantId}-${org}.turso.io`,
authToken: process.env.TURSO_AUTH_TOKEN
})
}Zarządzanie schematem
Schemat przez wiersz poleceń
# Pokaż schemat
turso db shell my-database ".schema"
# Export schema
turso db shell my-database ".schema" > schema.sql
# Import schema
turso db shell my-database < schema.sqlMigracje z Drizzle
# Struktura
migrations/
├── 0000_init.sql
├── 0001_add_posts.sql
└── 0002_add_comments.sql// migrate.ts
import { migrate } from 'drizzle-orm/libsql/migrator'
import { db } from './db'
async function main() {
console.log('Running migrations...')
await migrate(db, { migrationsFolder: './drizzle' })
console.log('Migrations complete!')
}
main()Zrzut i odtworzenie schematu
# Dump całej bazy
turso db shell my-database ".dump" > backup.sql
# Restore
turso db create my-database-restored
turso db shell my-database-restored < backup.sqlIntegracje
Next.js App Router
// app/api/users/route.ts
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
import { NextResponse } from 'next/server'
export async function GET() {
const allUsers = await db.select().from(users)
return NextResponse.json(allUsers)
}
export async function POST(request: Request) {
const data = await request.json()
const [user] = await db
.insert(users)
.values(data)
.returning()
return NextResponse.json(user, { status: 201 })
}Vercel Edge Functions
// app/api/edge/route.ts
import { createClient } from '@libsql/client/web'
export const runtime = 'edge'
const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
export async function GET(request: Request) {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [id]
})
return Response.json(result.rows[0])
}SvelteKit
// src/lib/db.ts
import { createClient } from '@libsql/client'
import { TURSO_DATABASE_URL, TURSO_AUTH_TOKEN } from '$env/static/private'
export const db = createClient({
url: TURSO_DATABASE_URL,
authToken: TURSO_AUTH_TOKEN
})// src/routes/api/users/+server.ts
import { db } from '$lib/db'
import { json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
export const GET: RequestHandler = async () => {
const result = await db.execute('SELECT * FROM users')
return json(result.rows)
}Remix
// app/utils/db.server.ts
import { createClient } from '@libsql/client'
export const db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})// app/routes/users.tsx
import { json } from '@remix-run/node'
import { useLoaderData } from '@remix-run/react'
import { db } from '~/utils/db.server'
export async function loader() {
const result = await db.execute('SELECT * FROM users')
return json({ users: result.rows })
}
export default function Users() {
const { users } = useLoaderData<typeof loader>()
return (
<ul>
{users.map((user: any) => (
<li key={user.id}>{user.name}</li>
))}
</ul>
)
}GitHub Actions CI/CD
# .github/workflows/migrate.yml
name: Database Migration
on:
push:
branches: [main]
paths: ['drizzle/**']
jobs:
migrate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20
- run: npm ci
- name: Run migrations
run: npx drizzle-kit push:sqlite
env:
TURSO_DATABASE_URL: ${{ secrets.TURSO_DATABASE_URL }}
TURSO_AUTH_TOKEN: ${{ secrets.TURSO_AUTH_TOKEN }}Wskazówki wydajnościowe
Indeksy
-- Twórz indeksy dla często używanych kolumn
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_posts_created ON posts(created_at DESC);
-- Composite index
CREATE INDEX idx_posts_author_published
ON posts(author_id, published);Optymalizacja zapytań
// Zle - pobiera wszystko
const users = await db.execute('SELECT * FROM users')
// Dobrze - tylko potrzebne kolumny
const users = await db.execute('SELECT id, name FROM users')
// Zle - brak limitu
const posts = await db.execute('SELECT * FROM posts ORDER BY created_at DESC')
// Dobrze - z limitem
const posts = await db.execute({
sql: 'SELECT * FROM posts ORDER BY created_at DESC LIMIT ?',
args: [20]
})
// Dobrze - stronicowanie
const posts = await db.execute({
sql: 'SELECT * FROM posts ORDER BY id LIMIT ? OFFSET ?',
args: [20, page * 20]
})Ponowne użycie połączenia
// Zle - nowy klient przy kazdym zadaniu
export async function handler() {
const db = createClient({ ... })
// ...
}
// Dobrze - jedna instancja klienta
let db: ReturnType<typeof createClient> | null = null
export function getDb() {
if (!db) {
db = createClient({
url: process.env.TURSO_DATABASE_URL!,
authToken: process.env.TURSO_AUTH_TOKEN!
})
}
return db
}Zapytanie wsadowe kontra wiele zapytań
// Zle - wiele obiegow do bazy
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
await db.execute({ sql: 'INSERT INTO users ...', args: [...] })
// Dobrze - jeden obieg do bazy
await db.batch([
{ sql: 'INSERT INTO users ...', args: [...] },
{ sql: 'INSERT INTO users ...', args: [...] },
{ sql: 'INSERT INTO users ...', args: [...] }
])Cennik
| Plan | Cena miesięcznie | Bazy | Miejsce | Odczyty wierszy | Zapisy wierszy |
|---|---|---|---|---|---|
| Free | 0 USD | 100 | 5 GB | 500 mln | 10 mln |
| Developer | 4,99 USD | bez limitu | 9 GB | 2,5 mld | 25 mln |
| Scaler | 24,92 USD | bez limitu | 24 GB | 100 mld | 100 mln |
| Pro | 416,58 USD | bez limitu | 50 GB | 250 mld | 250 mln |
Model rozliczeń jest tu inny niż w większości baz i to jego zrozumienie decyduje o tym, czy rachunek Cię zaskoczy. Nie płacisz za czas działania serwera, tylko za odczytane i zapisane wiersze. Przekroczenie limitu nie blokuje bazy, tylko dokłada opłatę: około jednego dolara za miliard dodatkowych odczytów i za milion dodatkowych zapisów, przy czym stawki maleją na wyższych planach.
Trzecia metryka, o której łatwo zapomnieć przy dwóch pozostałych, to miejsce na dane. Powyżej progu zawartego w planie płacisz od siedemdziesięciu pięciu centów za gigabajt na planie deweloperskim, przez pięćdziesiąt centów, do czterdziestu pięciu na najwyższym. Przy bazie rosnącej liniowo ta pozycja jest jedyną, która nie maleje po optymalizacji zapytań, więc warto ją śledzić osobno.
Warto zauważyć asymetrię między odczytem a zapisem, bo mówi wszystko o przeznaczeniu tej bazy. W planie darmowym mieści się pięćset milionów odczytów i tylko dziesięć milionów zapisów, czyli pięćdziesięciokrotnie mniej. To nie jest przeoczenie, tylko odzwierciedlenie architektury: baza jest zaprojektowana pod obciążenie zdominowane przez czytanie.
Osobną pozycją jest synchronizacja replik osadzonych, rozliczana za przesłane gigabajty. Przy replice odświeżanej często i w wielu instancjach aplikacji ta pozycja potrafi urosnąć bardziej niż same odczyty, więc przy planowaniu policz również, jak często i w ilu miejscach będziesz synchronizować.
Praktyczna rada: policz zapytania, zanim wybierzesz plan. Jedno wyświetlenie strony, które wykonuje pięć zapytań zwracających po dwadzieścia wierszy, to sto odczytanych wierszy. Przy milionie odsłon miesięcznie mówimy o stu milionach odczytów, czyli jeszcze w granicach planu darmowego, ale już w tym samym rzędzie wielkości.
Dobre praktyki
Przebieg pracy przy rozwoju
# 1. Lokalna baza do developmentu
turso db create my-app-dev
# 2. Staging
turso db create my-app-staging
# 3. Production
turso db create my-app-prod --location waw
turso db replicate my-app-prod fra
turso db replicate my-app-prod iadZmienne środowiskowe
# .env.local (development)
TURSO_DATABASE_URL=libsql://my-app-dev-username.turso.io
TURSO_AUTH_TOKEN=eyJhbGci...
# .env.production
TURSO_DATABASE_URL=libsql://my-app-prod-username.turso.io
TURSO_AUTH_TOKEN=eyJhbGci...Obsługa błędów
import { db } from '@/lib/db'
async function getUser(id: number) {
try {
const result = await db.execute({
sql: 'SELECT * FROM users WHERE id = ?',
args: [id]
})
if (result.rows.length === 0) {
return null
}
return result.rows[0]
} catch (error) {
if (error instanceof Error) {
console.error('Database error:', error.message)
// Retry logic dla network errors
if (error.message.includes('network')) {
// Retry...
}
}
throw error
}
}Bezpieczeństwo typów
// types/db.ts
interface User {
id: number
email: string
name: string | null
createdAt: string
}
// Typowany query
async function getUsers(): Promise<User[]> {
const result = await db.execute('SELECT * FROM users')
return result.rows as User[]
}
// Lub z Drizzle (pełna type safety)
import { db } from '@/lib/db'
import { users } from '@/lib/db/schema'
const allUsers = await db.select().from(users)
// allUsers jest automatycznie typowany!FAQ - najczęściej zadawane pytania
Kiedy używać embedded replicas?
Gdy masz read-heavy workload i zależy Ci na ultra-niskich latencjach. Idealne dla e-commerce (produkty), CMS (artykuły), analytics dashboards.
Czy Turso wspiera foreign keys?
Tak! W przeciwieństwie do PlanetScale, Turso/libSQL w pełni wspiera SQLite foreign keys.
Jak działa synchronizacja embedded replicas?
Embedded replica synchronizuje się z primary przy wywołaniu db.sync(). Możesz też ustawić syncInterval dla automatycznej synchronizacji.
Czy mogę używać Turso z istniejącą bazą SQLite?
Tak! Możesz zaimportować istniejącą bazę SQLite do Turso przez CLI lub API.
Jaka jest różnica między Turso a D1 (Cloudflare)?
D1 działa tylko na Cloudflare Workers. Turso jest niezależny od platformy i oferuje embedded replicas oraz więcej edge locations.
Czy Turso wspiera full-text search?
Tak, przez SQLite FTS5 extension. libSQL wspiera wszystkie standardowe rozszerzenia SQLite.
-- FTS5 przykład
CREATE VIRTUAL TABLE posts_fts USING fts5(title, content);
INSERT INTO posts_fts SELECT title, content FROM posts;
SELECT * FROM posts_fts WHERE posts_fts MATCH 'search query';Czy odczyt z repliki zwraca aktualne dane?
Nie zawsze, i to jest podstawowe ograniczenie tego modelu. Replika synchronizuje się z instancją główną, więc między zapisem a jego widocznością lokalnie mija chwila. Tam, gdzie potrzebujesz pewności, kieruj odczyt bezpośrednio do instancji głównej.
Czym różni się Turso od libSQL?
libSQL to odgałęzienie oryginalnego SQLite w języku C, na którym usługa opierała się wcześniej i które nadal jest utrzymywane. Turso to nazwa zarówno firmy i usługi, jak i nowego silnika napisanego od zera w Rust, który jest dziś głównym kierunkiem rozwoju.
Kiedy replika osadzona ma sens, a kiedy nie
Ta decyzja wraca w każdym wdrożeniu i warto podjąć ją świadomie, bo od niej zależy cała architektura, a nie tylko konfiguracja połączenia.
Replika osadzona ma sens wtedy, gdy spełnione są trzy warunki naraz. Baza jest na tyle mała, że jej pobranie nie boli, aplikacja żyje na tyle długo, że pobranie zdarza się rzadko, a odczyty stanowią zdecydowaną większość ruchu. Klasyczny przykład to serwis z treścią, gdzie artykuły zmieniają się kilka razy dziennie, a czytane są tysiące razy.
Traci sens, gdy któryś z tych warunków nie jest spełniony. W środowisku, które podnosi nową instancję przy każdym żądaniu, replika nie zdąży się przydać, bo koszt pobrania rozłoży się na jedno wywołanie. Przy bazie rosnącej do gigabajtów pobranie przestaje być operacją w tle i staje się problemem. Przy obciążeniu zdominowanym przez zapisy replika jedynie dokłada opóźnienie synchronizacji, nie dając nic w zamian.
Jest też wariant pośredni, o którym często się zapomina, a który bywa najrozsądniejszy. Możesz korzystać z bazy przez zwykłe połączenie sieciowe, bez repliki lokalnej, i nadal zyskujesz prostotę modelu oraz rozliczenie za wiersze zamiast za czas serwera. Replika jest funkcją opcjonalną, nie warunkiem korzystania z usługi, i dołożenie jej później nie wymaga zmiany schematu ani zapytań.
Praktyczna kolejność wdrożenia wygląda więc tak: zacznij od połączenia sieciowego, zmierz opóźnienia na realnym ruchu i dopiero wtedy zdecyduj, czy replika lokalna coś zmieni. Odwrotna kolejność, czyli zaczynanie od repliki, bo brzmi ciekawie, kończy się zwykle mierzeniem się z zimnym startem przy problemie, którego nie było.
Typowe błędy
Pierwszy to zakładanie natychmiastowej spójności. Kod, który zapisuje rekord, a zaraz potem odczytuje go z repliki, będzie działał poprawnie w dziewięciu przypadkach na dziesięć i zawiedzie w dziesiątym, zwykle na produkcji. Po zapisie albo czytaj z instancji głównej, albo pracuj na wartości, którą właśnie zapisałeś.
Drugi to lekceważenie kosztu zimnego startu przy replikach osadzonych. Nowa instancja aplikacji musi pobrać plik bazy, zanim odpowie na pierwsze żądanie. Przy bazie o rozmiarze stu megabajtów i środowisku, które podnosi instancje na żądanie, ten koszt bywa większy niż oszczędność na opóźnieniu.
Trzeci to liczenie odczytanych wierszy, a nie zapytań. Zapytanie bez indeksu, które przegląda całą tabelę, zużywa tyle odczytów, ile jest w niej wierszy, nawet jeśli zwróci jeden. Brakujący indeks przekłada się tu bezpośrednio na rachunek, a nie tylko na czas odpowiedzi.
Czwarty to traktowanie tej bazy jak pełnego PostgreSQL. Dialekt SQLite jest węższy, typowanie działa inaczej, a części funkcji okienkowych i typów danych po prostu nie ma. Sprawdź, czy Twoje zapytania mieszczą się w tym dialekcie, zanim zaplanujesz migrację.
Piąty to trzymanie w niej danych, które rosną bez ograniczeń. Rejestry zdarzeń, historia zmian czy dane pomiarowe szybko przekroczą rozmiar, przy którym replika osadzona ma sens. Takie zbiory trzymaj gdzie indziej, a w tej bazie zostaw to, co aplikacja realnie czyta przy każdym żądaniu.
Kod silnika i wydania znajdziesz w repozytorium Turso, a starszy silnik w repozytorium libSQL.