From 8138600dd1f555ba3dfc66d5d420c49f7f168938 Mon Sep 17 00:00:00 2001 From: Agniva De Sarker Date: Mon, 10 Aug 2020 22:40:19 +0530 Subject: [PATCH] MM-27575: Use index hints to prevent index_merge_intersection (#15207) Even after specifying all the 3 columns to coerce MySQL into choosing the right multi-column index, it sometimes uses 2 separate indices and does an index_merge of them. This creates problems because the DeleteAt=0 search is essentially the entire Posts table, and causes a disastrously bad performance than even choosing the wrong index (idx_create_at). The problem with this approach is that we hardcode the decision to a specific index when MySQL was free to choose the right index depending on table statistics. However, there does not appear to be a case where this index can cause regressions than using some other index. Another option here was to set optimizer_switch="index_merge_intersection=off" at a session level for a transaction and then switch it back on after the query is done. However, this can cause some unintentional consequences because this setting is only available at a session level and not at a query level. There is no need to set something at the session level when an index hint suffices. https://mattermost.atlassian.net/browse/MM-27575 --- store/sqlstore/post_store.go | 30 +++++++++++++++++++++++++++--- 1 file changed, 27 insertions(+), 3 deletions(-) diff --git a/store/sqlstore/post_store.go b/store/sqlstore/post_store.go index a14b7d15e4..fba4614a94 100644 --- a/store/sqlstore/post_store.go +++ b/store/sqlstore/post_store.go @@ -708,10 +708,18 @@ func (s *SqlPostStore) getPostsAround(before bool, options model.GetPostsOptions direction = ">" sort = "ASC" } + table := "Posts p" + // We force MySQL to use the right index to prevent it from accidentally + // using the index_merge_intersection optimization. + // See MM-27575. + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" + } + replyCountSubQuery := s.getQueryBuilder().Select("COUNT(Posts.Id)").From("Posts").Where(sq.Expr("Posts.RootId = (CASE WHEN p.RootId = '' THEN p.Id ELSE p.RootId END) AND Posts.DeleteAt = 0")) query := s.getQueryBuilder().Select("p.*") query = query.Column(sq.Alias(replyCountSubQuery, "ReplyCount")) - query = query.From("Posts p"). + query = query.From(table). Where(sq.And{ sq.Expr(`CreateAt `+direction+` (SELECT CreateAt FROM Posts WHERE Id = ?)`, options.PostId), sq.Eq{"ChannelId": options.ChannelId}, @@ -811,9 +819,17 @@ func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before sort = "ASC" } + table := "Posts" + // We force MySQL to use the right index to prevent it from accidentally + // using the index_merge_intersection optimization. + // See MM-27575. + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" + } + query := s.getQueryBuilder(). Select("Id"). - From("Posts"). + From(table). Where(sq.And{ direction, sq.Eq{"ChannelId": channelId}, @@ -841,9 +857,17 @@ func (s *SqlPostStore) getPostIdAroundTime(channelId string, time int64, before } func (s *SqlPostStore) GetPostAfterTime(channelId string, time int64) (*model.Post, *model.AppError) { + table := "Posts" + // We force MySQL to use the right index to prevent it from accidentally + // using the index_merge_intersection optimization. + // See MM-27575. + if s.DriverName() == model.DATABASE_DRIVER_MYSQL { + table += " USE INDEX(idx_posts_channel_id_delete_at_create_at)" + } + query := s.getQueryBuilder(). Select("*"). - From("Posts"). + From(table). Where(sq.And{ sq.Gt{"CreateAt": time}, sq.Eq{"ChannelId": channelId},