做iOS开发这么多年,数据持久化永远是绕不开的一环。不管是缓存用户信息、存储离线数据还是维护本地业务记录,SQL都是最基础也最核心的能力之一。这篇文章我把iOS开发里和SQL、SQLite相关的内容完整梳理一遍,从底层原理到实际写码,从基础语法到性能优化,再到我踩过的坑和排查思路,一次性讲透。
内容适合刚接触iOS数据库的新手,也适合已经写了一段时间、想系统性补强数据库这块的老手。看完之后你至少能独立完成一个基于SQLite的本地数据层设计,并且知道哪些场景该用数据库、哪些场景其实不该用。
1. iOS本地数据库选型,为什么SQLite是默认答案
1.1 iOS本地持久化的主流方案对比
很多初学者刚接触iOS数据存储时,第一个想到的往往是UserDefaults,因为简单嘛,设置一个key存一个value,几行代码搞定。再加上后来有了SwiftData、CoreData这些苹果官方框架,很多人会问:是不是直接用官方推荐的就够了?我的答案是:看场景。
如果你要存的只是几十个键值对、用户偏好设置、登录token这类轻量数据,UserDefaults完全够用,非要上SQLite反而是过度设计。但如果你要做的功能是聊天记录列表、商品历史浏览记录、打车订单流水、运动轨迹上报队列这类结构化、需要查询和筛选的数据,UserDefaults就会迅速变成噩梦——读取全量数据、手动做过滤、内存暴涨、写频繁导致性能差,这些坑我全踩过。
CoreData则是苹果官方推出的对象关系映射框架,底层可以用SQLite做持久化存储,但它更像一个框架而不是单纯的数据库工具。它有自己的数据模型文件、上下文管理、请求模板,学习成本不低,而且因为封装层级多,排查问题的时候经常要穿透很多层才能定位到真正的SQL语句。在团队协作里,如果大家没有统一规范,CoreData写出来的查询代码很容易风格各异,维护起来头疼。
SQLite是文件型数据库,整个数据库就是一个独立的文件,iOS应用可以直接把库文件放在沙盒里,不需要单独的数据库服务器,也不需要网络连接。它支持的SQL语法覆盖了绝大多数日常开发需求,性能、稳定性、跨平台兼容性都非常好。这也是为什么App Store里大量应用的数据层核心依然是SQLite,包括微信、支付宝这类体量的应用。
1.2 沙盒机制决定了SQLite文件存放在哪里
iOS的每个应用都有自己独立的沙盒目录,应用只能访问自己沙盒内的文件,这就决定了SQLite数据库文件必须放在沙盒路径下面。通常我会把数据库放在Library/Caches目录或者Documents目录,两者的区别在于:
- Documents目录:会被系统自动备份到iCloud,适合放用户真正产生的、不可重新生成的业务数据,比如用户创建的笔记正文。
- Library/Caches目录:不会备份,适合放可以重新下载或者重建的缓存数据,比如图片缓存索引、历史搜索记录。
如果你的数据库混入了敏感信息,比如用户的手机号、设备标识,我倾向于放在Library/Application Support目录下,并且开启文件保护属性。核心原则是:涉及隐私的库文件,一定要设置NSFileProtectionComplete,避免备份泄露风险。
1.3 SQLite在iOS里的三种用法
iOS项目里接SQLite,实际开发中一般有三种路径:
- 直接用系统自带的sqlite3 C语言接口。好处是零依赖、最底层、完全可控;坏处是API确实很原始,写起来繁琐,每次查询都要手动绑定参数、释放句柄。
- 用FMDB这样的轻量封装库。FMDB在sqlite3的基础上封装了Objective-C对象,写起来舒服很多,而且提供了线程安全的FMDatabaseQueue,是很多老项目里最常见的选型。
- 用SQLite.swift这类Swift原生库。类型安全、链式语法,适合纯Swift项目,但它的高级API背后会把你的操作翻译成SQL,有时反而不如直接写SQL直观。
我的建议是:如果你做的是快速原型,选FMDB或者SQLite.swift都可以;如果你做的是长期维护、需要稳定性和可控性的项目,直接掌握原生sqlite3 API,再包一层自己的数据访问层,反而最踏实。因为不管封装多好,底层都是SQL,你越懂底层,就越不会被框架卡脖子。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. SQL基础语法拆解,先搞清楚增删改查
2.1 建表语句是地基,字段设计决定成败
SQL语言里最重要的不是花哨的查询技巧,而是建表。表结构设计不好,后面所有查询都会很难受。iOS本地数据库建表,最常用的是CREATE TABLE语句:
sql复制CREATE TABLE IF NOT EXISTS user_info (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL UNIQUE,
nickname TEXT,
avatar_url TEXT,
age INTEGER DEFAULT 0,
created_at REAL NOT NULL,
updated_at REAL NOT NULL
);
这里面有几个点值得展开说说。
第一,主键设计。我见过很多表喜欢用自增整型做主键,本身没问题,但要注意自增主键在数据删除、迁移时可能会产生主键冲突或者空洞,所以如果业务上有业务主键(比如用户ID、订单编号),更好的是用业务主键做UNIQUE约束,再配一个自增id作为内部引用。
第二,字段类型。SQLite属于动态类型系统,它不严格校验你插入的数据类型,一个声明为INTEGER的列你完全可以塞入文本。表面看很灵活,实际上埋了雷。所以建表之后,最好在业务层就做好类型校验,SQLite层面也要给关键字段NOT NULL约束兜底。
第三,时间字段。我习惯用REAL类型存Unix时间戳,一方面是排序方便,另一方面是时区问题可规避。用字符串存时间做比较容易因为格式不统一翻车,用时间戳就干净利落。
第四,索引与约束的取舍。约束写多了会影响写入性能,写少了又会造成脏数据。常规策略是:单表数据量在万级以下,不用急着做太多索引;超过十万级,必须给查询频繁的WHERE条件字段加索引,这个下面第三章节会细讲。
2.2 核心CRUD语句,一篇全讲透
SQL上手最快的方式就是直接记增删改查四类语句。我按日常使用频率排个序:
查询(SELECT)
sql复制-- 基础查询
SELECT user_id, nickname FROM user_info WHERE age > 18;
