SQLite vs Core Data: iOS Data Storage

When building an iOS app, the native options for iOS data persistence on Apple’s SDKs are SQLite vs Core Data.

  • SQLite is an embedded relational database engine that runs inside the app process. It provides a standard SQL interface for interacting with stored data.
  • Core Data is Apple’s object graph and persistence framework. It provides an object-oriented abstraction layer on top of a persistent store like SQLite.

SQLite is a serverless relational database engine that runs SQL queries against single-file disk databases. Core Data is an object graph and persistence framework that provides an abstraction layer over a persistent store like SQLite. Key differences include SQLite using pure SQL versus Core Data’s declarative fetching API, SQLite’s flexible concurrency versus Core Data’s confined contexts, and SQLite’s performance being predictably tied to disk I/O versus Core Data’s greater memory overhead. There is no universally better choice – the optimal local database depends on the specific requirements of the iOS app.

Both solutions have been used successfully in countless iOS apps. But they take very different approaches and each carries distinctive pros and cons.

This guide will explore the key differences between SQLite vs Core Data. Along the way, you’ll see real code examples in Swift to illustrate usage of both technologies.

By the end, you’ll have a solid grasp of when to choose SQLite or Core Data for your next iOS project. Let’s dive in!

SQLite vs Core Data – Summary and Comparison

Here is a quick comparison summary between SQLite and Core Data:

SQLiteCore Data
ArchitectureServerless relational databaseObject graph & persistence framework
Data modelTables, rows, columnsModel objects, attributes, relationships
QueryingSQL statementsFetch requests on object graph
ConcurrencySerialized or multi-threadedConfined contexts
PerformanceDisk I/O boundMemory overhead
ExtensibilityCustom functions, virtual tables, pluginsSubclassing, iCloud sync
SQLite vs Core Data – At a glance

SQLite offers:

  • Maximum control via direct SQL access
  • Predictable performance based on disk I/O
  • Flexible concurrency configurations
  • Extensibility through custom functions and virtual tables

Core Data provides:

  • Automatic object-relational mapping
  • Declarative fetching and rich object graph management
  • Enforced thread confinement for transactional semantics
  • Tight iCloud integration

There is no universally superior choice. The best local database for your iOS app depends on the specific requirements and constraints:

  • For simple data structures queried primarily through primary keys, SQLite may suffice.
  • For complex interconnected objects with diverse relationship graph queries, Core Data is likely a better fit.
  • If advanced custom database logic is needed, SQLite offers more extensibility.
  • For syncing data across devices, Core Data + iCloud provides robust solutions.

Let’s now compare the individual differences between SQLite and Core Data to help you make a better choice.

SQLite vs Core Data – Persistence Architectures Compared

The first difference between SQLite vs Core Data lies in their internal persistence architectures.

SQLite – A Serverless Database

SQLite is a compact, open-source relational database management system contained in a C library. It implements most of the SQL standard but does not require a separate database server process. SQLite reads and writes directly to ordinary disk files.

Some key architectural features:

  • Self-contained – SQLite requires no configuration, just link the library into your app.
  • Serverless – There is no detached database process or service. SQLite runs in the same process as your app.
  • Zero-configuration – No complex setup needed. Just specify the path of the database file.
  • Transactional – Atomic commit and rollback with full ACID semantics.
  • Single-file – The entire database is contained in a single cross-platform disk file.

For example, here is how to open a new SQLite database file in Swift:

import SQLite

let db = try Connection("/path/to/db.sqlite3")

The Connection instance provides a gateway to executing SQL queries against the opened database:

try db.execute(
    """
    CREATE TABLE books (
       id INTEGER PRIMARY KEY,
       title TEXT NOT NULL,
       author TEXT NOT NULL
    )
    """
)

try db.execute(
    "INSERT INTO books (title, author) VALUES (?1, ?2)",
    "The Grapes of Wrath", 
    "John Steinbeck"
)

SQLite offers many advantages like simplicity, portability, and efficiently reading/writing to disk. But interacting using SQL can be cumbersome for application developers. This is where Core Data steps in.

Core Data – Object Graph Manager

Rather than SQL tables and rows, Core Data operates on object graphs – networks of Swift or Objective-C model objects. These represent application data as native domain entities.

Core Data takes care of:

  • Object lifecycle management
  • Change tracking
  • Data validation
  • Undo/redo
  • Background data loading

…and much more!

At a high-level, Core Data sits between your app and one or more persistent stores, typically SQLite. It acts as an object mapper, converting between object graphs in memory and data stored on disk:

For example, here is how to fetch books from Core Data:

let fetchRequest: NSFetchRequest<Book> = Book.fetchRequest()

let books = try context.fetch(fetchRequest)

Core Data handles translating this request to query the underlying SQLite store. It returns Book model objects which can be used directly in application code.

The major downside is that Core Data has a significant learning curve. But once mastered, it can accelerate development by abstracting away the raw database.

SQLite vs Core Data – Querying and Manipulating Data

When it comes to reading and writing data, SQLite requires developers to use SQL, whereas Core Data relies on object graph traversal and search APIs.

SQLite – Pure SQL

As a relational database, all interaction with SQLite data happens through regular SQL statements. This includes:

For instance, we can perform a basic query like this:

let results = try db.prepare("SELECT * FROM books WHERE author = ?")
    .bind(authorName)
    .map { row in
        return Book(
            id: row[0],
            title: row[1],
            author: row[2]
       )
}

The SQL interface provides maximum control, but also burdens developers with:

-Verbose SQL strings strewn throughout code
-Manual management of opening/closing connections
-Mapping query result sets to model objects

These downsides are precisely what Core Data aims to solve.

Core Data – Object Graph Queries

Core Data lets you interact with data through Swift or Objective-C model objects rather than SQL. This applies an object-oriented layer and hides the underlying store.

For example, fetching books by author looks like:

let fetchRequest: NSFetchRequest<Book> = Book.fetchRequest() 
fetchRequest.predicate = NSPredicate(format: "author == %@", authorName)

let books = try context.fetch(fetchRequest) 

Core Data handles translating the fetch request into the appropriate SQL query against the persistent store.

Other common querying patterns like filtering, sorting, and aggregations have similar declarative APIs:

// Filter books released after 2000 
fetchRequest.predicate = NSPredicate(format: "releaseDate > %@", year2000)

// Sort books by title
fetchRequest.sortDescriptors = [NSSortDescriptor(key: "title", ascending: true)]

// Count books by each author
let countRequest = NSCountRequest(entity: Book.entity(), predicate: predicate)
let countsByAuthor = try context.execute(countRequest) as! [NSCountResult]

Core Data provides much richer features for manipulating object graphs:

  • Validation – Ensure data integrity with value restrictions
  • Relationships – Define connections between entities
  • Undo manager – Rollback and restore past states
  • Change tracking – Observe modifications to objects
  • Background tasks – Safely save data asynchronously
  • Migrations – Incrementally evolve data model
  • Fetched properties – Computed model attributes

The downside is that Core Data has a learning curve. Simple operations often feel verbose compared to raw SQL.

SQLite vs Core Data – Concurrency and Thread Safety

Mobile apps require performing database operations across multiple threads to prevent blocking the UI. SQLite offers flexible concurrency while Core Data limits thread confinement by design.

SQLite – Serialized and Concurrent Modes

SQLite supports two concurrency modes:

  • Serialized – Serialize all database access onto a single thread. This avoids concurrency bugs but can limit performance.
  • Multi-thread – Allow concurrent database access from any thread. Requires more care but enables parallelism.

For example, serialized mode would look like:

//Serialize db access on the background thread
let queue = DispatchQueue(label: "dbAccessQueue") 

//Open connection
queue.sync {
   db = try Connection(...) 
}

//Query db 
queue.sync {
  let results = try db.prepare(...).map { ... }
}

Multi-thread mode is more flexible:

//Open connection 
db = try Connection(...)

//Start background task
DispatchQueue.global().async {

  //Concurrently query
  let results = try db.prepare(...).map { ... }
  
  //Update db
  try db.execute(...)
  
} 

With either approach, developers must take care to avoid problems like race conditions and deadlocks.

Core Data – Confined Contexts

Core Data takes a different approach by scoping database operations to managed object contexts. A context represents a scratch pad for changes to the object graph.

The golden rule is that context instances must never be shared across threads. Each thread must have its own confined context instance. For example:

//Main thread context
let mainContext = NSManagedObjectContext(concurrencyType: .mainQueueConcurrencyType)

DispatchQueue.global().async {

  //Background thread context
  let bgContext = NSManagedObjectContext(concurrencyType: .privateQueueConcurrencyType)
  
  //Use bgContext...
  
  mainContext.perform {
    //Merge changes from bgContext
    mainContext.mergeChanges(fromContextDidSave: bgContextNotification) 
  }

}

Violating thread confinement leads to nasty crashes. But used properly, contexts provide transactional semantics where changes don’t impact other threads until explicitly saved.

The downside is that passing contexts between threads can be messy. SQLite offers more control over concurrency trade-offs.

SQLite vs Core Data – Performance and Scalability

For small datasets, SQLite and Core Data both offer great performance. But as data grows, architectural differences have significant impacts.

SQLite – Predictable Disk I/O

SQLite reads and writes directly to ordinary disk files. All database operations ultimately translate to file seeks and reads/writes.

This means performance can be predicted by understanding disk I/O characteristics – mostly impacted by the speed of the storage device.

For example, you can expect:

  • ~50,000 inserts/sec and ~500 selects/sec on spinning HDD
  • ~250,000 inserts/sec and ~5,000 selects/sec on SSD
  • ~1,000,000 inserts/sec and ~50,000 selects/sec on NVMe

Source: Appropriate Uses for SQLite

Optimizations like covering indices, tiered storage, and SELECT statement tuning can help significantly. But ultimately SQLite performance is bounded by physical media.

The upside is that SQLite scales predictably. Bigger/faster storage provides better performance at similar CPU cost.

Core Data – Memory Overhead

In Core Data, the object graph is primarily held in memory. Fetching data loads records from disk into active model objects.

As a result, the performance and scalability of Core Data depends heavily on the memory available. Some factors:

  • More objects/relationships consume more RAM
  • Graph traversal and filtering happen in memory
  • Table joins occur in object graph rather than SQL

This provides excellent performance for small datasets. But larger object graphs introduce major memory overhead. Complex fetches and relationships can choke available RAM and trigger low memory warnings.

Developers use faulting to load objects lazily and reduce memory pressure. But unlike SQLite, scaling Core Data requires more than just faster storage hardware.

SQLite vs Core Data – Integration and Extensibility

SQLite and Core Data differ substantially in how they enable integration with external systems and libraries.

SQLite – Extensibility Built-in

SQLite provides hooks and extension points that enable deep integration:

  • Custom SQL functions – Write C/Swift code hooked into SQL queries to implement application-specific logic.
  • Custom collations – Plug in string sorting routines for TEXT columns.
  • Custom tokenizer – Tailor string tokenization algorithms like for search.
  • Virtual tables – Add virtual tables backed by application code, not stored data.
  • Plugins – Load shared library plugins for custom SQL functions.

For example, full-text search can be added by developing a custom FTS5 tokenizer.

These capabilities allow the SQLite database to remain lightweight while still offering extensibility for app-specific needs.

Core Data – Limited Openness

With Core Data, extensibility options are limited since most database integration happens under the hood:

  • Custom managed object subclasses – Application models can extend NSManagedObject to add business logic.
  • Integration with SQLite – Direct access to the raw SQLite database is discouraged. This breaks the abstraction barrier and goes against recommended practices.
  • iCloud integration – NSPersistentCloudKitContainer allows syncing Core Data across devices via iCloud, subject to strict limits.

For the most part, Core Data strives to provide a complete solution out of the box. But this comes at the cost of extensibility should requirements evolve over time.

So Which Database Would You Choose – SQLite vs Core Data?

This guide covered a lot of ground comparing SQLite vs Core Data – from persistence architectures and querying to performance and extensibility.

Here are some key takeaways when comparing SQLite vs Core Data:

  • SQLite is a compact, embeddable SQL database engine. It provides maximum control and predictability tied closely to physical media performance.
  • Core Data is an object graph and persistence framework. It delivers abstraction from the raw store through declarative fetching and rich features for model objects.
  • There is no universally superior choice between SQLite and Core Data. The best option depends on your app’s specific requirements and constraints.

Hopefully, the comparisons and code examples provided here will help you choose the best local database for your next iOS app. Whichever data persistence technology you leverage, build something great that delights users!