今天小熙学习资料,看到了一个有意思的问题,数据库查询的1+N问题。查了下资料,汇总下前人的经验,分享下。
一. 1+N问题
-
场景:
基本场景 : Article(文章)关联Category(分类)
class Category(models.Model): name = models.CharField(max_length=30) class Article(models.Model): title = models.CharField(max_length=30) body = models.TextField() category = models.ForeignKey(Category) time = models.DateTimeField() #----前端列表页模板 {% for a in Article.objects.all %} {{ a.title }} {{ a.category.name }} {% endfor %}
-
过程:
(1)在生成列表页面时,首先执行一次 (页面获取文章列表)
select * from article limited 0,N
(2)然后逐条获取 category.name,又需要执行N次 (在文章类获取到后,再次获取文章下一级的分类)
select name from category where id = category_id
-
结论
所以N+1问题其实应该叫做1+N 问题,这只是一个数据库设计模式的问题.但是会对数据库带来很大的压力,一个简单的列表页可能会有几百次数据库查询
N+1问题并不是ORM独有,只是使用orm的时候,数据库表中的行变成一个对象,于是很自然的就容易使用上面的方法来进行查询不使用orm进行编程的情况,一般直接用子查询或者inner join ,但是
select a.*,c.name from article a,category b where a.category_id = b.id
子查询或者inner join对数据库来说,也是很费资源的操作,因为需要锁表,高并发的情况下很容易锁死
二. 优化解决
-
查询的时候join子表。(不推荐)
(1). 优点:一次查询.
(2). 缺点: 做分页查询的时候不准,如果关联子表多,笛卡尔积会非常大,消耗资源,高并发可能有死锁问题
-
分步优化
(1). 描述:先发起主表的1次查询,把主表的ID都收集起来, 再发起一次性查询把所有子表的数据抓取出来。
(2). 优点: 把1+N查询变成了1+1查询,避免了系统的死锁,提高了并发响应能力
(3). 缺点: 第二次查询的结果需要人工处理拼装到对象里,没有了工具的自动化辅助,会花费更多的cpu时间。
-
查询的时候不join子表, 另外发起select去抓取子表数据
(1)缺点: 1+N查询问题, 发起的sql数量会非常恐怖
-
利用懒加载延缓抓取的时机
(1). 优点: 一开始只有1次主表查询,不会立即产生1+N
(2). 缺点:但是立马在循环里get被懒加载的属性, 那么和第四种方案没有什么区别,这种方法在Hibenrate里有一个变种,就是通过设置batch fetch size来减少N次查询的次数(感兴趣的话可以点击详解描述链接看看)。
小熙借鉴了下大佬的问题描述鲁塔弗原创文章,自己替换了下解决的方法,有兴趣可以看看其他的。欢迎联系,辩论。