- 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.
28 KiB
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
# 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
# Ubuntu/Debian
sudo apt update
sudo apt install postgresql postgresql-contrib
# Create database and user
sudo -u postgres psql
In PostgreSQL shell:
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)
# 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:
# 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:
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)
# 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
# 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
# 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
# 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:
# 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
# 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
# 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
# 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
# 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:
- Immediate rollback:
# 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;"
- Backup PostgreSQL data:
pg_dump -U tablet_user -d tablet_management > postgres_backup_$(date +%Y%m%d).sql
Docker Compose (Optional)
For easy deployment with Docker:
# 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:
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
-- 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
-- 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
# 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
# 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
# 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
-- 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:
# Check if PostgreSQL is running
sudo systemctl status postgresql
# Check port
netstat -tuln | grep 5432
Authentication failed:
# 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:
# Create database
createdb -U postgres tablet_management
Permission denied:
-- Grant permissions
GRANT ALL PRIVILEGES ON DATABASE tablet_management TO tablet_user;
GRANT ALL ON SCHEMA public TO tablet_user;
Logs
# 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.