dbaplus社群

被骂了20年的N+1查询问题,为何在SQLite上不存在?

事情的起因是SQLite自己惹的。

SQLite官网有一篇名为《SQLite的适用场景》的文档,里面提到了一件事:SQLite官方网站本身由Fossil版本控制系统驱动,而Fossil生成的每个动态页面——比如时间线、工单、Wiki——大约要执行200条SQL语句。

注意,这话是SQLite官方自己写在文档里的——竞争对手没挖,安全研究者没扒,SQLite自己大大方方摆在了台面上:我们一个页面,200条SQL。

然后读者先炸了。

“一个页面两百条SQL?这简直是糟糕透顶的设计。” “对大多数网站来说,两百条查询是太多太多太多了。” “N+1查询问题是教科书级别的反面教材,SQLite团队连这个都不知道吗?”

骂得有理有据,因为任何上过数据库优化课的人都能背出来:每条SQL就是一次网络往返,200次往返叠在一起,页面响应时间直奔天际。

整个推理链条完全正确,每一个环节都没有问题——只有一个前提被漏掉了:批评者们默认了一个他们甚至没有意识到的假设,即数据库一定跑在另一台服务器上。

SQLite的作者D. Richard Hipp看到这些评论时,大概是最平静的那个人,因为他知道这些愤怒的程序员在骂一个SQLite里根本不存在的概念。SQLite没有网络往返,从来不跟服务器通信,它就是一个嵌入在应用程序里的函数库,每一条”查询”本质上是一次C语言函数调用。

函数调用和网络往返之间的性能差距,大约是六个数量级。

但光讲道理是不够的,SQLite团队的回应方式比任何白皮书都更有杀伤力。

绝大多数数据库产品遇到性能争议时,标准公关流程是发一篇优化指南,教你如何把查询从两百条降到二十条,而SQLite的选择截然相反。他们从自己网站的一次真实页面渲染中抓取了完整的SQL执行日志——示意图、示例代码这些花活一概没有,直接上2016年9月16日那天,用户访问时间线页面时,数据库引擎实际执行的每一条SQL语句——然后把这份两百多行的日志全文贴了出来,附上一行淡定的标注:

“50条条目的时间线页面,总生成时间不到25毫秒。”

翻译成人话就是:你骂我之前,先把日志看完,看完了再告诉我问题出在哪儿。

“你确定问题出在查询数量上吗?”

一、N+1为什么是反模式——以及这个”为什么”比”是什么”重要一百倍

先理清概念。N+1查询问题,说的是应用程序先执行一条查询拉出一个列表(1次),再对列表里的每一项分别执行查询(N次),总共N+1次。典型场景:先查出50条时间线条目,再对每条条目分别查标签、权限、父节点。

在MySQL和PostgreSQL的世界里,这确实是要命的设计:应用程序跑在一台机器上,数据库跑在另一台机器上,每条SQL都要走一遍完整的TCP/IP协议栈——应用打包SQL→操作系统网络栈→网卡→交换机→数据库服务器网卡→操作系统→数据库引擎→计算结果→原路返回。这个流程每执行一次叫作一次”消息往返”(message round-trip),每一次往返,即使是本地网络,延迟也以毫秒计。

200次往返,哪怕每次只要1毫秒,光网络开销就是200毫秒——还没算上查询本身的计算时间,而用户感知到”这个网页有点慢”的阈值大约只有100毫秒。所以200条SQL的页面,在传统数据库架构下,确实是慢的,骂得对。

但问题就在这里。

SQLite既没有TCP/IP协议栈,也没有消息往返——应用程序调用SQLite的API时,SQLite的代码被直接编译进同一个进程,数据和索引躺在同一个文件系统的同一个文件里。一条查询的执行路径是:sqlite3_prepare_v2()→解析SQL→查B树索引→从磁盘读页面→返回结果,以上所有操作全部发生在当前进程的当前线程上,没有序列化,没有网络栈,没有上下文切换。

用Richard Hipp的原话说:”SQLite is not client/server. Queries do not involve message round-trips, only a function call.”

翻译成人话:你们骂的那个东西,SQLite里根本没有。

二、25毫秒,和它背后的代码架构账

SQLite官网由Fossil版本控制系统驱动,Fossil的每个动态页面——时间线、工单、Wiki——的SQL执行模式都是同一套逻辑。

先来一条大的,把所有时间线条目从数据库里拉出来的主查询,涉及多表关联、子查询、条件过滤:

INSERT OR IGNORE INTO timeline SELECT  blob.rid AS blobRid,  uuid AS uuid,  datetime(event.mtime,toLocal()) AS timestamp,  coalesce(ecomment, comment) AS comment,  coalesce(euser, user) AS user,  blob.rid IN leaf AS leaf,  bgcolor AS bgColor,  event.type AS eventType,  (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref    WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid      AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags,  tagid AS tagid,  brief AS brief,  event.mtime AS mtime FROM event CROSS JOIN blobWHERE blob.rid=event.objid AND NOT EXISTS(SELECT 1 FROM tagxref WHERE tagid=5 AND tagtype>0 AND rid=blob.rid) ORDER BY event.mtime DESC LIMIT 50;

这条查询同时证明了另一件事:SQLite完全有能力执行复杂的大查询——多表JOIN、嵌套子查询、GROUP_CONCAT聚合,MySQL能做的它都能做。如果你愿意用一条巨型SQL把整个页面需要的数据全部捞出来,SQLite不会有任何抱怨。

但Fossil偏不。

大查询跑完之后,Fossil对时间线上的每一个条目,又分别发出各自独立的小查询。比如对编号68028的条目,它依次查标签、查父节点链接、查权限:

SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68028;SELECT pid FROM plink WHERE cid=68028 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68028;

然后对编号68026的条目,再来一遍同样的查询,接着是68024、68018、68012……一共50个条目,每个至少3条小查询,加上页面首尾的配置查询和统计查询——整整齐齐,200条以上。

任何一个MySQL DBA看到这个日志都会心脏骤停。但页面底部赫然写着:生成耗时,少于25毫秒。

25毫秒。如果你的显示器是60Hz刷新率,这个时间够刷一帧半的画面。

更值得注意的细节是这份性能数据的进一步拆解:在这25毫秒里,真正花在SQLite数据库引擎内部的时间只占少数几毫秒,剩余消耗在HTTP处理、模板渲染、HTML输出上。那200多条SQL的查询开销,在整个页面的性能剖面图里甚至排不进前三。

把嘴巴里”200条一定慢”的肌肉记忆咽回去,你看到的事实是:当每条查询的开销从”一次洲际航班”降级为”在家里走两步”,200条查询就等价于200次从客厅走到厨房——谁会在意自己一天从客厅到厨房走了多少步?

三、有人在读教科书,有人在读教科书封面

到这一步,性能问题已经讲清楚了,但真正值得展开的戏肉在后面。

Fossil团队选择N+1模式,跟炫耀SQLite性能没有任何关系,他们的账算在了另一个方向上——代码维护成本。

时间线页面展示的不止一种内容:提交记录(check-in)是一类,工单(ticket)是另一类,Wiki页面又是完全不同的一类,每一种类型的条目需要展示的信息不同、渲染逻辑不同、查询逻辑也不同。

提交记录需要查分支名、父节点链接、是否处于leaf位置;工单需要查关闭状态、处理人、优先级;Wiki页面需要查修订历史、最后编辑者、关联标签。

如果你把所有东西揉进一条巨型SQL里捞出来,你得到的是一条膨胀到几百行的JOIN语句,和一个把所有渲染分支塞在一起的巨型模板函数。提交人和工单处理人的列名在同一个结果集里打架,状态字段的语义互相污染。改一个工单显示逻辑,有可能震塌整个时间线的SQL结构。

N+1模式的反面,是在两套代码组织哲学之间做的一场取舍。把所有数据塞进一条查询,代码只有一块,但这块代码里装着三重交叉职责:数据获取、类型判断、格式渲染,谁也改不动,谁也不敢动。N+1模式把获取提交数据的代码放在提交渲染模块里,获取工单数据的代码放在工单渲染模块里,获取Wiki数据的代码放在Wiki渲染模块里。每种类型的开发人员只需要理解自己那块的查询逻辑,互不越界。

这是教科书级的关注点分离,只不过它恰好和另一本教科书(数据库性能优化)撞了车——两本教科书都对,但在SQLite的底盘上,关注点分离这本的分量更重。

这里有一个几乎在所有技术争论中都会复现的认知陷阱:把一门技术栈下形成的工程判断,当作所有技术栈下的物理定律。

N+1是反模式,因为网络往返昂贵;SQLite没有网络往返,所以N+1不是性能问题——这条推理链本身无懈可击。但在二十年的传播中,第一句话和第二句话之间的”因为”被磨掉了,只剩下一个光溜溜的结论:N+1在任何时候、任何场景、任何数据库上都是坏的。

四、迁移结论的人很多,迁移前提的人很少

这个剧本在技术界反复上演。

微服务拆分,原教旨是解决大单体在特定组织规模下的协调成本爆炸。传到后来变成了”微服务就是比单体好”,至于好在哪里、前提条件是什么,不重要了,拆就完了。容器化,原教旨是解决物理机时代的环境漂移和依赖地狱。传到后来变成了”容器化就是现代化”,一个十个人访问的内部工具也要上Kubernetes,不问你为什么,问就是最佳实践。

ORM的诞生,原始动机之一就是帮你自动合并N+1查询——Hibernate有@BatchSize,Entity Framework有Include(),Django ORM有select_related()和prefetch_related(),这些工具解决的是同一个问题:在客户端/服务器数据库上,查询数量太多会要命。但当数据库引擎变成SQLite——一个连进程边界都不跨的函数库——ORM里那些为了减少查询数量而引入的抽象层、批处理策略、预加载配置,从”必要的基础设施”变成了”不必要的复杂度”。

同样的现象在SQLite社区内部也有倒影。很多人第一次接触SQLite时的本能反应是”这个数据库太弱了,不适合生产环境”。他们的证据链是:SQLite不支持并发写入→高并发场景下MySQL更好→所以SQLite是玩具。

这个推理漏掉了一个关键事实:大多数网站和应用的实际写入并发量,远远低于开发者自以为的水平。SQLite的官方文档给出了一个让人沉默的数据:SQLite官方网站本身日均访问量约50万次动态页面请求,单台服务器承载,每个页面约200条SQL——正常运行,没有宕机,没有并发瓶颈。

SQLite作者对这个问题的回应有一种外科手术式的精准:”SQLite does not compete with client/server databases. SQLite competes with fopen().”

SQLite的竞争对手不是MySQL,是文件读写函数——当你把复杂度基线从”一个分布式数据库集群”拉到”一次文件操作”的时候,200条SQL的性能负担露出了它的真实面目:接近于零。

很多人在这个点上停住了,觉得这不过是一个技术冷知识,但如果把视角拉高,会看到一个更大的图案。

软件工程的代际传承,存在一个结构性的信息衰减。第一代工程师在某个具体技术条件下发现了一个问题,设计了一个解决方案,写出了文档。第二代工程师读到了文档,记住了方案,但未必知道当初的问题长什么样。第三代工程师接手代码时,方案已经成了铁律,问题已经成了历史,前提条件已经沉入海底——水面上只剩下一条孤零零的戒律:N+1是不好的。

把责任归结为某一代人偷懒,公平吗?恐怕不,这种衰减是知识在大规模传播中的必然宿命。每一代传下去的,是结论,是规则,是面试题的标准答案,但当初那个”为什么”——那个让规则之所以成为规则的具体场景、具体约束、具体成本结构——传着传着就散了。

SQLite的200条SQL恰好是一台完美的前提条件压力测试机,它对着所有面试过”N+1问题怎么解决”的开发者发出同一个灵魂拷问:你知道为什么N+1是问题吗?还是你只知道N+1是问题?

五、与其优化查询数量,不如理解查询成本

SQLite官网上的时间线页面至今仍然在运行,那个被骂了多年的架构每天都在生产流量面前正常工作。以下是生成一个典型时间线页面的完整SQL日志(2016年9月16日抓取)——从数据库打开到页面渲染结束,没有删减,没有”优化后”的表演版本:

-- sqlite3_open: /home/drh/sqlite/sqlite/.fslckoutPRAGMA foreign_keys=OFF;SELECT sql FROM localdb.sqlite_schema WHERE name=='vfile';-- sqlite3_open: /home/drh/.fossilPRAGMA foreign_keys=OFF;SELECT value FROM vvar WHERE name='repository';ATTACH DATABASE '/home/drh/www/repos/sqlite.fossil' AS 'repository' KEY '';SELECT value FROM config WHERE name='allow-symlinks';SELECT value FROM global_config WHERE name='allow-symlinks';SELECT value FROM config WHERE name='aux-schema';SELECT 1 FROM config WHERE name='baseurl:http://';SELECT value FROM config WHERE name='ip-prefix-terms';SELECT value FROM global_config WHERE name='ip-prefix-terms';SELECT value FROM config WHERE name='localauth';SELECT value FROM vvar WHERE name='default-user';SELECT uid FROM user WHERE cap LIKE '%s%';SELECT login FROM user WHERE uid=1;SELECT cap FROM user WHERE login = 'nobody';SELECT cap FROM user WHERE login = 'anonymous';SELECT value FROM config WHERE name='public-pages';SELECT value FROM global_config WHERE name='public-pages';SELECT value FROM config WHERE name='header';SELECT value FROM config WHERE name='project-name';SELECT value FROM config WHERE name='th1-setup';SELECT value FROM global_config WHERE name='th1-setup';SELECT value FROM config WHERE name='redirect-to-https';SELECT value FROM global_config WHERE name='redirect-to-https';SELECT value FROM config WHERE name='index-page';SELECT mtime FROM config WHERE name='css';SELECT mtime FROM config WHERE name='logo-image';SELECT mtime FROM config WHERE name='background-image';CREATE TEMP TABLE IF NOT EXISTS timeline(  rid INTEGER PRIMARY KEY,  uuid TEXT,  timestamp TEXT,  comment TEXT,  user TEXT,  isleaf BOOLEAN,  bgcolor TEXT,  etype TEXT,  taglist TEXT,  tagid INTEGER,  short TEXT,  sortby REAL);INSERT OR IGNORE INTO timeline SELECT  blob.rid AS blobRid,  uuid AS uuid,  datetime(event.mtime,toLocal()) AS timestamp,  coalesce(ecomment, comment) AS comment,  coalesce(euser, user) AS user,  blob.rid IN leaf AS leaf,  bgcolor AS bgColor,  event.type AS eventType,  (SELECT group_concat(substr(tagname,5), ', ') FROM tag, tagxref    WHERE tagname GLOB 'sym-*' AND tag.tagid=tagxref.tagid      AND tagxref.rid=blob.rid AND tagxref.tagtype>0) AS tags,  tagid AS tagid,  brief AS brief,  event.mtime AS mtime FROM event CROSS JOIN blobWHERE blob.rid=event.objid AND NOT EXISTS(SELECT 1 FROM tagxref WHERE tagid=5 AND tagtype>0 AND rid=blob.rid) ORDER BY event.mtime DESC LIMIT 50;-- SELECT value FROM config WHERE name='timeline-utc';SELECT count(*) FROM timeline WHERE etype!='div';SELECT min(timestamp) FROM timeline;SELECT julianday('2016-09-15 14:54:51',fromLocal());SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob WHERE blob.rid=event.objid AND mtime<=2457647.121412037);SELECT max(timestamp) FROM timeline;SELECT julianday('2016-09-24 17:42:43',fromLocal());SELECT EXISTS (SELECT 1 FROM event CROSS JOIN blob WHERE blob.rid=event.objid AND mtime>=2457656.238009259);SELECT value FROM config WHERE name='search-ci';SELECT value FROM vvar WHERE name='checkout';SELECT value FROM config WHERE name='timeline-max-comment';SELECT value FROM global_config WHERE name='timeline-max-comment';SELECT value FROM config WHERE name='timeline-date-format';SELECT value FROM config WHERE name='timeline-truncate-at-blank';SELECT value FROM global_config WHERE name='timeline-truncate-at-blank';SELECT * FROM timeline ORDER BY sortby DESC;SELECT value FROM config WHERE name='hash-digits';SELECT value FROM global_config WHERE name='hash-digits';SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68028;SELECT pid FROM plink WHERE cid=68028 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM tagxref WHERE rid=68028 AND tagid=9 AND tagtype>0;SELECT value FROM config WHERE name='timeline-block-markup';SELECT value FROM config WHERE name='timeline-plaintext';SELECT value FROM config WHERE name='wiki-use-html';SELECT value FROM global_config WHERE name='wiki-use-html';SELECT 1 FROM private WHERE rid=68028;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68026;SELECT pid FROM plink WHERE cid=68026 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68026;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68024;SELECT pid FROM plink WHERE cid=68024 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68024;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68018;SELECT pid FROM plink WHERE cid=68018 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68018;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68012;SELECT pid FROM plink WHERE cid=68012 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68012;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68011;SELECT value FROM config WHERE name='details';SELECT pid FROM plink WHERE cid=68011 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM tagxref WHERE rid=68011 AND tagid=9 AND tagtype>0;SELECT 1 FROM private WHERE rid=68011;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68008;SELECT pid FROM plink WHERE cid=68008 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68008;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68006;SELECT pid FROM plink WHERE cid=68006 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68006;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=68000;SELECT pid FROM plink WHERE cid=68000 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=68000;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67997;SELECT pid FROM plink WHERE cid=67997 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67997;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67992;SELECT pid FROM plink WHERE cid=67992 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67992;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67990;SELECT pid FROM plink WHERE cid=67990 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67990;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67989;SELECT pid FROM plink WHERE cid=67989 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67989;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67984;SELECT pid FROM plink WHERE cid=67984 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67984;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67983;SELECT pid FROM plink WHERE cid=67983 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67983;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67979;SELECT pid FROM plink WHERE cid=67979 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67979;SELECT value FROM config WHERE name='ticket-closed-expr';SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='1ec41379c9c1e400' AND tkt_uuid<'1ec41379c9c1e401';SELECT 1 FROM private WHERE rid=67980;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67977;SELECT pid FROM plink WHERE cid=67977 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='1ec41379c9c1e400' AND tkt_uuid<'1ec41379c9c1e401';SELECT 1 FROM private WHERE rid=67977;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='1ec41379c9c1e400' AND tkt_uuid<'1ec41379c9c1e401';SELECT 1 FROM private WHERE rid=67974;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67971;SELECT pid FROM plink WHERE cid=67971 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67971;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67972;SELECT pid FROM plink WHERE cid=67972 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67972;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67969;SELECT pid FROM plink WHERE cid=67969 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67969;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67966;SELECT pid FROM plink WHERE cid=67966 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67966;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67962;SELECT pid FROM plink WHERE cid=67962 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67962;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67960;SELECT pid FROM plink WHERE cid=67960 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67960;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67957;SELECT pid FROM plink WHERE cid=67957 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67957;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67955;SELECT pid FROM plink WHERE cid=67955 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67955;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67953;SELECT pid FROM plink WHERE cid=67953 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='5990a1bdb4a073' AND tkt_uuid<'5990a1bdb4a074';SELECT 1 FROM blob WHERE uuid>='5990a1bdb4a073' AND uuid<'5990a1bdb4a074';SELECT 1 FROM private WHERE rid=67953;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67941;SELECT pid FROM plink WHERE cid=67941 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67941;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67940;SELECT pid FROM plink WHERE cid=67940 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67940;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67938;SELECT pid FROM plink WHERE cid=67938 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67938;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67935;SELECT pid FROM plink WHERE cid=67935 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67935;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67934;SELECT pid FROM plink WHERE cid=67934 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67934;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67932;SELECT pid FROM plink WHERE cid=67932 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67932;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67930;SELECT pid FROM plink WHERE cid=67930 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67930;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67928;SELECT pid FROM plink WHERE cid=67928 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM tagxref WHERE rid=67928 AND tagid=9 AND tagtype>0;SELECT 1 FROM private WHERE rid=67928;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='0eab1ac7591f511d' AND tkt_uuid<'0eab1ac7591f511e';SELECT 1 FROM private WHERE rid=67919;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='01874d252ac44861' AND tkt_uuid<'01874d252ac44862';SELECT 1 FROM blob WHERE uuid>='01874d252ac44861' AND uuid<'01874d252ac44862';SELECT 1 FROM private WHERE rid=67918;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67916;SELECT pid FROM plink WHERE cid=67916 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='0eab1ac759' AND tkt_uuid<'0eab1ac75:';SELECT 1 FROM private WHERE rid=67916;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='a49bc0a8244feb08' AND tkt_uuid<'a49bc0a8244feb09';SELECT 1 FROM blob WHERE uuid>='a49bc0a8244feb08' AND uuid<'a49bc0a8244feb09';SELECT 1 FROM private WHERE rid=67914;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67913;SELECT pid FROM plink WHERE cid=67913 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='0eab1ac7591f' AND tkt_uuid<'0eab1ac7591g';SELECT 1 FROM private WHERE rid=67913;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67911;SELECT pid FROM plink WHERE cid=67911 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67911;SELECT status='Closed' OR status='Fixed' FROM ticket  WHERE tkt_uuid>='0eab1ac7591f511d' AND tkt_uuid<'0eab1ac7591f511e';SELECT 1 FROM private WHERE rid=67909;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67907;SELECT pid FROM plink WHERE cid=67907 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67907;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67899;SELECT pid FROM plink WHERE cid=67899 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67899;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67897;SELECT pid FROM plink WHERE cid=67897 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67897;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67895;SELECT pid FROM plink WHERE cid=67895 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67895;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67893;SELECT pid FROM plink WHERE cid=67893 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67893;SELECT value FROM tagxref WHERE tagid=8 AND tagtype>0 AND rid=67891;SELECT pid FROM plink WHERE cid=67891 AND pid NOT IN phantom ORDER BY isprim DESC;SELECT 1 FROM private WHERE rid=67891;SELECT count(*) FROM plink WHERE pid=67928 AND isprim   AND coalesce((SELECT value FROM tagxref                  WHERE tagid=8 AND rid=plink.pid), 'trunk')      =coalesce((SELECT value FROM tagxref                  WHERE tagid=8 AND rid=plink.cid), 'trunk');SELECT count(*) FROM plink WHERE pid=68011 AND isprim   AND coalesce((SELECT value FROM tagxref                  WHERE tagid=8 AND rid=plink.pid), 'trunk')      =coalesce((SELECT value FROM tagxref                  WHERE tagid=8 AND rid=plink.cid), 'trunk');SELECT count(*) FROM plink WHERE pid=68028 AND isprim   AND coalesce((SELECT value FROM tagxref                  WHERE tagid=8 AND rid=plink.pid), 'trunk')      =coalesce((SELECT value FROM tagxref                  WHERE tagid=8 AND rid=plink.cid), 'trunk');SELECT value FROM config WHERE name='show-version-diffs';SELECT value FROM config WHERE name='adunit-omit-if-admin';SELECT value FROM global_config WHERE name='adunit-omit-if-admin';SELECT value FROM config WHERE name='adunit-omit-if-user';SELECT value FROM global_config WHERE name='adunit-omit-if-user';SELECT value FROM config WHERE name='adunit';SELECT value FROM global_config WHERE name='adunit';SELECT value FROM config WHERE name='auto-hyperlink-delay';SELECT value FROM global_config WHERE name='auto-hyperlink-delay';SELECT value FROM config WHERE name='footer';PRAGMA database_list;PRAGMA database_list;PRAGMA localdb.freelist_count;PRAGMA localdb.page_count;

把这份日志完整地摊在读者面前,是SQLite团队做过的最有底气的一件事——它比任何白皮书都更有说服力,因为它是未经剪辑的原始记录:没有隐藏的批量优化,没有事后补的缓存层,时间线上每一个你看得到的条目,背后都是逐条查询、逐条渲染。

如果批评者愿意花五分钟看完这份日志,至少有一半人会沉默,而剩下一半人可能会开始问另一个问题——

“如果200条SQL真的没问题,那我过去十年学的那套数据库优化理论,有多少条是放之四海皆准的,有多少条是只在特定技术栈上成立的?”

这个问题,比SQLite本身的性能数据更有价值。

软件开发最大的认知债务,是把工程判断当成物理定律。物理定律不依赖上下文——重力在MySQL上有效,在SQLite上也有效,在纸上推导时也有效;工程判断是一个特定上下文下的最优解,换一个上下文,同一个判断可能从最优解退化成过度设计。

200条SQL在MySQL上是性能灾难,200条SQL在SQLite上是完全不值得优化的背景噪声,两个结论同时成立、互相不矛盾,因为它们的判据来自同一个根源:查询开销到底长什么样。

网络往返的代价是毫秒级,函数调用的代价是纳秒级,差了一百万倍。当你要优化的开销存在于网络层,合并查询是正确策略;当网络层直接不参与这件事,合并查询就从”性能优化”退化为”为整洁而整洁”。

很多开发者的职业生涯中,真正理解了数据库内部原理的时刻,往往发生在这样一个瞬间:第一次看到SQLite那200条SQL的完整日志,在心里快速算了一下25毫秒的加载时间,然后突然意识到——过去花了大把精力给MySQL写的查询合并优化方案,放在SQLite上全是无用功。

那个瞬间,才算是真正读懂了N+1问题。

>>>>

参考链接

  • 《SQLite的适用场景》

https://www.sqlite.org/whentouse.html#website

作者丨最后的绅士
来源丨网址:https://zhuanlan.zhihu.com/p/2044824229754013399?share_code=6oVxdvjyvEeJ&utm_psn=2045885026336780948
dbaplus社群欢迎广大技术人员投稿,投稿邮箱:[email protected]
Image
Image