文章内容

2019/4/17 16:59:15,作 者: 黄兵

MySQL查询优化

索引(在MySQL中也叫“键(key)”)是存储引擎用于快速找到记录的一种数据结构。这是索引的基本功能之一。

索引对于良好的性能非常关键。尤其是当表中的数据量越来越大时,索引对性能的影响愈发重要。在数据量较小且负载较低时,不恰当色索引对性能的影响可能还不明显,但数据量逐渐增大时,性能则会急剧下降。

不过,索引却经常被忽略,有时候甚至被误解。索引优化应该是对查询性能优化最有效的手段了。索引能够轻易地将性能提高好几个数量级,“最优”的索引有时比一个“好的”索引性能要好几个数量级。创建一个真正“最优”的索引经常需要重写查询。

最近在线接收短信数据量增加很快,当时并没有很好的考虑到数据量增加会带来如此大的性能影响,现在数据量在140w左右,当翻到2w多页的时候,查询需要2s多,这对于网站来说简直是无法忍受。

必须要对数据库优化索引,原来索引已经无法满足性能要求了。

原来未经优化的查询:

SELECT * FROM table_name
where xx=xx and a=a and c=c order by d desc
limit 17280,10;

这个是原来的查询,一个查询需要2s左右,但是通过对xx,a,c列建立唯一索引,当然顺序也很重要,经过不断尝试,找出最优的索引顺序,关于这一点可以参考《高性能MySQL 第三版》[美]Baron Scbwartz,Peter Zaitsev,Vadim Tkacbenko著,第159页5.3.4节。

之后采用覆盖索引,来继续优化查询,具体查询实例:

SELECT * FROM table_name
  INNER JOIN (SELECT id FROM table_name WHERE a = true AND b = 1 ORDER BY id DESC  LIMIT 178240, 10)
    AS x ON (x.id=table_name.id);

关于覆盖索引可以参考《高性能MySQL 第三版》[美]Baron Scbwartz,Peter Zaitsev,Vadim Tkacbenko著,第171页5.3.6节。

虽然索引做好了,但是后端编码还需要修改,使用的是flask+SQLAlchemy(ORM)的方式,flask_sqlalchemy封装了一些东西,改动起来有些麻烦,但是经过不断查询资料,最终将这个问题解决。

原来查询分页的方式代码如下:

pagination = table_name.query.order_by(table_name.id.desc()) \
        .filter(and_(table_name.IsShow == True, table_name.e == f)) \
        .paginate(page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)

生成的sql没有经过任何优化,sql内容看上面第一个查询。

这里使用了一个flask_sqlalchemy的自动分页.paginate(),具体参考这里:flask_sqlalchemy.BaseQuery.paginate

但是使用封装好的分页方式,完全无法优化查询,必须要重写他的查询方式,之后参考stackoverflow,重写了分页查询的方式,具体代码如下:

page = request.args.get('page', 1, type=int)
    # 选最剩余短信内容

c_query = db.session.query(table_name.id).order_by(table_name.id.desc()).filter(
    and_(table_name.IsShow == True, table_name.e == f))

pagination = paginate(c_query, page, per_page=current_app.config['FLASKY_POSTS_PER_PAGE'], error_out=False)

surplus = pagination.items

.paginate()具体实现方式:

def paginate(c_query, page, per_page, error_out):
    if error_out and page < 1:
        abort(404)
    parent_items = c_query.limit(per_page).offset((page - 1) * per_page).subquery()
    items = db.session.query(table_name).join(parent_items, table_name.id == parent_items.c.id).all()
    if not items and page != 1 and error_out:
        abort(404)

    # No need to count if we're on the first page and there are fewer
    # items than we expected.
    if page == 1 and len(items) < per_page:
        total = len(items)
    else:
        total = c_query.order_by(None).count()

    return Pagination(c_query, page, per_page, total, items)

这个翻页重写参考了stackoverflow的这篇文章:How to paginate in Flask-SQLAlchemy for db.session joined queries?

这里面用了子查询,关于子查询的相关资料参考这里:SQL Subquery 概念

还使用了SQLAlchemyde 连接查询,具体可以参考这篇文章:SQLAlchemy ORM教程之三:RelationshipSQLAlchemy —— 多表查询

如果在调试中需要查看SQLAlchemy生成的SQL语句,可以参考这篇文章:在 flask-sqlalchemy中,如何将查询语句转换成原始SQL打印出来?

具体看看sql是如何执行的:

EXPLAIN SELECT * FROM table_name
  INNER JOIN (SELECT id FROM table_name WHERE a = true AND b = 1 ORDER BY id DESC  LIMIT 178240, 10)
    AS x ON (x.id=table_name.id);


通过这些优化以及重新修改分页的查询方式,将查询优化到了0.4s左右,140w的数据,有这个速度已经不错了。

在将来如果数据量达到了1000w级别,再怎么优化索引,也很难达到0.xs的速度,采用“分表”的方式,可能会解决这个问题,同时升级数据库的配置。

期待着这一天的到来。

陆陆续续搞了2天终于把这个问题解决了,以前一直要解决,一直没有下定决心去优化,但是这次借着服务器被DDOS的间隙,顺便优化查询,减轻数据库服务器的压力。

下面是优化前首字节加载时间:


优化之后首字节加载时间:


优化完成之后,又花了一上午完成了这篇文章,方便后来人。

由于这个项目是真实存在了,所以对于一些表结构进行了隐藏。

有问题欢迎留言。


参考资料:

1、《高性能MySQL 第三版》[美]Baron Scbwartz,Peter Zaitsev,Vadim Tkacbenko

2、灵活使用 SQLAlchemy 中的 ORM 查询

3、SQLAlchemy —— 多表查询

4、flask_sqlalchemy.BaseQuery.paginate

5、How to paginate in Flask-SQLAlchemy for db.session joined queries?

6、SQL Subquery 概念

7、SQLAlchemy ORM教程之三:RelationshipSQLAlchemy —— 多表查询

8、在 flask-sqlalchemy中,如何将查询语句转换成原始SQL打印出来?


黄兵个人博客原创。

转载请注明出处:黄兵个人博客 - MySQL查询优化

分享到:

发表评论

评论列表

user-ico

Eric on 回复 有用(0

你不像个程序员, 像个艺术家,优雅的艺术家!!

游客dtmc on 2019-05-28 10:03:53

博主回复:感谢您的肯定。