Skip to main content

Local vs Remote Databases

Understanding the architectural differences between local and remote databases is crucial for choosing the right solution.

SQLite and File-Based Systems

Architecture

Entire database stored in a single file on the same machine as the application

Access Method

Direct file system access - no separate server process needed

Concurrency

Single concurrent writer, multiple readers

Use Cases

Development, testing, mobile apps, small desktop applications

Characteristics

  • Zero configuration - works out of the box
  • Serverless - no daemon or background service
  • Portable - single file can be copied/moved easily
  • Lightweight - minimal resource consumption
  • Limited concurrency - not suitable for high-traffic applications

Comparison Table

FeatureLocal (SQLite)Remote (MySQL/PostgreSQL)
Setup ComplexityNoneModerate to High
Concurrent Writes1Hundreds/Thousands
Network DependencyNoYes
Data SizeUp to ~280TB (practical: < 100GB)Terabytes+
ACID ComplianceFullFull
User ManagementFile permissionsDatabase users & roles
Ideal ForDev, testing, small appsProduction, large-scale apps
Start with SQLite for development and prototyping, then migrate to a remote database when you need concurrency, scaling, or network access.

Flask-SQLite Integration

Flask connects to SQLite through Python’s built-in library with a structured approach.

Connection Methods

import sqlite3
from flask import g

def get_db():
    if 'db' not in g:
        g.db = sqlite3.connect('database.db')
        g.db.row_factory = sqlite3.Row
    return g.db

@app.teardown_appcontext
def close_db(error):
    db = g.pop('db', None)
    if db is not None:
        db.close()

@app.route('/users')
def users():
    db = get_db()
    users = db.execute('SELECT * FROM users').fetchall()
    return {'users': [dict(user) for user in users]}
Advantages:
  • Direct control over SQL
  • No additional dependencies
  • Simple for small projects
Disadvantages:
  • Manual connection management
  • No abstraction layer
  • Prone to SQL injection if not careful

Connection Lifecycle

1

Connection establishment

conn = sqlite3.connect('database.db')
Creates or opens database file and establishes connection.
2

Row factory configuration

conn.row_factory = sqlite3.Row
Enables dictionary-like access to rows: row['column'] instead of row[0]
3

Cursor creation

cursor = conn.cursor()
Cursor object executes SQL commands and fetches results.
4

Query execution

cursor.execute('SELECT * FROM users')
results = cursor.fetchall()
Execute SQL and retrieve data.
5

Transaction management

conn.commit()  # Save changes
conn.rollback()  # Undo changes
Changes are not permanent until commit() is called.
6

Connection cleanup

conn.close()
Always close connections to release resources.

CRUD Operations Fundamentals

CRUD operations form the foundation of data management in any application.

What is CRUD?

CRUD is an acronym for the four basic functions of persistent storage:

Create

Purpose: Insert new data
HTTP: POST
SQL: INSERT

Read

Purpose: Retrieve existing data
HTTP: GET
SQL: SELECT

Update

Purpose: Modify existing data
HTTP: PUT/PATCH
SQL: UPDATE

Delete

Purpose: Remove data
HTTP: DELETE
SQL: DELETE

Why CRUD is Fundamental

CRUD operations represent the entire lifecycle of any piece of data:
  1. Born - Created when user signs up, creates post, etc.
  2. Lives - Read and displayed throughout the application
  3. Evolves - Updated as information changes
  4. Dies - Deleted when no longer needed
Without any one operation, the application cannot fully manage its data.
Every user interaction translates to a CRUD operation:
User ActionCRUD OperationExample
Sign upCreateNew user account
View profileReadDisplay user information
Edit settingsUpdateChange password
Close accountDeleteRemove user data
Post commentCreateNew comment record
View postsReadDisplay all posts
Edit commentUpdateModify comment text
Remove commentDeleteDelete comment
CRUD operations are universal across all applications:
  • E-commerce: Create orders, Read products, Update cart, Delete items
  • Social Media: Create posts, Read feed, Update profile, Delete messages
  • CMS: Create pages, Read content, Update articles, Delete drafts
  • Task Manager: Create tasks, Read list, Update status, Delete completed
Understanding CRUD helps you design any data-driven application systematically.

CRUD in Practice

import sqlite3
from flask import Flask, request, jsonify

app = Flask(__name__)

def get_db():
    conn = sqlite3.connect('app.db')
    conn.row_factory = sqlite3.Row
    return conn

# CREATE
@app.route('/users', methods=['POST'])
def create_user():
    data = request.get_json()
    conn = get_db()
    cursor = conn.execute(
        'INSERT INTO users (name, email) VALUES (?, ?)',
        (data['name'], data['email'])
    )
    conn.commit()
    return jsonify({'id': cursor.lastrowid}), 201

# READ (all)
@app.route('/users', methods=['GET'])
def read_users():
    conn = get_db()
    users = conn.execute('SELECT * FROM users').fetchall()
    return jsonify([dict(user) for user in users])

# READ (one)
@app.route('/users/<int:user_id>', methods=['GET'])
def read_user(user_id):
    conn = get_db()
    user = conn.execute(
        'SELECT * FROM users WHERE id = ?', 
        (user_id,)
    ).fetchone()
    if user is None:
        return jsonify({'error': 'Not found'}), 404
    return jsonify(dict(user))

# UPDATE
@app.route('/users/<int:user_id>', methods=['PUT'])
def update_user(user_id):
    data = request.get_json()
    conn = get_db()
    conn.execute(
        'UPDATE users SET name = ?, email = ? WHERE id = ?',
        (data['name'], data['email'], user_id)
    )
    conn.commit()
    if conn.total_changes == 0:
        return jsonify({'error': 'Not found'}), 404
    return jsonify({'message': 'Updated'})

# DELETE
@app.route('/users/<int:user_id>', methods=['DELETE'])
def delete_user(user_id):
    conn = get_db()
    conn.execute('DELETE FROM users WHERE id = ?', (user_id,))
    conn.commit()
    if conn.total_changes == 0:
        return jsonify({'error': 'Not found'}), 404
    return jsonify({'message': 'Deleted'})

Migrating to Remote Database

When your application outgrows SQLite, migration to a remote DBMS involves two main steps.

Step 1: Database Migration

1

Set up remote database server

Install and configure MySQL or PostgreSQL:
# PostgreSQL
sudo apt-get install postgresql

# MySQL
sudo apt-get install mysql-server
2

Create new database

-- PostgreSQL
CREATE DATABASE myapp;

-- MySQL
CREATE DATABASE myapp CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
3

Export schema from SQLite

sqlite3 database.db .dump > schema.sql
4

Import into remote database

# PostgreSQL
psql -d myapp -f schema.sql

# MySQL
mysql -u root -p myapp < schema.sql
Schema syntax may need adjustments - SQLite SQL is not 100% compatible with PostgreSQL/MySQL.

Step 2: Application Refactoring

Changes required in your Flask application:
import sqlite3
conn = sqlite3.connect('database.db')

Migration Checklist

1

Backup data

Create complete backup before migration
2

Test schema conversion

Verify all tables, indexes, and constraints
3

Update connection code

Install drivers and change connection strings
4

Add connection pooling

Configure pool size and timeouts
5

Update SQL queries

Fix dialect-specific syntax differences
6

Test thoroughly

Verify all CRUD operations work correctly
7

Monitor performance

Track query performance and connection usage
Using an ORM like SQLAlchemy from the beginning makes migration significantly easier, as most code remains unchanged - only the connection string needs to be updated.

Key Takeaways

Choose Wisely

Use SQLite for development and small apps, remote databases for production and scale

ORM Benefits

Consider using SQLAlchemy for database abstraction and easier migration

CRUD is Universal

Master CRUD operations - they apply to every data-driven application

Plan for Growth

Design with migration in mind - keep database logic separate and testable