最近数据库遇上些性能问题,分析了下原因之后发现时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的生成处,那么解决方案就来了,

  1. 替换SQLCompiler的as_sql方法
  2. 让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不是什么好方法,但临时解决方案只能想到这了,权宜之计,先把问题解决了再进一步寻找更靠谱的优化方法吧。