ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

SQLite撑起中小型社交项目:从Schema设计到并发与迁移实践

SQLite撑起中小型社交项目:从Schema设计到并发与迁移实践 做了几年的中小型社交类项目我发现一个有意思的现象很多团队一提到社交网络立刻默认要上 MySQL 或者 PostgreSQL再不济也得是 MongoDB。但实际做下来对于早期项目、内部工具、垂直社群类应用SQLite 反而是最省心的那个。它单文件、零运维、事务完整、性能在绝大多数场景下足够用只要 Schema 和写并发策略设计得当支撑三五万日活用户完全不是问题。这篇文章就从我在真实项目里的实践出发聊聊 SQLite 在社交网络场景里的存储方案设计、锁与并发处理、性能实测和后续迁移路径帮你在做技术选型时多一个不被忽视的选项。1. 社交网络里 SQLite 的合理定位不是什么都能做但比想象中能做的多先说结论SQLite 适合社交项目中数据量可控、单机部署、以读为主、写并发不高的部分典型场景包括用户资料与关系链、私信会话、动态内容存储、点赞评论等。它也能支撑一定的写并发只要控制在合理范围且正确使用 WAL 模式和事务完全能覆盖从原型到生产的过程。但不适合的是海量用户同时写入、全文检索、多租户高隔离的复杂业务。1.1 为什么很多中小型社交项目低估了 SQLite我见过太多团队在项目初期就引进了完整的 MySQL 主从架构、Redis 缓存、消息队列结果业务量连一台 2 核 4G 的云主机都跑不满。这不是说这些技术不好而是存在明显的复杂度错配为了一个日新增几千条数据的项目付出了几倍的运维成本、备份成本和故障排查成本。SQLite 在这类场景下的优势是实打实的。第一它不需要独立的数据库服务进程嵌入在应用里减少了部署链路上的故障点第二单文件存储备份就是拷贝文件配合VACUUM INTO甚至能做到在线一致性备份第三它完整支持 ACID 事务在发一条动态同时更新点赞数、评论数、时间线这种复合写操作上事务保证比很多 NoSQL 方案更让人放心第四它读性能极其出色在一个几百 MB 的库文件上做带索引的点查耗时通常在个位数的毫秒级别。社交网络的核心路径读自己/好友的动态列表本质上是按用户和时间做倒序分页查询。只要建好(user_id, created_at)联合索引SQLite 在十万级数据量下返回一页 20 条记录完全不需要刻意优化。这个量级下SQL ite 和 MySQL 的差距可以用毫秒计算而运维上的差距是数量级的。1.2 边界条件什么时候应该果断说不当然我不主张在场景已经明显超出 SQLite 能力时还硬扛。当你遇到下面这些信号时就该认真考虑迁移到传统客户端-服务器数据库了写并发持续超过每秒几十个事务且事务是跨多行的大事务多个服务实例共享同一份数据需要通过 TCP 远程访问数据库文件数据量达到几十 GB 级别且查询需要全表扫描需要细粒度权限控制、在线跨节点扩展或完整的 SQL 用户体系需要长时间运行的写事务与读事务并行且互不干扰WAL 虽缓解了读写互斥但写写仍然串行。一句话概括SQLite 适合单机为王的阶段当你需要在多台服务器之间共享一块可写的存储时就该换装了。关键是在恰当的时候换而不是一开始就背上企业级数据库的沉重负担。2. SQLite 社交数据库的 Schema 设计从用户表到关系链的完整实操Schema 设计是决定性的一步。很多人觉得 SQLite 简单就直接建表结果等数据量上来之后才发现索引缺失、冗余混乱、改字段痛苦。SQLite 对ALTER TABLE的支持远不如 MySQL 灵活所以前期的 Schema 设计需要格外用心。2.1 用户信息表与关系链表的设计要点社交网络最基础的是用户表和关系表。用户表通常按这个思路建CREATE TABLE IF NOT EXISTS users ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, nickname TEXT NOT NULL, avatar_url TEXT, bio TEXT, password_hash TEXT, created_at INTEGER NOT NULL, -- 存 Unix 时间戳(秒) updated_at INTEGER ); CREATE INDEX idx_users_created_at ON users(created_at DESC);注意这里created_at用的是整数时间戳而不是DATETIME文本。原因有两个一是在 SQLite 里对整数做范围查询和排序比文本快二是后续如果要按时间分表或清理数据操作边界非常清晰。时间展示交给应用层处理。好友/关注关系是社交网络里最容易设计错的表。我常用的方案是一条记录存一段关系加上唯一约束CREATE TABLE IF NOT EXISTS follows ( user_id INTEGER NOT NULL, -- 主动关注的一方 follow_id INTEGER NOT NULL, -- 被关注的一方 status TEXT NOT NULL DEFAULT active, -- active / blocked created_at INTEGER NOT NULL, PRIMARY KEY (user_id, follow_id) ); CREATE INDEX idx_follows_follow_id ON follows(follow_id, status);很多初学者会问为什么不直接存user_id_a和user_id_b并规定a b这样一条记录就能存双向关系还省空间。但在真实社交场景里我关注了你和你关注了我是两个动作记录user_a - user_b与user_b - user_a方向信息是必要的。用两条记录或按照状态字段区分设计天然支持单向关注、好友双向两种模式的切换。查询某个人的粉丝列表时用follow_id索引反向查即可SELECT user_id FROM follows WHERE follow_id ? AND status active ORDER BY created_at DESC LIMIT 50;2.2 动态表、点赞表和评论表冗余字段的价值动态 feed 表的核心是加索引的时间戳 冗余的计数。我经历过一个血泪教训早期版本做点赞数/评论数统计时用COUNT(*)实时计算结果用户量到两万时接口就明显变慢。后来改成在 feed 表上冗余like_count和comment_count字段在点赞和评论事务里同步更新查询列表时直接读字段性能提升了十倍不止。CREATE TABLE IF NOT EXISTS feeds ( feed_id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, content_type TEXT NOT NULL DEFAULT text, -- text / image / video / link content_json TEXT NOT NULL, -- 内容体用 JSON 存灵活结构 like_count INTEGER NOT NULL DEFAULT 0, comment_count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE INDEX idx_feeds_user_time ON feeds(user_id, created_at DESC);点赞表的设计重点在于唯一约束防止同一用户对同一动态重复点赞CREATE TABLE IF NOT EXISTS likes ( target_type TEXT NOT NULL DEFAULT feed, -- feed / comment target_id INTEGER NOT NULL, user_id INTEGER NOT NULL, created_at INTEGER NOT NULL, PRIMARY KEY (target_type, target_id, user_id) );评论表的设计类似但要多加一层parent_id以支持楼中楼核心索引是(feed_id, created_at)CREATE TABLE IF NOT EXISTS comments ( comment_id INTEGER PRIMARY KEY AUTOINCREMENT, feed_id INTEGER NOT NULL, user_id INTEGER NOT NULL, parent_id INTEGER DEFAULT 0, -- 0 表示顶层评论 content TEXT NOT NULL, like_count INTEGER NOT NULL DEFAULT 0, created_at INTEGER NOT NULL ); CREATE INDEX idx_comments_feed_time ON comments(feed_id, created_at ASC);2.3 私信会话与消息表的两种建模思路私信是社交网络里复杂度最高的模块之一。常见的建模有只建消息表和会话表 消息表两种我强烈建议后者。原因很简单会话列表是高频查询而消息是高频写入。如果把两者混在一张表里每次查会话列表都要对消息表做聚合数据量一上来就容易成为瓶颈。会话表保存唯一会话信息用两个用户的 ID 拼接生成唯一的会话标识。同时冗余最后一条消息摘要CREATE TABLE IF NOT EXISTS conversations ( conv_id INTEGER PRIMARY KEY AUTOINCREMENT, user_a INTEGER NOT NULL, user_b INTEGER NOT NULL, last_message TEXT, last_msg_time INTEGER, unread_a INTEGER DEFAULT 0, -- user_a 的未读数 unread_b INTEGER DEFAULT 0, created_at INTEGER NOT NULL, UNIQUE (user_a, user_b) ); CREATE INDEX idx_conversations_user_a_time ON conversations(user_a, last_msg_time DESC); CREATE INDEX idx_conversations_user_b_time ON conversations(user_b, last_msg_time DESC); CREATE TABLE IF NOT EXISTS messages ( msg_id INTEGER PRIMARY KEY AUTOINCREMENT, conv_id INTEGER NOT NULL, sender_id INTEGER NOT NULL, content TEXT NOT NULL, msg_type TEXT NOT NULL DEFAULT text, status TEXT NOT NULL DEFAULT sent, -- sent / read created_at INTEGER NOT NULL ); CREATE INDEX idx_messages_conv_time ON messages(conv_id, msg_id ASC);私信消息的查询参照的是按会话取最近 N 条的逻辑(conv_id, msg_id)联合索引可以让翻页查询走索引覆盖避免 RowID 回表。这里有个细节消息表用msg_id排序其实等价于按时间排序因为INTEGER PRIMARY KEY AUTOINCREMENT是单调递增的。2.4 修改表结构SQLite 的 ALTER TABLE 和高效重建法前面说过 SQLite 对ALTER TABLE的支持很弱。它默认只能改表名、添加列ADD COLUMN直接改字段类型会报错。热搜里看到的sqlite修改字段的类型在实际操作中确实很常见但 SQLite 不提供 MySQL 的MODIFY COLUMN。正确做法是重建表新建一张新 Schema 的表把旧数据转换后导入然后删旧表、重命名新表。这个思路虽然听起来繁琐但配合事务做起来是安全的。我封装过一套通用流程PRAGMA foreign_keysoff; BEGIN TRANSACTION; -- 1. 新建临时表字段类型按新需求设计 CREATE TABLE users_new ( user_id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT NOT NULL UNIQUE, -- ... 新字段定义 ); -- 2. 将旧表数据导入新表用 CAST 做类型转换 INSERT OR IGNORE INTO users_new (user_id, username, ...) SELECT user_id, username, ... FROM users; -- 3. 删除旧表 DROP TABLE users; -- 4. 重命名新表 ALTER TABLE users_new RENAME TO users; -- 5. 重建索引 CREATE INDEX idx_users_created_at ON users(created_at DESC); COMMIT; PRAGMA foreign_keyson;整个流程最关键的步骤是第 5 步索引不会跟着RENAME一起带过来必须在重命名后重建。我在早期做 Schema 迁移时漏了这一步导致某次发布后查询性能骤降大部分用户 SQL 走了全表扫描。血的教训。3. 并发写入与锁机制社交场景最容易翻车的地方很多人对 SQLite 的刻板印象是只适合单机单用户。这个说法在默认配置下部分成立但只要你理解它的锁模型并正确配置SQLite 完全可以胜任中等规模的社交应用写入场景。并发问题往往是配置不当而不是 SQLite 本身不行。3.1 SQLite 的锁模型和 WAL 模式的意义SQLite 的锁是数据库文件级别的锁。在默认的 rollback journal回滚日志模式下一旦有写事务持锁整个数据库文件的读操作都会被阻塞这个特性确实让人头疼。但切换到 WALWrite-Ahead Logging预写式日志模式后读写之间不再互斥读操作可以和一个正在提交的写事务并行读到的是一致性的快照视图。启用 WAL 只需要一行配置但很多人在建库之后就把这条忘了PRAGMA journal_modeWAL; PRAGMA synchronousNORMAL; PRAGMA busy_timeout5000;journal_modeWAL是核心synchronousNORMAL在 WAL 模式下是安全性和性能的最佳平衡尽管理论上在断电瞬间可能丢一点最近提交的事务但对社交网络场景完全可接受busy_timeout5000的作用是当遇到写锁冲突时SQLite 会等待 5 秒而不是立即返回SQLITE_BUSY这个对用户体验的改善是决定性的。WAL NORMAL busy_timeout 三件套是 SQLite 在社交网络项目里能不能跑得动写并发的分水岭。用了它之后我服务里的写并发从每秒个位数事务提升到可以稳定吃下每秒几十个事务且读请求完全不受影响。3.2 应用层如何规避写写冲突串行化写事务即使开了 WALSQLite 的写事务依然是串行的。两个写事务同时提交时后发的那一个会被阻塞直到前面的提交结束。这本身不是问题问题是如果应用层没有合理控制写事务的粒度很容易放大锁等待时间。我在实践里总结出几条铁律写事务只做写操作。不要在写事务内部执行耗时的查询、网络请求或文件 IO这些都放在事务外完成。事务提交时间越短锁持有时间越短并发写冲突的概率越低。合并批量写入。一次循环里插入几百条消息的场景很常见如果每条 INSERT 都自动提交锁会被反复竞争。正确的做法是显式包在事务里# 错误示例循环内自动提交 for comment in comments: cursor.execute(INSERT INTO comments (feed_id, ...) VALUES (?, ...), ...) # 正确示例批量插入同一个事务 conn.execute(BEGIN IMMEDIATE TRANSACTION) try: for comment in comments: cursor.execute(INSERT INTO comments (feed_id, ...) VALUES (?, ...), ...) conn.commit() except: conn.rollback() raise用 BEGIN IMMEDIATE 抢占写锁。普通BEGIN是延迟获取写锁在事务内第一条读语句时只拿读锁第一条写语句时才升级为写锁这种升级过程可能在中途遇到锁冲突而失败。BEGIN IMMEDIATE则在事务一开始就请求写锁虽然牺牲了一点点并发窗口但换来了事务的确定性避免在操作中途突然SQLITE_BUSY。在短事务前提下这个取舍非常划算。控制单事务行数。单事务写入行数太多会让提交变慢内存消耗也会增加。比如一次性插入十万条导入数据时可以拆成每 1000 条一个事务提交。实测下这种方式相比单个大事务内存更平稳失败回滚的代价也更小。3.3 连接管理与单体应用的并发模型SQLite 不支持一个数据库文件同时被大量连接写入每个连接打开和关闭也有开销。在 Web 服务里最佳实践是使用连接池并对写操作做全局串行化通常是应用层加一个写锁或者用一个专门的写连接。具体来说可以维护两个连接一个长期驻留的写连接所有写事务都走它一组短连接或一个读连接池服务读请求。因为 WAL 模式下读和写可以并行这个模型能在不引入额外组件的情况下获得不错的并发表现。一个常被忽视的细节是打开 SQLite 连接时务必设置事务超时和 busy handler很多语言的驱动默认遇到锁冲突会直接抛异常。设置busy_timeout5000之后偶发的写冲突会在等待后自动重试成功应用层完全无感知。Python 的sqlite3模块可以在连接时指定Go 的mattn/go-sqlite3也支持_busy_timeout5000参数Java 的 Xerial sqlite-jdbc 则需通过setBusyTimeout设置。不同语言配置方式虽然各异但核心思路是同一个。3.4 事务隔离级别与社交场景下的读一致性SQLite 的默认隔离级别是可串行化SERIALIZABLE在 WAL 模式下表现为每个读事务看到一个一致的数据库快照。这意味着在同一个读事务里你反复查询同一张表得到的结果一定是同一个时间点的数据不管中间有没有别的写事务提交。对社交网络的动态流场景这种一致性是好的——用户在刷信息流时不会看到第一条是 10 秒前的动态第二条突然跳出一个刚刚插入的新动态这种割裂感。但如果你的读事务内需要主动感知最新数据比如用户点赞后立即刷新页面才看到点赞状态就不要长时间持有读事务。把拉取一页 feed做成一个短读事务拿完数据立即结束否则在长事务内始终看不到其他连接提交的新数据容易造成我点赞了怎么刷不出来的诡异 Bug。4. 十万条数据量和查询性能实测结果与优化策略热搜里有个问题特别真实sqlite十万条数据查询需要多久。很多人第一次接触 SQLite 都担心它到十万条就卡了。我不打算空谈直接说说我在一台 2 核 4G 云主机上的实测结果和优化做法。4.1 基础索引查询 vs 全表扫描指数级的差距我先构造了一个十万行的动态 feed 表字段包含 user_id、content、created_at然后对比几种查询。不带任何索引按user_id查用户的最近动态SQLite 只能全表扫描耗时约 120-180ms。这个数字单独看不算离谱但注意它随着数据量线性增长到五十万条时就是 600-900ms完全不可接受。建立(user_id, created_at DESC)联合索引后做同样的查询耗时降到 1ms 以内翻了差不多两百倍。这个差距就是索引的价值也是为什么我一直强调 Schema 阶段就要把索引结构想清楚。按created_at做全局时间线倒序分页建索引后查询 10 万条数据里最新的 20 条记录耗时同样在个位数的毫秒级因为 SQLite 可以利用索引的排序特性直接取头部数据不需要排序操作。重要的测试结论是SQLite 在十万条量级、走索引的前提下性能完全不是瓶颈。真正会出问题的场景是没有索引 全表扫描 大范围查询三者叠加。所以优化的第一步永远是审视查询语句有没有走索引。4.2 写入性能与事务合并的实测效果测完读再说写。之前我用单条自动提交的方式插入一万条消息记录总耗时大约 4.8 秒平均每秒约 2000 次写入。同一批数据改用批量事务每 1000 条一个事务之后总耗时压缩到约 0.5 秒速度提升了接近十倍。这个数字直接解释了为什么我前面反复强调合并写入事务——在有大量导入、消息批量推送、后台脚本灌数据的场景里事务粒度对性能是数量级的影响。如果你需要在业务里做数据导入SQLite 还提供了一个专门的生产力工具INSERT变体INSERT INTO ... SELECT ...批量插入来自查询的数据比逐行插入快得多。配合临时表和事务十万级别的数据导入可以在几秒内完成。4.3 模糊搜索场景SQLite 的 LIKE 与 FTS5 的选择社交动态经常需要按关键词搜索内容很多人的第一反应是写LIKE %关键词%。这种写法在 SQLite 里是禁止走索引的只能全表扫描10 万条数据下每次查询都要 200ms 以上体验很差。方案是 FTS5 扩展。SQLite 官方从 3.9.0 版本开始内置 FTS5 模块可以针对文本内容建全文索引查找效率和 MySQL 的 FULLTEXT 相比并不逊色。以动态内容为例CREATE VIRTUAL TABLE feeds_fts USING fts5( content, content_rowidfeed_id, contentfeeds ); -- 同步更新用触发器保持同步 CREATE TRIGGER feeds_ai AFTER INSERT ON feeds BEGIN INSERT INTO feeds_fts (rowid, content) VALUES (new.feed_id, new.content_json); END; CREATE TRIGGER feeds_ad AFTER DELETE ON feeds BEGIN INSERT INTO feeds_fts (feeds_fts, rowid, content) VALUES (delete, old.feed_id, old.content_json); END; -- 查询 SELECT feed_id, content_json FROM feeds_fts WHERE feeds_fts MATCH 社交 网络 ORDER BY rank LIMIT 20;FTS5 的 MATCH 语法支持中文分词虽然词法分析和拼音支持需要配合特定配置但至少从性能上说它把 200ms 的全表扫描变成了 5ms 内的倒排索引查找。如果你的社交项目里搜索是刚需请务必花时间把内容同步到 FTS5 虚拟表而不是硬扛 LIKE。4.4 翻页查询的两种写法OFFSET 与 Keyset Pagination信息流类业务八成离不开翻页。SQLite 里最简单的翻页写法是LIMIT 20 OFFSET 1000。OFFSET 翻页的问题是页码越深SQLite 需要扫描并丢弃的行越多取第 50 页时它得先把前 980 行读出来再丢掉耗时随页码线性增长。社交信息流更合适的方案是 Keyset Pagination也叫基于游标的分页——用上一页最后一条记录的排序键做过滤条件数据库可以直接通过索引定位翻页深度对性能无影响。SQL 大概长这样-- 第一页 SELECT * FROM feeds WHERE user_id ? ORDER BY created_at DESC, feed_id DESC LIMIT 20; -- 第二页传入上一页最后一条的 (created_at, feed_id) SELECT * FROM feeds WHERE user_id ? AND (created_at :last_created_at OR (created_at :last_created_at AND feed_id :last_feed_id)) ORDER BY created_at DESC, feed_id DESC LIMIT 20;很多人在 MySQL 里用过这个技巧但在 SQLite 里我见到用的人很少。实测数据10 万条数据量下OFFSET 翻到第 100 页耗时已经要 60-80ms而 Keyset 分页无论翻多少页都稳定在 1-3ms。如果你的信息流需要加载更多能力建议直接上 Keyset。5. 运维与扩展备份、迁移和通往更大数据库的升级路径选了 SQLite 不代表不用运维。相反因为架构简单运维的核心是备份要靠谱、恢复要快速、必要时能平滑迁走。5.1 备份策略文件拷贝与 VACUUM INTOSQLite 的备份最直观的是直接拷贝数据库文件但要先搞清楚一个前提WAL 模式下数据库文件里可能并没有包含所有最新提交的数据一部分在-wal文件中。直接拷贝主库文件会丢失 WAL 里的最新事务除非先执行一次 checkpoint 把日志合并回主库PRAGMA wal_checkpoint(FULL);更省心的方案是 SQLite 自带的在线备份 API无需中间步骤且不影响业务的读写。Python 里用sqlite3.Connection.backup()一行代码就能做一致性备份命令行环境下用sqlite3 source.db .backup backup.db也是同样的效果。这个方案强烈推荐——它既保证了数据一致性又不用自己关心 WAL 文件的处理。再进阶一点定期做VACUUM INTO生成一个紧凑的备份文件可以顺带压缩数据库体积和清理无用页。对于几天一备份的节奏这条命令是高效又省心的选择。数据库体积如果持续膨胀多半是大量 DELETE UPDATE 留下的空闲页导致VACUUM能有效回收。5.2 从 SQLite 平滑迁移到 MySQL / PostgreSQL 的实操思路业务走到某个阶段单机 SQLite 撑不住了这是情理之中的事情需要提前想清楚迁移路径。SQLite 和 MySQL/PostgreSQL 虽然 SQL 语法大致兼容但数据类型和部分函数存在差异直接导 SQL 文件容易报错。我经历过两次数据库迁移沉淀了一套流程用PRAGMA table_info导出每张表的完整 Schema手动改写成目标数据库语法特别留意INTEGER PRIMARY KEY AUTOINCREMENT对应各数据库自增主键写法、布尔和日期格式差异。数据导出用 CSV 或 JSON 作为中间格式不要直接导 SQL。CSV 对 NULL、特殊字符处理相对可控配合批处理导入工具既方便校验出错行也容易做重试。导入完成后做对账查询每张表的行数是否一致抽样校验关键业务表的数据值是否对应。线上切换采用双写或只读切换的灰度策略先切读出流量再切写入观察目标数据库负载曲线确认稳定后再停 SQLite。这个过程中最大的坑是数据类型隐性转换。SQLite 是动态类型系统字段声明为 INTEGER 时实际可以存文本这在迁移时会导致目标数据库的严格类型校验直接报错。所以迁移前一定要用typeof(column)排查异常数据早发现问题比到时候处理批量失败高效得多。5.3 SQLite 管理工具推荐DB Browser for SQLite 与命令行技巧日常开发和排障离不开趁手的管理工具。我最常用的是 DB Browser for SQLite也可以叫 DB4S开源、跨平台支持可视化的表结构查看、SQL 执行、数据编辑和简单的 ER 图展示。对刚接触 SQLite 的开发者它比命令行友好太多能直观地看到每条 SQL 的查询结果和索引命中情况。命令行侧也有不少高效的技巧。sqlite3工具加.timer on能精确查看每条 SQL 的耗时这对定位慢查询非常有用.expert命令会直接给出推荐索引——它在 SQLite 内部模拟查询计划后告诉你建哪些索引能提升当前语句的性能省去了反复试错的时间。对线上问题排查.checkpoint触发的时机、PRAGMA wal_checkpoint(FULL)的执行结果也都是在命令行环境下最直白的状态反馈。6. 一点实战总结SQLite 存社交数据什么时候该坚持、什么时候该撤回到开头那个话题SQLite 到底适不适合社交网络我的答案是在项目早期、数据量可控、单机部署的阶段它是最适合的数据库没有之一。它能让你把精力放在业务而不是运维上而且性能和事务能力远超大多数人的预期。但 SQLite 也存在天花板。我在实际项目里遇到的极限场景是写并发突然暴增 需要跨多节点分布这时候开始出现写锁等待和运维瓶颈就意味着迁移时机到了。我个人体会是SQLite 和 MySQL/PostgreSQL 并不是二选一的生死对决而是一条路线的两个阶段先用 SQLite 把业务跑通积累到真实数据量和真实并发模型后再带着明确的 Schema 和索引设计迁移到中心化数据库这种路线比一开始盲目上重型存储要平滑得多。如果你正在规划一个小型或中型社交应用不妨先放下社交网络必须用 MySQL的观念认真评估一下 SQLite 能不能先把事情做起来。
返回列表