MM-21211: Optimize Channelstore.UpdateLastViewedAt (#13529)

* MM-21211: Optimize Channelstore.UpdateLastViewedAt

For postgres, we use CTEs to do both queries at one
and return the data.

For mysql, CTEs aren't available until version 8. So we just optimize the
existing query.

- We use IN instead of multiple OR conditions for the channel membership check.
According to mysql https://dev.mysql.com/doc/refman/5.7/en/comparison-operators.html#operator_in:

If no type conversion is needed for the values in the IN() list,
they are all constants of the same type, and expr can be compared to each of them
as a value of the same type (possibly after type conversion), an optimization takes place.

The values the list are sorted and the search for expr is done using a binary search,
which makes the IN() operation very quick.

- We use the previous column LastViewedAt to replace the value of LastUpdateAt
instead of doing a switch case again.

https://dev.mysql.com/doc/refman/5.6/en/ansi-diff-update.html

If you access a column from the table to be updated in an expression, UPDATE uses the
current value of the column.

Results (ran with getchannel profile):

Postgres:
| Metric | Baseline | Actual | Delta | Delta % |
| --- | --- | --- | --- | --- |
| Hits | 1594 | 1574 | -20 | -1.25%
| Error Rate | 0.00% | 0.00% | 0% | 0% |
| Mean Response Time | 7.08ms | 6.36ms | -0.72ms | -10.20% |
| Median Response Time | 5.00ms | 4.00ms | -1.00ms | -20.00% |
| 95th Percentile | 9.00ms | 8.00ms | -1.00ms | -11.11% |

Mysql:
| Metric | Baseline | Actual | Delta | Delta % |
| --- | --- | --- | --- | --- |
| Hits | 1600 | 1566 | -34 | -2.12%
| Error Rate | 0.00% | 0.00% | 0% | 0% |
| Mean Response Time | 7.39ms | 7.02ms | -0.37ms | -4.94% |
| Median Response Time | 6.00ms | 6.00ms | 0ms | 0% |
| 95th Percentile | 10.00ms | 10.00ms | 0ms | 0% |

* Add comment on using CTEs for mysql8

* incorporate review comments

Co-authored-by: mattermod <mattermod@users.noreply.github.com>
This commit is contained in:
Agniva De Sarker
2020-01-09 22:04:30 +05:30
committed by GitHub
parent efffb790e4
commit d938d70dcd

View File

@@ -1652,19 +1652,8 @@ func (s SqlChannelStore) PermanentDeleteMembersByUser(userId string) *model.AppE
}
func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string) (map[string]int64, *model.AppError) {
props := make(map[string]interface{})
updateIdQuery := ""
for index, channelId := range channelIds {
if len(updateIdQuery) > 0 {
updateIdQuery += " OR "
}
props["channelId"+strconv.Itoa(index)] = channelId
updateIdQuery += "ChannelId = :channelId" + strconv.Itoa(index)
}
selectIdQuery := strings.Replace(updateIdQuery, "ChannelId", "Id", -1)
keys, props := MapStringsToQueryParams(channelIds, "Channel")
props["UserId"] = userId
var lastPostAtTimes []struct {
Id string
@@ -1672,21 +1661,51 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string)
TotalMsgCount int64
}
selectQuery := "SELECT Id, LastPostAt, TotalMsgCount FROM Channels WHERE (" + selectIdQuery + ")"
query := `SELECT Id, LastPostAt, TotalMsgCount FROM Channels WHERE Id IN ` + keys
// TODO: use a CTE for mysql too when version 8 becomes the minimum supported version.
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
query = `WITH c AS ( ` + query + `),
updated AS (
UPDATE
ChannelMembers cm
SET
MentionCount = 0,
MsgCount = greatest(cm.MsgCount, c.TotalMsgCount),
LastViewedAt = greatest(cm.LastViewedAt, c.LastPostAt),
LastUpdateAt = greatest(cm.LastViewedAt, c.LastPostAt)
FROM c
WHERE cm.UserId = :UserId
AND c.Id=cm.ChannelId
)
SELECT Id, LastPostAt FROM c`
}
if _, err := s.GetMaster().Select(&lastPostAtTimes, selectQuery, props); err != nil || len(lastPostAtTimes) <= 0 {
var extra string
_, err := s.GetMaster().Select(&lastPostAtTimes, query, props)
if err != nil || len(lastPostAtTimes) == 0 {
status := http.StatusInternalServerError
var extra string
if err == nil {
status = http.StatusBadRequest
extra = "No channels found"
} else {
extra = err.Error()
}
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAt", "store.sql_channel.update_last_viewed_at.app_error", nil, "channel_ids="+strings.Join(channelIds, ",")+", user_id="+userId+", "+extra, status)
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAt",
"store.sql_channel.update_last_viewed_at.app_error",
nil,
"channel_ids="+strings.Join(channelIds, ",")+", user_id="+userId+", "+extra,
status)
}
times := map[string]int64{}
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
for _, t := range lastPostAtTimes {
times[t.Id] = t.LastPostAt
}
return times, nil
}
msgCountQuery := ""
lastViewedQuery := ""
for index, t := range lastPostAtTimes {
@@ -1701,33 +1720,16 @@ func (s SqlChannelStore) UpdateLastViewedAt(channelIds []string, userId string)
props["channelId"+strconv.Itoa(index)] = t.Id
}
var updateQuery string
if s.DriverName() == model.DATABASE_DRIVER_POSTGRES {
updateQuery = `UPDATE
ChannelMembers
SET
MentionCount = 0,
MsgCount = CAST(CASE ChannelId ` + msgCountQuery + ` END AS BIGINT),
LastViewedAt = CAST(CASE ChannelId ` + lastViewedQuery + ` END AS BIGINT),
LastUpdateAt = CAST(CASE ChannelId ` + lastViewedQuery + ` END AS BIGINT)
WHERE
UserId = :UserId
AND (` + updateIdQuery + `)`
} else if s.DriverName() == model.DATABASE_DRIVER_MYSQL {
updateQuery = `UPDATE
updateQuery := `UPDATE
ChannelMembers
SET
MentionCount = 0,
MsgCount = CASE ChannelId ` + msgCountQuery + ` END,
LastViewedAt = CASE ChannelId ` + lastViewedQuery + ` END,
LastUpdateAt = CASE ChannelId ` + lastViewedQuery + ` END
LastUpdateAt = LastViewedAt
WHERE
UserId = :UserId
AND (` + updateIdQuery + `)`
}
props["UserId"] = userId
AND ChannelId IN ` + keys
if _, err := s.GetMaster().Exec(updateQuery, props); err != nil {
return nil, model.NewAppError("SqlChannelStore.UpdateLastViewedAt", "store.sql_channel.update_last_viewed_at.app_error", nil, "channel_ids="+strings.Join(channelIds, ",")+", user_id="+userId+", "+err.Error(), http.StatusInternalServerError)