docs: add PostgreSQL migration guide for future scalability

- Add section 9 to TECHNICAL_SPECIFICATIONS.md with:
  - Migration criteria (DB size, connections, users, transactions)
  - Proposed repository pattern architecture
  - Migration steps overview
  - Benefits of PostgreSQL

- Create docs/MIGRATION_TO_POSTGRES.md with comprehensive guide:
  - When to migrate (detailed thresholds)
  - Architecture overview (repository pattern)
  - Step-by-step migration process (10 steps)
  - Complete code examples:
    * Base repository interfaces
    * SQLite implementation
    * PostgreSQL implementation
    * Repository factory
    * Migration script
  - Docker Compose configuration
  - Rollback plan
  - Monitoring and maintenance
  - Performance optimization
  - Troubleshooting guide

This enables future migration when database grows beyond SQLite capabilities.
This commit is contained in:
ijuanes 2026-06-17 17:22:50 +01:00
parent 85978a710f
commit 1ad0e8bf13
2 changed files with 1038 additions and 0 deletions

View file

@ -0,0 +1,987 @@
# PostgreSQL Migration Guide
This document describes how to migrate the Tablet Management System from SQLite to PostgreSQL when the database grows beyond SQLite's capabilities.
## When to Migrate
Consider migrating to PostgreSQL when you encounter any of these scenarios:
| Metric | SQLite Limit | PostgreSQL | Migration Trigger |
|--------|--------------|------------|-------------------|
| Database Size | ~10GB max | Unlimited | >1GB |
| Concurrent Writers | 1 | Thousands | >50 simultaneous |
| Active Users | <100 | Millions | >200 |
| Transactions/min | <100 | 100K+ | >500 |
| Servers | Single machine | Cluster | Multiple servers |
| High Availability | No | Yes | Required |
| Backup Strategy | Manual | Automated | Automated needed |
## Architecture Overview
The migration uses a **Repository Pattern** to abstract the database layer, allowing both SQLite and PostgreSQL to work seamlessly.
```
project/
├── backend/
│ ├── config/
│ │ ├── __init__.py
│ │ ├── settings.py # Database configuration
│ │ └── database.py # Repository factory
│ ├── repositories/
│ │ ├── __init__.py
│ │ ├── base_repository.py # Abstract base classes
│ │ ├── sqlite_repo.py # SQLite implementation
│ │ └── postgres_repo.py # PostgreSQL implementation
│ └── app.py # Main application (unchanged)
├── migrations/ # Alembic migrations
│ └── versions/
│ └── initial_schema.py
├── scripts/
│ └── migrate_to_postgres.py # Migration script
└── docker-compose.yml # Optional Docker setup
```
## Step 1: Install Dependencies
```bash
# For development
pip install psycopg2-binary sqlalchemy alembic
# For production (more efficient)
pip install psycopg2 sqlalchemy alembic
```
## Step 2: Set Up PostgreSQL
### Option A: Local Installation
```bash
# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib
# Create database and user
sudo -u postgres psql
```
In PostgreSQL shell:
```sql
CREATE DATABASE tablet_management;
CREATE USER tablet_user WITH PASSWORD 'your_secure_password';
GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user;
ALTER USER tablet_user CREATEDB;
\q
```
### Option B: Docker (Recommended for Development)
```bash
# Start PostgreSQL container
docker run --name tablet-db -e POSTGRES_PASSWORD=your_password -e POSTGRES_USER=tablet_user -e POSTGRES_DB=tablet_management -p 5432:5432 -d postgres:16-alpine
# Or use docker-compose (see docker-compose.yml)
docker-compose up -d postgres
```
## Step 3: Configure Environment
Create a `.env` file:
```bash
# Database configuration
DB_TYPE=postgres # or 'sqlite'
DB_URL=postgresql://tablet_user:your_password@localhost:5432/tablet_management
# For SQLite (fallback)
DB_PATH=tablets.db
```
Or set environment variables:
```bash
export DB_TYPE=postgres
export DB_URL=postgresql://tablet_user:your_password@localhost:5432/tablet_management
```
## Step 4: Create Repository Abstraction
### Base Repository (Abstract Interface)
```python
# backend/repositories/base_repository.py
from abc import ABC, abstractmethod
from typing import Optional, List
from datetime import datetime
class BaseTabletRepository(ABC):
@abstractmethod
def get_by_id(self, tablet_id: int) -> Optional[dict]:
pass
@abstractmethod
def get_by_serial(self, serial: str) -> Optional[dict]:
pass
@abstractmethod
def get_all(self, status: Optional[str] = None) -> List[dict]:
pass
@abstractmethod
def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict:
pass
@abstractmethod
def update_status(self, tablet_id: int, status: str) -> bool:
pass
@abstractmethod
def delete(self, tablet_id: int) -> bool:
pass
class BaseUserRepository(ABC):
@abstractmethod
def get_by_id(self, user_id: int) -> Optional[dict]:
pass
@abstractmethod
def get_by_identification(self, identification: str) -> Optional[dict]:
pass
@abstractmethod
def get_all(self) -> List[dict]:
pass
@abstractmethod
def add(self, name: str, email: Optional[str], phone: Optional[str], identification: str) -> dict:
pass
class BaseLoanRepository(ABC):
@abstractmethod
def get_by_id(self, loan_id: int) -> Optional[dict]:
pass
@abstractmethod
def get_active_by_tablet(self, tablet_id: int) -> Optional[dict]:
pass
@abstractmethod
def get_by_user(self, user_id: int) -> List[dict]:
pass
@abstractmethod
def get_all(self, status: Optional[str] = None) -> List[dict]:
pass
@abstractmethod
def create(self, tablet_id: int, user_id: int) -> dict:
pass
@abstractmethod
def return_loan(self, loan_id: int) -> bool:
pass
class BaseNonLoanableDeviceRepository(ABC):
@abstractmethod
def get_by_id(self, device_id: int) -> Optional[dict]:
pass
@abstractmethod
def get_all(self) -> List[dict]:
pass
@abstractmethod
def add(self, brand: str, model: str, serial_number: str, device_type: str,
location: Optional[str] = None, notes: Optional[str] = None,
purchase_date: Optional[str] = None, purchase_cost: Optional[float] = None) -> dict:
pass
@abstractmethod
def update(self, device_id: int, **kwargs) -> bool:
pass
@abstractmethod
def delete(self, device_id: int) -> bool:
pass
```
### SQLite Implementation
```python
# backend/repositories/sqlite_repo.py
import sqlite3
from typing import Optional, List
from .base_repository import (
BaseTabletRepository, BaseUserRepository,
BaseLoanRepository, BaseNonLoanableDeviceRepository
)
class SQLiteTabletRepository(BaseTabletRepository):
def __init__(self, db_path: str = 'tablets.db'):
self.db_path = db_path
self._init_db()
def _init_db(self):
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS tablets (
id INTEGER PRIMARY KEY AUTOINCREMENT,
brand TEXT NOT NULL,
model TEXT NOT NULL,
serial_number TEXT UNIQUE NOT NULL,
status TEXT DEFAULT 'available',
notes TEXT
)
''')
conn.commit()
conn.close()
def get_by_id(self, tablet_id: int) -> Optional[dict]:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_by_serial(self, serial: str) -> Optional[dict]:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute("SELECT * FROM tablets WHERE serial_number = ?", (serial,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_all(self, status: Optional[str] = None) -> List[dict]:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
query = "SELECT * FROM tablets"
params = ()
if status:
query += " WHERE status = ?"
params = (status,)
cursor.execute(query, params)
results = [dict(row) for row in cursor.fetchall()]
conn.close()
return results
def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict:
conn = sqlite3.connect(self.db_path)
conn.row_factory = sqlite3.Row
cursor = conn.cursor()
cursor.execute('''
INSERT INTO tablets (brand, model, serial_number, status, notes)
VALUES (?, ?, ?, 'available', ?)
''', (brand, model, serial_number, notes))
conn.commit()
tablet_id = cursor.lastrowid
cursor.execute("SELECT * FROM tablets WHERE id = ?", (tablet_id,))
row = cursor.fetchone()
conn.close()
return dict(row)
def update_status(self, tablet_id: int, status: str) -> bool:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("UPDATE tablets SET status = ? WHERE id = ?", (status, tablet_id))
conn.commit()
changed = cursor.rowcount > 0
conn.close()
return changed
def delete(self, tablet_id: int) -> bool:
conn = sqlite3.connect(self.db_path)
cursor = conn.cursor()
cursor.execute("DELETE FROM tablets WHERE id = ?", (tablet_id,))
conn.commit()
deleted = cursor.rowcount > 0
conn.close()
return deleted
# Similar implementations for SQLiteUserRepository, SQLiteLoanRepository, etc.
```
### PostgreSQL Implementation
```python
# backend/repositories/postgres_repo.py
import psycopg2
from psycopg2 import sql
from psycopg2.extras import DictCursor
from typing import Optional, List
from .base_repository import (
BaseTabletRepository, BaseUserRepository,
BaseLoanRepository, BaseNonLoanableDeviceRepository
)
class PostgreSQLTabletRepository(BaseTabletRepository):
def __init__(self, connection_string: str):
self.connection_string = connection_string
self._init_db()
def _get_connection(self):
return psycopg2.connect(self.connection_string)
def _init_db(self):
conn = self._get_connection()
cursor = conn.cursor()
cursor.execute('''
CREATE TABLE IF NOT EXISTS tablets (
id SERIAL PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
serial_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'available',
notes TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_tablets_serial
ON tablets(serial_number)
''')
cursor.execute('''
CREATE INDEX IF NOT EXISTS idx_tablets_status
ON tablets(status)
''')
conn.commit()
cursor.close()
conn.close()
def get_by_id(self, tablet_id: int) -> Optional[dict]:
conn = self._get_connection()
cursor = conn.cursor(cursor_factory=DictCursor)
cursor.execute("SELECT * FROM tablets WHERE id = %s", (tablet_id,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_by_serial(self, serial: str) -> Optional[dict]:
conn = self._get_connection()
cursor = conn.cursor(cursor_factory=DictCursor)
cursor.execute("SELECT * FROM tablets WHERE serial_number = %s", (serial,))
row = cursor.fetchone()
conn.close()
return dict(row) if row else None
def get_all(self, status: Optional[str] = None) -> List[dict]:
conn = self._get_connection()
cursor = conn.cursor(cursor_factory=DictCursor)
query = "SELECT * FROM tablets"
params = ()
if status:
query += " WHERE status = %s"
params = (status,)
cursor.execute(query, params)
results = [dict(row) for row in cursor.fetchall()]
conn.close()
return results
def add(self, brand: str, model: str, serial_number: str, notes: Optional[str] = None) -> dict:
conn = self._get_connection()
cursor = conn.cursor(cursor_factory=DictCursor)
cursor.execute('''
INSERT INTO tablets (brand, model, serial_number, status, notes)
VALUES (%s, %s, %s, 'available', %s)
RETURNING *
''', (brand, model, serial_number, notes))
row = cursor.fetchone()
conn.commit()
conn.close()
return dict(row)
def update_status(self, tablet_id: int, status: str) -> bool:
conn = self._get_connection()
cursor = conn.cursor()
cursor.execute(
"UPDATE tablets SET status = %s, updated_at = NOW() WHERE id = %s",
(status, tablet_id)
)
conn.commit()
changed = cursor.rowcount > 0
conn.close()
return changed
def delete(self, tablet_id: int) -> bool:
conn = self._get_connection()
cursor = conn.cursor()
cursor.execute("DELETE FROM tablets WHERE id = %s", (tablet_id,))
conn.commit()
deleted = cursor.rowcount > 0
conn.close()
return deleted
# Similar implementations for PostgreSQLUserRepository, PostgreSQLLoanRepository, etc.
```
## Step 5: Create Repository Factory
```python
# backend/config/database.py
import os
from backend.repositories.sqlite_repo import (
SQLiteTabletRepository, SQLiteUserRepository,
SQLiteLoanRepository, SQLiteNonLoanableDeviceRepository
)
from backend.repositories.postgres_repo import (
PostgreSQLTabletRepository, PostgreSQLUserRepository,
PostgreSQLLoanRepository, PostgreSQLNonLoanableDeviceRepository
)
from backend.repositories.base_repository import (
BaseTabletRepository, BaseUserRepository,
BaseLoanRepository, BaseNonLoanableDeviceRepository
)
class DatabaseConfig:
def __init__(self):
self.db_type = os.getenv('DB_TYPE', 'sqlite')
self.db_url = os.getenv('DB_URL', '')
self.db_path = os.getenv('DB_PATH', 'tablets.db')
@property
def is_postgres(self) -> bool:
return self.db_type == 'postgres'
def get_tablet_repository() -> BaseTabletRepository:
config = DatabaseConfig()
if config.is_postgres:
return PostgreSQLTabletRepository(config.db_url)
else:
return SQLiteTabletRepository(config.db_path)
def get_user_repository() -> BaseUserRepository:
config = DatabaseConfig()
if config.is_postgres:
return PostgreSQLUserRepository(config.db_url)
else:
return SQLiteUserRepository(config.db_path)
def get_loan_repository() -> BaseLoanRepository:
config = DatabaseConfig()
if config.is_postgres:
return PostgreSQLLoanRepository(config.db_url)
else:
return SQLiteLoanRepository(config.db_path)
def get_non_loanable_device_repository() -> BaseNonLoanableDeviceRepository:
config = DatabaseConfig()
if config.is_postgres:
return PostgreSQLNonLoanableDeviceRepository(config.db_url)
else:
return SQLiteNonLoanableDeviceRepository(config.db_path)
```
## Step 6: Update Application to Use Repositories
Modify your application to use the repository pattern:
```python
# In your app.py or service layer
from backend.config.database import (
get_tablet_repository, get_user_repository,
get_loan_repository, get_non_loanable_device_repository
)
# Instead of direct SQLite calls:
tablet_repo = get_tablet_repository()
user_repo = get_user_repository()
loan_repo = get_loan_repository()
# Example: Loan a tablet
def loan_tablet(tablet_id: int, user_id: int):
# Get repositories
tablet_repo = get_tablet_repository()
user_repo = get_user_repository()
loan_repo = get_loan_repository()
# Validate
tablet = tablet_repo.get_by_id(tablet_id)
if not tablet:
raise ValueError("Tablet not found")
if tablet['status'] != 'available':
raise ValueError("Tablet not available")
user = user_repo.get_by_id(user_id)
if not user:
raise ValueError("User not found")
# Check for active loan
active_loan = loan_repo.get_active_by_tablet(tablet_id)
if active_loan:
raise ValueError("Tablet already loaned")
# Create loan
loan = loan_repo.create(tablet_id, user_id)
# Update tablet status
tablet_repo.update_status(tablet_id, 'loaned')
return loan
```
## Step 7: Create Migration Script
```python
# scripts/migrate_to_postgres.py
#!/usr/bin/env python3
"""
Migration script from SQLite to PostgreSQL
"""
import sqlite3
import psycopg2
from psycopg2.extras import DictCursor
import argparse
from tqdm import tqdm
import os
def create_postgres_tables(conn):
"""Create all tables in PostgreSQL"""
cursor = conn.cursor()
# Tablets
cursor.execute('''
CREATE TABLE IF NOT EXISTS tablets (
id SERIAL PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
serial_number VARCHAR(50) UNIQUE NOT NULL,
status VARCHAR(20) DEFAULT 'available',
notes TEXT,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
''')
# Users
cursor.execute('''
CREATE TABLE IF NOT EXISTS users (
id SERIAL PRIMARY KEY,
name VARCHAR(100) NOT NULL,
email VARCHAR(255),
phone VARCHAR(20),
identification VARCHAR(50) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
''')
# Loans
cursor.execute('''
CREATE TABLE IF NOT EXISTS loans (
id SERIAL PRIMARY KEY,
tablet_id INTEGER NOT NULL REFERENCES tablets(id) ON DELETE RESTRICT,
user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE RESTRICT,
loan_date TIMESTAMP NOT NULL DEFAULT NOW(),
return_date TIMESTAMP,
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW()
)
''')
# Non-loanable devices
cursor.execute('''
CREATE TABLE IF NOT EXISTS non_loanable_devices (
id SERIAL PRIMARY KEY,
brand VARCHAR(100) NOT NULL,
model VARCHAR(100) NOT NULL,
serial_number VARCHAR(50) UNIQUE NOT NULL,
device_type VARCHAR(50) NOT NULL,
location VARCHAR(100),
status VARCHAR(20) DEFAULT 'available',
notes TEXT,
purchase_date DATE,
purchase_cost DECIMAL(10,2),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
)
''')
# Indexes for performance
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_serial ON tablets(serial_number)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_status ON tablets(status)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_tablets_brand ON tablets(brand)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_users_identification ON users(identification)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_tablet ON loans(tablet_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_user ON loans(user_id)')
cursor.execute('CREATE INDEX IF NOT EXISTS idx_loans_status ON loans(status)')
conn.commit()
def migrate_table(conn_sqlite, conn_pg, table_name: str, pg_create_table: str):
"""Generic migration for a table"""
cursor_sqlite = conn_sqlite.cursor()
cursor_pg = conn_pg.cursor()
# Get all data from SQLite
cursor_sqlite.execute(f"SELECT * FROM {table_name}")
rows = cursor_sqlite.fetchall()
if not rows:
print(f"No data to migrate for {table_name}")
return
# Get column names
column_names = [desc[0] for desc in cursor_sqlite.description]
# Prepare INSERT statement
columns = ', '.join(column_names)
placeholders = ', '.join(['%s'] * len(column_names))
insert_sql = f"INSERT INTO {table_name} ({columns}) VALUES ({placeholders}) ON CONFLICT DO NOTHING"
# Migrate data
for row in tqdm(rows, desc=f"Migrating {table_name}"):
cursor_pg.execute(insert_sql, row)
conn_pg.commit()
print(f"✓ Migrated {len(rows)} rows from {table_name}")
def migrate_all(sqlite_path: str, pg_url: str):
"""Migrate all data from SQLite to PostgreSQL"""
print("Starting migration from SQLite to PostgreSQL...")
# Connect to SQLite
conn_sqlite = sqlite3.connect(sqlite_path)
# Connect to PostgreSQL
conn_pg = psycopg2.connect(pg_url)
try:
# Create tables
print("Creating PostgreSQL tables...")
create_postgres_tables(conn_pg)
# Migrate each table
migrate_table(conn_sqlite, conn_pg, 'tablets', '')
migrate_table(conn_sqlite, conn_pg, 'users', '')
migrate_table(conn_sqlite, conn_pg, 'loans', '')
migrate_table(conn_sqlite, conn_pg, 'non_loanable_devices', '')
print("\n✓ Migration completed successfully!")
print(f" SQLite: {sqlite_path}")
print(f" PostgreSQL: {pg_url}")
except Exception as e:
conn_pg.rollback()
print(f"\n✗ Migration failed: {e}")
raise
finally:
conn_sqlite.close()
conn_pg.close()
if __name__ == '__main__':
parser = argparse.ArgumentParser(description='Migrate from SQLite to PostgreSQL')
parser.add_argument('--sqlite', default='tablets.db', help='SQLite database path')
parser.add_argument('--postgres', required=True, help='PostgreSQL connection URL')
args = parser.parse_args()
migrate_all(args.sqlite, args.postgres)
```
## Step 8: Run Migration
```bash
# Test the migration first (dry run)
python scripts/migrate_to_postgres.py --sqlite tablets.db --postgres postgresql://tablet_user:password@localhost:5432/tablet_management_test
# Verify data in test database
psql -U tablet_user -d tablet_management_test -c "SELECT COUNT(*) FROM tablets;"
# When ready, migrate to production
python scripts/migrate_to_postgres.py --sqlite tablets.db --postgres postgresql://tablet_user:password@localhost:5432/tablet_management
```
## Step 9: Switch to PostgreSQL
```bash
# Update environment variables
export DB_TYPE=postgres
export DB_URL=postgresql://tablet_user:password@localhost:5432/tablet_management
# Restart application
python app.py
```
## Step 10: Verify and Monitor
```bash
# Check application logs for errors
# Monitor database connections
psql -U tablet_user -d tablet_management -c "SELECT COUNT(*) FROM tablets;"
# Check active connections
psql -U postgres -c "SELECT * FROM pg_stat_activity WHERE datname = 'tablet_management';"
```
## Rollback Plan
If something goes wrong:
1. **Immediate rollback:**
```bash
# Switch back to SQLite
export DB_TYPE=sqlite
export DB_PATH=tablets.db
python app.py
```
2. **Data verification:**
```bash
# Compare counts
sqlite3 tablets.db "SELECT COUNT(*) FROM tablets;"
psql -U tablet_user -d tablet_management -c "SELECT COUNT(*) FROM tablets;"
```
3. **Backup PostgreSQL data:**
```bash
pg_dump -U tablet_user -d tablet_management > postgres_backup_$(date +%Y%m%d).sql
```
## Docker Compose (Optional)
For easy deployment with Docker:
```yaml
# docker-compose.yml
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: tablet_db
environment:
POSTGRES_DB: tablet_management
POSTGRES_USER: tablet_user
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-changeme}
ports:
- "5432:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U tablet_user -d tablet_management"]
interval: 5s
timeout: 5s
retries: 5
restart: unless-stopped
app:
build: .
container_name: tablet_app
environment:
DB_TYPE: postgres
DB_URL: postgresql://tablet_user:${POSTGRES_PASSWORD:-changeme}@postgres:5432/tablet_management
ports:
- "5000:5000"
depends_on:
postgres:
condition: service_healthy
restart: unless-stopped
volumes:
postgres_data:
```
Start with Docker:
```bash
docker-compose up -d
```
## Benefits of PostgreSQL
### Performance
- **Concurrency:** Multiple writers simultaneously (no lock contention)
- **Indexing:** Advanced index types (B-tree, Hash, GiST, GIN, BRIN)
- **Query Optimization:** Advanced query planner
- **Connection Pooling:** Built-in support
### Scalability
- **Vertical:** Handles large datasets efficiently
- **Horizontal:** Read replicas, partitioning, sharding
- **Connections:** Supports thousands of concurrent connections
### Reliability
- **ACID Compliance:** Full transaction support
- **Point-in-Time Recovery:** Restore to any moment
- **Replication:** Master-slave, synchronous, asynchronous
- **Backups:** `pg_dump`, `pg_basebackup`, continuous archiving
### Security
- **Authentication:** Multiple methods (password, MD5, SCRAM, LDAP, Kerberos)
- **Authorization:** Role-based access control (RBAC)
- **Row-Level Security:** Policies for fine-grained access
- **Encryption:** SSL, at-rest encryption
### Features
- **JSON Support:** Native JSON/JSONB data type
- **Full-Text Search:** Advanced text search capabilities
- **Arrays:** Store arrays of values
- **Custom Types:** Create your own data types
- **Triggers:** Automatic actions on events
- **Stored Procedures:** Server-side functions
## Monitoring PostgreSQL
### Basic Queries
```sql
-- Active connections
SELECT * FROM pg_stat_activity WHERE datname = 'tablet_management';
-- Table sizes
SELECT table_name, pg_size_pretty(pg_total_relation_size(table_name))
FROM information_schema.tables WHERE table_schema = 'public';
-- Index usage
SELECT indexrelname, idx_scan, idx_tup_read, idx_tup_fetch
FROM pg_stat_user_indexes;
-- Slow queries (requires pg_stat_statements extension)
SELECT query, total_time, calls, mean_time
FROM pg_stat_statements ORDER BY mean_time DESC LIMIT 10;
```
### Enable pg_stat_statements
```sql
-- In PostgreSQL
CREATE EXTENSION pg_stat_statements;
-- Then in postgresql.conf
shared_preload_libraries = 'pg_stat_statements'
pg_stat_statements.track = all
```
## Maintenance Tasks
### Regular Maintenance
```bash
# Vacuum (reclaim space, update statistics)
vacuumdb -U tablet_user -d tablet_management --analyze
# Reindex (rebuild indexes)
reindexdb -U tablet_user -d tablet_management
```
### Backup Strategy
```bash
# Daily backup
pg_dump -U tablet_user -d tablet_management > /backups/tablet_management_$(date +%Y%m%d).sql
# Compressed backup
pg_dump -U tablet_user -d tablet_management | gzip > /backups/tablet_management_$(date +%Y%m%d).sql.gz
# Continuous archiving (WAL)
# In postgresql.conf:
wal_level = replica
archive_mode = on
archive_command = 'test ! -f /backups/wal/%f && cp %p /backups/wal/%f'
```
## Performance Optimization
### Configuration Tuning
```conf
# postgresql.conf recommendations
shared_buffers = 4GB # 25% of total RAM
work_mem = 16MB # For complex sorts
maintenance_work_mem = 512MB # For VACUUM, index creation
effective_cache_size = 12GB # 75% of total RAM
random_page_cost = 1.1 # SSD: 1.1, HDD: 4.0
max_worker_processes = 8 # Number of CPU cores
max_parallel_workers_per_gather = 4 # Parallel query workers
max_connections = 200 # Expected max connections
```
### Index Optimization
```sql
-- Add indexes for common queries
CREATE INDEX idx_loans_user_status ON loans(user_id, status);
CREATE INDEX idx_loans_date_range ON loans(loan_date, return_date);
-- Partial index for active loans
CREATE INDEX idx_loans_active ON loans(tablet_id) WHERE status = 'active';
-- Composite index for user loans
CREATE INDEX idx_loans_user_tablet ON loans(user_id, tablet_id);
```
## Troubleshooting
### Common Issues
**Connection refused:**
```bash
# Check if PostgreSQL is running
sudo systemctl status postgresql
# Check port
netstat -tuln | grep 5432
```
**Authentication failed:**
```bash
# Verify user and password
psql -U tablet_user -d tablet_management -h localhost
# Check pg_hba.conf
sudo nano /etc/postgresql/16/main/pg_hba.conf
```
**Database does not exist:**
```bash
# Create database
createdb -U postgres tablet_management
```
**Permission denied:**
```sql
-- Grant permissions
GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user;
GRANT ALL ON SCHEMA public TO tablet_user;
```
### Logs
```bash
# PostgreSQL logs
sudo tail -f /var/log/postgresql/postgresql-16-main.log
# Application logs
journalctl -u tablet_management -f
```
## Conclusion
Migrating from SQLite to PostgreSQL provides:
- Better performance at scale
- True concurrency
- Enhanced reliability
- Advanced features
- Production-ready infrastructure
The repository pattern ensures a smooth transition with minimal code changes, and the migration script automates the data transfer process.