SQLite

SQLite — встраиваемая реляционная СУБД, которая хранит всю базу данных в одном файле. Не требует отдельного сервера, работает как библиотека внутри приложения.


Что такое SQLite

SQLite — самая распространённая база данных в мире. Она встроена в каждый смартфон (iOS, Android), браузер, и миллионы приложений.

Ключевые характеристики:

ХарактеристикаОписание
ServerlessНет отдельного процесса, работает in-process
Zero-configНе требует установки и настройки
Single fileВся БД в одном файле (.sqlite, .db)
Cross-platformФайл переносим между платформами
ACIDПолная поддержка транзакций
РазмерБиблиотека ~600KB
┌─────────────────────────────────────────────────────────┐
│                    Ваше приложение                       │
│  ┌─────────────────────────────────────────────────┐    │
│  │              SQLite Library                      │    │
│  │         (встроена в приложение)                  │    │
│  └─────────────────────┬───────────────────────────┘    │
│                        │                                 │
│                        ▼                                 │
│              ┌─────────────────┐                        │
│              │   database.db   │  ← Один файл           │
│              └─────────────────┘                        │
└─────────────────────────────────────────────────────────┘

Преимущества SQLite

Простота

  • Нулевая конфигурация — нет сервера, пользователей, прав доступа
  • Один файл — легко бэкапить, копировать, переносить
  • Самодостаточность — всё в одной библиотеке

Надёжность

  • ACID-транзакции — атомарность, согласованность, изоляция, долговечность
  • Crash-safe — защита от сбоев питания и краша
  • Тестирование — 100% покрытие тестами, миллионы тестов

Производительность

  • Быстрые чтения — часто быстрее клиент-серверных БД для простых запросов
  • Нет сетевого overhead — данные читаются напрямую с диска
  • Кэширование — эффективное использование page cache

Универсальность

  • Любая платформа — Windows, macOS, Linux, iOS, Android, embedded
  • Любой язык — биндинги для всех популярных языков
  • SQL — стандартный SQL с расширениями

Недостатки SQLite

Ограничения масштабирования

ОграничениеОписание
Один writerТолько один процесс может писать одновременно
Нет сетевого доступаНе подходит для распределённых систем
Размер БДДо 281 TB теоретически, практически — до нескольких GB
КонкурентностьБлокировка на уровне файла при записи

Функциональные ограничения

  • Нет пользователей/ролей — безопасность на уровне файловой системы
  • Ограниченный ALTER TABLE — нельзя удалить/переименовать колонки (до версии 3.35)
  • Нет RIGHT JOIN, FULL OUTER JOIN — только LEFT JOIN
  • Динамическая типизация — тип колонки не строго проверяется

Когда НЕ использовать

  • Высокая конкурентность записи (много пишущих клиентов)
  • Клиент-серверная архитектура с множеством подключений
  • Очень большие базы данных (>100GB)
  • Требуется репликация и высокая доступность

Сценарии использования

Идеально подходит

СценарийПримеры
Мобильные приложенияiOS, Android — локальное хранение данных
Desktop приложенияElectron, Tauri — настройки, кэш, данные
Embedded системыIoT, роутеры, умные устройства
ПрототипированиеБыстрый старт без настройки БД
ТестированиеIn-memory БД для тестов
КэшЛокальный кэш данных с сервера
Edge computingЛокальные данные на устройствах
БраузерыIndexedDB, Web SQL (устарел)

Примеры использования в production

  • Браузеры — Chrome, Firefox, Safari хранят историю, куки, кэш
  • Мессенджеры — WhatsApp, Telegram, Signal хранят сообщения локально
  • Операционные системы — iOS, Android системные данные
  • Приложения — Skype, iTunes, Dropbox, Adobe Lightroom

Основы работы с SQLite

Установка CLI

# macOS
brew install sqlite
 
# Ubuntu/Debian
sudo apt install sqlite3
 
# Windows — скачать с sqlite.org

Базовые команды CLI

# Создание/открытие базы
sqlite3 mydb.sqlite
 
# Команды SQLite CLI (начинаются с точки)
sqlite> .help              # Справка
sqlite> .databases         # Список баз
sqlite> .tables            # Список таблиц
sqlite> .schema users      # Схема таблицы
sqlite> .headers on        # Показывать заголовки
sqlite> .mode column       # Табличный вывод
sqlite> .quit              # Выход
 
# Экспорт/импорт
sqlite> .dump              # Дамп всей базы
sqlite> .dump users        # Дамп таблицы
sqlite> .read backup.sql   # Выполнить SQL-файл
sqlite> .output result.txt # Вывод в файл

Создание таблиц

-- Создание таблицы
CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT,
    age INTEGER CHECK(age >= 0),
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
);
 
CREATE TABLE posts (
    id INTEGER PRIMARY KEY,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    content TEXT,
    published INTEGER DEFAULT 0,  -- Boolean как INTEGER
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
 
-- Индексы
CREATE INDEX idx_posts_user_id ON posts(user_id);
CREATE UNIQUE INDEX idx_users_email ON users(email);

Типы данных

SQLite использует динамическую типизацию с 5 классами хранения:

Storage ClassОписание
NULLОтсутствие значения
INTEGERЦелое число (1, 2, 4, 6 или 8 байт)
REALЧисло с плавающей точкой (8 байт)
TEXTСтрока (UTF-8, UTF-16)
BLOBБинарные данные
-- SQLite гибко интерпретирует типы
CREATE TABLE example (
    id INTEGER,           -- Строго INTEGER для PRIMARY KEY
    name VARCHAR(100),    -- Хранится как TEXT
    price DECIMAL(10,2),  -- Хранится как REAL или TEXT
    data BLOB,
    active BOOLEAN        -- Хранится как INTEGER (0/1)
);

CRUD-операции

-- INSERT
INSERT INTO users (email, name, age) VALUES ('john@example.com', 'John', 30);
INSERT INTO users (email, name) VALUES ('jane@example.com', 'Jane');
 
-- SELECT
SELECT * FROM users;
SELECT id, name FROM users WHERE age > 25;
SELECT * FROM users ORDER BY created_at DESC LIMIT 10;
 
-- UPDATE
UPDATE users SET age = 31 WHERE email = 'john@example.com';
 
-- DELETE
DELETE FROM users WHERE id = 1;
 
-- UPSERT (INSERT OR REPLACE)
INSERT OR REPLACE INTO users (id, email, name) VALUES (1, 'new@example.com', 'New');
 
-- INSERT OR IGNORE
INSERT OR IGNORE INTO users (email, name) VALUES ('existing@example.com', 'Existing');

Транзакции

-- Явная транзакция
BEGIN TRANSACTION;
INSERT INTO users (email, name) VALUES ('user1@example.com', 'User 1');
INSERT INTO posts (user_id, title) VALUES (last_insert_rowid(), 'First Post');
COMMIT;
 
-- Откат
BEGIN;
UPDATE users SET age = 100;
-- Что-то пошло не так...
ROLLBACK;
 
-- Режимы транзакций
BEGIN DEFERRED;    -- Блокировка при первой операции (по умолчанию)
BEGIN IMMEDIATE;   -- Блокировка сразу (рекомендуется для записи)
BEGIN EXCLUSIVE;   -- Эксклюзивная блокировка

Pragma (настройки)

-- Включить внешние ключи (по умолчанию отключены!)
PRAGMA foreign_keys = ON;
 
-- WAL mode (рекомендуется для конкурентности)
PRAGMA journal_mode = WAL;
 
-- Синхронизация (NORMAL — баланс скорости и надёжности)
PRAGMA synchronous = NORMAL;
 
-- Размер кэша (в страницах, по умолчанию 2000)
PRAGMA cache_size = 10000;
 
-- Информация о базе
PRAGMA database_list;
PRAGMA table_info(users);
PRAGMA index_list(users);

SQLite в Node.js

better-sqlite3 (синхронный, рекомендуется)

Самая быстрая библиотека для Node.js, синхронный API.

npm install better-sqlite3
import Database from 'better-sqlite3';
 
// Открытие базы
const db = new Database('mydb.sqlite', {
  verbose: console.log,  // Логирование запросов
  fileMustExist: false   // Создать если не существует
});
 
// Настройки производительности
db.pragma('journal_mode = WAL');
db.pragma('foreign_keys = ON');
 
// Создание таблиц
db.exec(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  );
 
  CREATE TABLE IF NOT EXISTS posts (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    content TEXT,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
  );
`);
 
// Prepared statements (рекомендуется)
const insertUser = db.prepare(`
  INSERT INTO users (email, name) VALUES (@email, @name)
`);
 
const getUser = db.prepare(`
  SELECT * FROM users WHERE id = ?
`);
 
const getAllUsers = db.prepare(`
  SELECT * FROM users ORDER BY created_at DESC
`);
 
const getUserPosts = db.prepare(`
  SELECT p.*, u.name as author_name
  FROM posts p
  JOIN users u ON p.user_id = u.id
  WHERE p.user_id = ?
`);
 
// Использование
const result = insertUser.run({ email: 'john@example.com', name: 'John' });
console.log(result.lastInsertRowid); // ID нового пользователя
 
const user = getUser.get(1);  // Одна строка
const users = getAllUsers.all();  // Все строки
const posts = getUserPosts.all(1);
 
// Транзакции
const createUserWithPosts = db.transaction((user, posts) => {
  const { lastInsertRowid: userId } = insertUser.run(user);
 
  const insertPost = db.prepare(`
    INSERT INTO posts (user_id, title, content) VALUES (?, ?, ?)
  `);
 
  for (const post of posts) {
    insertPost.run(userId, post.title, post.content);
  }
 
  return userId;
});
 
// Использование транзакции
const userId = createUserWithPosts(
  { email: 'jane@example.com', name: 'Jane' },
  [
    { title: 'First Post', content: 'Hello!' },
    { title: 'Second Post', content: 'World!' }
  ]
);
 
// Batch insert (очень быстро)
const insertMany = db.transaction((users) => {
  for (const user of users) {
    insertUser.run(user);
  }
});
 
insertMany([
  { email: 'user1@example.com', name: 'User 1' },
  { email: 'user2@example.com', name: 'User 2' },
  { email: 'user3@example.com', name: 'User 3' }
]);
 
// Итератор для больших результатов
for (const user of getAllUsers.iterate()) {
  console.log(user.name);
}
 
// Закрытие
db.close();

sql.js (WebAssembly, браузер/Node.js)

SQLite скомпилированный в WebAssembly, работает в браузере и Node.js.

npm install sql.js
import initSqlJs from 'sql.js';
 
// Инициализация
const SQL = await initSqlJs({
  locateFile: file => `https://sql.js.org/dist/${file}`
});
 
// Создание БД в памяти
const db = new SQL.Database();
 
// Или загрузка из файла
const fileBuffer = fs.readFileSync('mydb.sqlite');
const db = new SQL.Database(fileBuffer);
 
// Выполнение запросов
db.run(`
  CREATE TABLE users (id INTEGER PRIMARY KEY, name TEXT);
  INSERT INTO users VALUES (1, 'John');
  INSERT INTO users VALUES (2, 'Jane');
`);
 
// Получение данных
const result = db.exec('SELECT * FROM users');
console.log(result);
// [{ columns: ['id', 'name'], values: [[1, 'John'], [2, 'Jane']] }]
 
// Prepared statements
const stmt = db.prepare('SELECT * FROM users WHERE id = $id');
stmt.bind({ $id: 1 });
 
while (stmt.step()) {
  const row = stmt.getAsObject();
  console.log(row); // { id: 1, name: 'John' }
}
stmt.free();
 
// Экспорт базы
const data = db.export();
const buffer = Buffer.from(data);
fs.writeFileSync('backup.sqlite', buffer);
 
db.close();

Drizzle ORM (Type-safe, современный)

npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
// 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 }),
  userId: integer('user_id').notNull().references(() => users.id),
  title: text('title').notNull(),
  content: text('content')
});
 
// db.ts
import { drizzle } from 'drizzle-orm/better-sqlite3';
import Database from 'better-sqlite3';
import * as schema from './schema';
 
const sqlite = new Database('mydb.sqlite');
export const db = drizzle(sqlite, { schema });
 
// Использование
import { db } from './db';
import { users, posts } from './schema';
import { eq } from 'drizzle-orm';
 
// Insert
const newUser = await db.insert(users).values({
  email: 'john@example.com',
  name: 'John'
}).returning();
 
// Select
const allUsers = await db.select().from(users);
 
const userWithPosts = await db.query.users.findFirst({
  where: eq(users.id, 1),
  with: {
    posts: true
  }
});
 
// Update
await db.update(users)
  .set({ name: 'John Doe' })
  .where(eq(users.id, 1));
 
// Delete
await db.delete(users).where(eq(users.id, 1));

SQLite в React Native + Expo

expo-sqlite (Expo SDK 50+)

Официальная библиотека Expo для SQLite.

npx expo install expo-sqlite
Базовое использование
// database.ts
import * as SQLite from 'expo-sqlite';
 
// Открытие базы (асинхронно, Expo SDK 50+)
const db = await SQLite.openDatabaseAsync('myapp.db');
 
// Настройки
await db.execAsync('PRAGMA journal_mode = WAL');
await db.execAsync('PRAGMA foreign_keys = ON');
 
// Создание таблиц
await db.execAsync(`
  CREATE TABLE IF NOT EXISTS users (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    email TEXT UNIQUE NOT NULL,
    name TEXT,
    avatar TEXT,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP
  );
 
  CREATE TABLE IF NOT EXISTS notes (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    title TEXT NOT NULL,
    content TEXT,
    is_favorite INTEGER DEFAULT 0,
    created_at TEXT DEFAULT CURRENT_TIMESTAMP,
    updated_at TEXT DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
  );
 
  CREATE INDEX IF NOT EXISTS idx_notes_user ON notes(user_id);
`);
 
export { db };
CRUD-операции
// userService.ts
import { db } from './database';
 
interface User {
  id: number;
  email: string;
  name: string | null;
  avatar: string | null;
  created_at: string;
}
 
// Получение всех пользователей
export async function getAllUsers(): Promise<User[]> {
  return await db.getAllAsync<User>('SELECT * FROM users ORDER BY created_at DESC');
}
 
// Получение одного пользователя
export async function getUserById(id: number): Promise<User | null> {
  return await db.getFirstAsync<User>('SELECT * FROM users WHERE id = ?', id);
}
 
// Создание пользователя
export async function createUser(email: string, name?: string): Promise<number> {
  const result = await db.runAsync(
    'INSERT INTO users (email, name) VALUES (?, ?)',
    email,
    name ?? null
  );
  return result.lastInsertRowId;
}
 
// Обновление пользователя
export async function updateUser(id: number, data: Partial<User>): Promise<void> {
  const fields: string[] = [];
  const values: any[] = [];
 
  if (data.name !== undefined) {
    fields.push('name = ?');
    values.push(data.name);
  }
  if (data.avatar !== undefined) {
    fields.push('avatar = ?');
    values.push(data.avatar);
  }
 
  if (fields.length === 0) return;
 
  values.push(id);
  await db.runAsync(
    `UPDATE users SET ${fields.join(', ')} WHERE id = ?`,
    ...values
  );
}
 
// Удаление пользователя
export async function deleteUser(id: number): Promise<void> {
  await db.runAsync('DELETE FROM users WHERE id = ?', id);
}
Транзакции
// noteService.ts
import { db } from './database';
 
interface Note {
  id: number;
  user_id: number;
  title: string;
  content: string | null;
  is_favorite: number;
  created_at: string;
  updated_at: string;
}
 
// Создание заметки с тегами (транзакция)
export async function createNoteWithTags(
  userId: number,
  title: string,
  content: string,
  tagIds: number[]
): Promise<number> {
  return await db.withTransactionAsync(async () => {
    // Создаём заметку
    const result = await db.runAsync(
      'INSERT INTO notes (user_id, title, content) VALUES (?, ?, ?)',
      userId,
      title,
      content
    );
 
    const noteId = result.lastInsertRowId;
 
    // Добавляем теги
    for (const tagId of tagIds) {
      await db.runAsync(
        'INSERT INTO note_tags (note_id, tag_id) VALUES (?, ?)',
        noteId,
        tagId
      );
    }
 
    return noteId;
  });
}
 
// Получение заметок с пагинацией
export async function getNotes(
  userId: number,
  page: number = 1,
  limit: number = 20
): Promise<Note[]> {
  const offset = (page - 1) * limit;
 
  return await db.getAllAsync<Note>(
    `SELECT * FROM notes
     WHERE user_id = ?
     ORDER BY updated_at DESC
     LIMIT ? OFFSET ?`,
    userId,
    limit,
    offset
  );
}
 
// Поиск заметок
export async function searchNotes(userId: number, query: string): Promise<Note[]> {
  return await db.getAllAsync<Note>(
    `SELECT * FROM notes
     WHERE user_id = ?
       AND (title LIKE ? OR content LIKE ?)
     ORDER BY updated_at DESC`,
    userId,
    `%${query}%`,
    `%${query}%`
  );
}
React Hook для SQLite
// hooks/useDatabase.ts
import { useEffect, useState, useCallback } from 'react';
import * as SQLite from 'expo-sqlite';
 
let dbInstance: SQLite.SQLiteDatabase | null = null;
 
export function useDatabase() {
  const [db, setDb] = useState<SQLite.SQLiteDatabase | null>(null);
  const [isReady, setIsReady] = useState(false);
 
  useEffect(() => {
    async function initDatabase() {
      if (dbInstance) {
        setDb(dbInstance);
        setIsReady(true);
        return;
      }
 
      try {
        const database = await SQLite.openDatabaseAsync('myapp.db');
 
        // Миграции
        await database.execAsync(`
          PRAGMA journal_mode = WAL;
          PRAGMA foreign_keys = ON;
 
          CREATE TABLE IF NOT EXISTS users (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            email TEXT UNIQUE NOT NULL,
            name TEXT
          );
 
          CREATE TABLE IF NOT EXISTS notes (
            id INTEGER PRIMARY KEY AUTOINCREMENT,
            user_id INTEGER NOT NULL,
            title TEXT NOT NULL,
            content TEXT,
            created_at TEXT DEFAULT CURRENT_TIMESTAMP,
            FOREIGN KEY (user_id) REFERENCES users(id)
          );
        `);
 
        dbInstance = database;
        setDb(database);
        setIsReady(true);
      } catch (error) {
        console.error('Database initialization error:', error);
      }
    }
 
    initDatabase();
  }, []);
 
  return { db, isReady };
}
 
// hooks/useNotes.ts
import { useState, useEffect, useCallback } from 'react';
import { useDatabase } from './useDatabase';
 
interface Note {
  id: number;
  title: string;
  content: string | null;
  created_at: string;
}
 
export function useNotes(userId: number) {
  const { db, isReady } = useDatabase();
  const [notes, setNotes] = useState<Note[]>([]);
  const [loading, setLoading] = useState(true);
 
  const fetchNotes = useCallback(async () => {
    if (!db) return;
 
    setLoading(true);
    try {
      const result = await db.getAllAsync<Note>(
        'SELECT * FROM notes WHERE user_id = ? ORDER BY created_at DESC',
        userId
      );
      setNotes(result);
    } catch (error) {
      console.error('Error fetching notes:', error);
    } finally {
      setLoading(false);
    }
  }, [db, userId]);
 
  useEffect(() => {
    if (isReady) {
      fetchNotes();
    }
  }, [isReady, fetchNotes]);
 
  const addNote = useCallback(async (title: string, content: string) => {
    if (!db) return;
 
    await db.runAsync(
      'INSERT INTO notes (user_id, title, content) VALUES (?, ?, ?)',
      userId,
      title,
      content
    );
    await fetchNotes();
  }, [db, userId, fetchNotes]);
 
  const deleteNote = useCallback(async (noteId: number) => {
    if (!db) return;
 
    await db.runAsync('DELETE FROM notes WHERE id = ?', noteId);
    await fetchNotes();
  }, [db, fetchNotes]);
 
  return { notes, loading, addNote, deleteNote, refetch: fetchNotes };
}
Использование в компонентах
// screens/NotesScreen.tsx
import { View, FlatList, Text, Button, ActivityIndicator } from 'react-native';
import { useNotes } from '../hooks/useNotes';
 
export function NotesScreen({ userId }: { userId: number }) {
  const { notes, loading, addNote, deleteNote } = useNotes(userId);
 
  if (loading) {
    return <ActivityIndicator size="large" />;
  }
 
  return (
    <View style={{ flex: 1 }}>
      <Button
        title="Add Note"
        onPress={() => addNote('New Note', 'Content...')}
      />
 
      <FlatList
        data={notes}
        keyExtractor={(item) => item.id.toString()}
        renderItem={({ item }) => (
          <View style={{ padding: 16, borderBottomWidth: 1 }}>
            <Text style={{ fontWeight: 'bold' }}>{item.title}</Text>
            <Text>{item.content}</Text>
            <Button
              title="Delete"
              onPress={() => deleteNote(item.id)}
              color="red"
            />
          </View>
        )}
      />
    </View>
  );
}
Миграции базы данных
// migrations.ts
import * as SQLite from 'expo-sqlite';
 
interface Migration {
  version: number;
  up: string;
}
 
const migrations: Migration[] = [
  {
    version: 1,
    up: `
      CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        email TEXT UNIQUE NOT NULL,
        name TEXT
      );
    `
  },
  {
    version: 2,
    up: `
      CREATE TABLE IF NOT EXISTS notes (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        title TEXT NOT NULL,
        content TEXT,
        FOREIGN KEY (user_id) REFERENCES users(id)
      );
    `
  },
  {
    version: 3,
    up: `
      ALTER TABLE notes ADD COLUMN is_favorite INTEGER DEFAULT 0;
      CREATE INDEX idx_notes_favorite ON notes(is_favorite);
    `
  }
];
 
export async function runMigrations(db: SQLite.SQLiteDatabase): Promise<void> {
  // Создаём таблицу миграций
  await db.execAsync(`
    CREATE TABLE IF NOT EXISTS migrations (
      version INTEGER PRIMARY KEY,
      applied_at TEXT DEFAULT CURRENT_TIMESTAMP
    );
  `);
 
  // Получаем текущую версию
  const result = await db.getFirstAsync<{ version: number }>(
    'SELECT MAX(version) as version FROM migrations'
  );
  const currentVersion = result?.version ?? 0;
 
  // Применяем новые миграции
  for (const migration of migrations) {
    if (migration.version > currentVersion) {
      console.log(`Applying migration ${migration.version}...`);
 
      await db.withTransactionAsync(async () => {
        await db.execAsync(migration.up);
        await db.runAsync(
          'INSERT INTO migrations (version) VALUES (?)',
          migration.version
        );
      });
    }
  }
}
 
// Использование при инициализации
const db = await SQLite.openDatabaseAsync('myapp.db');
await runMigrations(db);
Синхронизация с сервером
// sync.ts
import * as SQLite from 'expo-sqlite';
 
interface SyncableEntity {
  id: number;
  updated_at: string;
  synced_at: string | null;
  deleted: number;
}
 
export async function syncNotes(
  db: SQLite.SQLiteDatabase,
  userId: number
): Promise<void> {
  // 1. Получаем локальные изменения
  const localChanges = await db.getAllAsync<SyncableEntity>(
    `SELECT * FROM notes
     WHERE user_id = ?
       AND (synced_at IS NULL OR updated_at > synced_at)`,
    userId
  );
 
  // 2. Отправляем на сервер
  const response = await fetch('https://api.example.com/sync', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      changes: localChanges,
      lastSync: await getLastSyncTime(db, userId)
    })
  });
 
  const { serverChanges, syncTime } = await response.json();
 
  // 3. Применяем изменения с сервера
  await db.withTransactionAsync(async () => {
    for (const change of serverChanges) {
      if (change.deleted) {
        await db.runAsync('DELETE FROM notes WHERE id = ?', change.id);
      } else {
        await db.runAsync(
          `INSERT OR REPLACE INTO notes
           (id, user_id, title, content, updated_at, synced_at)
           VALUES (?, ?, ?, ?, ?, ?)`,
          change.id,
          change.user_id,
          change.title,
          change.content,
          change.updated_at,
          syncTime
        );
      }
    }
 
    // Обновляем synced_at для локальных изменений
    for (const local of localChanges) {
      await db.runAsync(
        'UPDATE notes SET synced_at = ? WHERE id = ?',
        syncTime,
        local.id
      );
    }
  });
}

Оптимизация производительности

Индексы

-- Создание индексов для частых запросов
CREATE INDEX idx_notes_user_updated ON notes(user_id, updated_at DESC);
CREATE INDEX idx_notes_favorite ON notes(is_favorite) WHERE is_favorite = 1;
 
-- Анализ использования индексов
EXPLAIN QUERY PLAN SELECT * FROM notes WHERE user_id = 1 ORDER BY updated_at DESC;

WAL mode

-- Включить WAL (Write-Ahead Logging)
PRAGMA journal_mode = WAL;
 
-- Преимущества:
-- + Параллельное чтение и запись
-- + Лучшая производительность
-- + Атомарный checkpoint
 
-- Недостатки:
-- - Дополнительные файлы (-wal, -shm)
-- - Не работает на сетевых дисках

Batch operations

// Плохо: много отдельных INSERT
for (const note of notes) {
  await db.runAsync('INSERT INTO notes (title) VALUES (?)', note.title);
}
 
// Хорошо: одна транзакция
await db.withTransactionAsync(async () => {
  for (const note of notes) {
    await db.runAsync('INSERT INTO notes (title) VALUES (?)', note.title);
  }
});
 
// Ещё лучше: batch INSERT (если поддерживается)
const placeholders = notes.map(() => '(?)').join(', ');
await db.runAsync(
  `INSERT INTO notes (title) VALUES ${placeholders}`,
  ...notes.map(n => n.title)
);

Best Practices

Безопасность

// НИКОГДА не делайте так (SQL injection!)
const query = `SELECT * FROM users WHERE name = '${userInput}'`;
 
// ВСЕГДА используйте параметризованные запросы
const result = await db.getAllAsync(
  'SELECT * FROM users WHERE name = ?',
  userInput
);

Рекомендации

ПрактикаОписание
PRAGMA foreign_keys = ONВключать в каждом подключении
WAL modeИспользовать для мобильных приложений
Prepared statementsИспользовать для повторяющихся запросов
ТранзакцииГруппировать связанные операции
ИндексыСоздавать для WHERE и JOIN колонок
VACUUMПериодически выполнять для оптимизации
БэкапыКопировать файл БД при закрытом подключении

Альтернативы

БиблиотекаОписание
expo-sqliteОфициальная для Expo
react-native-sqlite-storageДля bare React Native
WatermelonDBORM с lazy loading, синхронизация
RealmАльтернатива SQLite, своя БД
MMKVДля простых key-value (быстрее AsyncStorage)

Ссылки