最近数据库遇上些性能问题,分析了下原因之后发现时mysql在执行query时没有选择正确的索引。mysql的query plan为啥选错了索引,一时半会也没找到解决方案,于是就想着在django这层来告诉mysql如何选取索引。
mysql的index hint语法比较简单,参见其文档。不过django却不支持这种特定语法的orm生成,没有办法写下面这样的语句,
Post.objects.filter(id=1).force_index('primary')
当然也有“简单”的做法,就是采用django对raw sql的支持,
Post.objects.raw('select * from blog_post force index (primary) where id = 1')
采用raw sql也不是不可接受,但总还是过于繁琐,此外原有每一个想指定index的代码都要进行这样的改写,代码改动相对来说大了点。另一个不方便之处在于,testcase依赖的sqlite不支持这么个语法,于是还需要在testcase中对每个地方再进行一次处理。“简单”不简单。
网上搜了一下没有发现好的解决方案,django不支持这种专属特定数据库的操作。这么着只能自己找寻解决之道了。
设想的方案是在django生成sql的时候插入force index,于是去看了django sql生成部分的代码,果然还是收获到了解决方案。
django中sql的生成在,django.db.models.sql.query中,对与每一个query set,可以进行如下调用,
print str(Post.objects.filter(id=1).query)
上面这句就会输出对应的sql,这个和,
from django.db import connection
print connection.queries
中看到的sql会是一致的。更深入去看,sql的生成是在from django.db.models.sql.compiler.SQLCompiler的as_sql方法中。找到了sql的生成处,那么解决方案就来了,
- 替换SQLCompiler的as_sql方法
- 让QuerySet支持设置index
具体来看代码吧,
from django.db.models.sql.compiler import SQLCompiler
# 替换as_sql
old_as_sql = SQLCompiler.as_sql
def as_sql(self, with_limits=True, with_col_aliases=False):
sql, params = old_as_sql(self, with_limits, with_col_aliases)
index = sql.index(' WHERE ')
if hasattr(self.query, 'index'):
sql = sql[:index] + (' force index (%s) ' % getattr(self.query, 'index')) + sql[index:]
return sql, params
SQLCompiler.as_sql = as_sql
def force_index(self, index):
setattr(self.query, 'index', index)
使用方法
qs = force_index(Post.objects.filter(id=1), 'primary')
之后通过connection.queries去查看实际执行的sql语句时就可以发现增加上了force index。
虽说force index不是什么好方法,但临时解决方案只能想到这了,权宜之计,先把问题解决了再进一步寻找更靠谱的优化方法吧。