MM-45956: Optimize FileInfo stats query (#22603)

* MM-45956: Optimize FileInfo stats query

We Denormalize Post.ChannelId on FileInfo.ChannelId

```release-note
The file info stats query is now optimized by denormalizing the channelID column into the table itself. This will speed up the query to get the file count for a channel on clicking the RHS.

Migration times:
On a MySQL 8.0.31 DB with
1405 rows in FileInfo and 11M posts, it took around 0.3s

On a Postgres 12.14 DB with
1731 rows in FileInfo and 11M posts, it took around 0.27s
```

https://mattermost.atlassian.net/browse/MM-45956
Этот коммит содержится в:
Agniva De Sarker
2023-03-23 22:14:04 +05:30
коммит произвёл GitHub
родитель 1edbde8aa3
Коммит 56b18ca7bf
25 изменённых файлов: 266 добавлений и 138 удалений

Просмотреть файл

@@ -2265,7 +2265,7 @@ func (s SqlChannelStore) GetFileCount(channelId string) (int64, error) {
FileInfo
WHERE
FileInfo.DeleteAt = 0
AND FileInfo.PostId IN (SELECT id FROM Posts WHERE ChannelId = ?)`,
AND FileInfo.ChannelId = ?`,
channelId)
if err != nil {
return 0, errors.Wrapf(err, "failed to count files with channelId=%s", channelId)

Просмотреть файл

@@ -88,6 +88,7 @@ func newSqlFileInfoStore(sqlStore *SqlStore, metrics einterfaces.MetricsInterfac
"FileInfo.Id",
"FileInfo.CreatorId",
"FileInfo.PostId",
"COALESCE(FileInfo.ChannelId, '') AS ChannelId",
"FileInfo.CreateAt",
"FileInfo.UpdateAt",
"FileInfo.DeleteAt",
@@ -118,10 +119,10 @@ func (fs SqlFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
query := `
INSERT INTO FileInfo
(Id, CreatorId, PostId, CreateAt, UpdateAt, DeleteAt, Path, ThumbnailPath, PreviewPath,
(Id, CreatorId, PostId, ChannelId, CreateAt, UpdateAt, DeleteAt, Path, ThumbnailPath, PreviewPath,
Name, Extension, Size, MimeType, Width, Height, HasPreviewImage, MiniPreview, Content, RemoteId)
VALUES
(:Id, :CreatorId, :PostId, :CreateAt, :UpdateAt, :DeleteAt, :Path, :ThumbnailPath, :PreviewPath,
(:Id, :CreatorId, :PostId, :ChannelId, :CreateAt, :UpdateAt, :DeleteAt, :Path, :ThumbnailPath, :PreviewPath,
:Name, :Extension, :Size, :MimeType, :Width, :Height, :HasPreviewImage, :MiniPreview, :Content, :RemoteId)
`
@@ -133,9 +134,8 @@ func (fs SqlFileInfoStore) Save(info *model.FileInfo) (*model.FileInfo, error) {
func (fs SqlFileInfoStore) GetByIds(ids []string) ([]*model.FileInfo, error) {
query := fs.getQueryBuilder().
Select(append(fs.queryFields, "COALESCE(P.ChannelId, '') as ChannelId")...).
Select(fs.queryFields...).
From("FileInfo").
LeftJoin("Posts as P ON FileInfo.PostId=P.Id").
Where(sq.Eq{"FileInfo.Id": ids}).
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
OrderBy("FileInfo.CreateAt DESC")
@@ -166,6 +166,8 @@ func (fs SqlFileInfoStore) Upsert(info *model.FileInfo) (*model.FileInfo, error)
return nil, err
}
// PostID and ChannelID are deliberately ignored
// from the list of fields to keep those two immutable.
queryString, args, err := fs.getQueryBuilder().
Update("FileInfo").
SetMap(map[string]any{
@@ -261,8 +263,7 @@ func (fs SqlFileInfoStore) GetWithOptions(page, perPage int, opt *model.GetFileI
From("FileInfo")
if len(opt.ChannelIds) > 0 {
query = query.Join("Posts ON FileInfo.PostId = Posts.Id").
Where(sq.Eq{"Posts.ChannelId": opt.ChannelIds})
query = query.Where(sq.Eq{"FileInfo.ChannelId": opt.ChannelIds})
}
if len(opt.UserIds) > 0 {
@@ -388,10 +389,11 @@ func (fs SqlFileInfoStore) GetForUser(userId string) ([]*model.FileInfo, error)
return infos, nil
}
func (fs SqlFileInfoStore) AttachToPost(fileId, postId, creatorId string) error {
func (fs SqlFileInfoStore) AttachToPost(fileId, postId, channelId, creatorId string) error {
query := fs.getQueryBuilder().
Update("FileInfo").
Set("PostId", postId).
Set("ChannelId", channelId).
Where(sq.And{
sq.Eq{"Id": fileId},
sq.Eq{"PostId": ""},
@@ -506,10 +508,9 @@ func (fs SqlFileInfoStore) Search(paramsList []*model.SearchParams, userId, team
return nil, err
}
query := fs.getQueryBuilder().
Select(append(fs.queryFields, "Coalesce(P.ChannelId, '') AS ChannelId")...).
Select(fs.queryFields...).
From("FileInfo").
LeftJoin("Posts as P ON FileInfo.PostId=P.Id").
LeftJoin("Channels as C ON C.Id=P.ChannelId").
LeftJoin("Channels as C ON C.Id=FileInfo.ChannelId").
LeftJoin("ChannelMembers as CM ON C.Id=CM.ChannelId").
Where(sq.Eq{"FileInfo.DeleteAt": 0}).
OrderBy("FileInfo.CreateAt DESC").
@@ -715,9 +716,8 @@ func (fs SqlFileInfoStore) CountAll() (int64, error) {
func (fs SqlFileInfoStore) GetFilesBatchForIndexing(startTime int64, startFileID string, limit int) ([]*model.FileForIndexing, error) {
files := []*model.FileForIndexing{}
sql, args, _ := fs.getQueryBuilder().
Select(append(fs.queryFields, "Coalesce(p.ChannelId, '') AS ChannelId")...).
Select(fs.queryFields...).
From("FileInfo").
LeftJoin("Posts AS p ON FileInfo.PostId = p.Id").
Where(sq.Or{
sq.Gt{"FileInfo.CreateAt": startTime},
sq.And{

Просмотреть файл

@@ -134,6 +134,15 @@ func checkChannelsPostsIntegrity(ss *SqlStore) model.IntegrityCheckResult {
})
}
func checkChannelsFileInfoIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Channels",
parentIdAttr: "ChannelId",
childName: "FileInfo",
childIdAttr: "Id",
})
}
func checkCommandsCommandWebhooksIntegrity(ss *SqlStore) model.IntegrityCheckResult {
return checkParentChildIntegrity(ss, relationalCheckConfig{
parentName: "Commands",
@@ -458,6 +467,7 @@ func checkChannelsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckRes
results <- checkChannelsIncomingWebhooksIntegrity(ss)
results <- checkChannelsOutgoingWebhooksIntegrity(ss)
results <- checkChannelsPostsIntegrity(ss)
results <- checkChannelsFileInfoIntegrity(ss)
}
func checkCommandsIntegrity(ss *SqlStore, results chan<- model.IntegrityCheckResult) {

Просмотреть файл

@@ -115,7 +115,7 @@ func createEmoji(ss store.Store, userId string) *model.Emoji {
return emoji
}
func createFileInfo(ss store.Store, postId, userId string) *model.FileInfo {
func createFileInfo(ss store.Store, postId, channelId, userId string) *model.FileInfo {
m := model.FileInfo{}
m.PostId = postId
m.CreatorId = userId
@@ -623,7 +623,7 @@ func TestCheckPostsFileInfoIntegrity(t *testing.T) {
t.Run("should generate a report with one record", func(t *testing.T) {
postId := model.NewId()
info := createFileInfo(ss, postId, model.NewId())
info := createFileInfo(ss, postId, model.NewId(), model.NewId())
result := checkPostsFileInfoIntegrity(store)
require.NoError(t, result.Err)
data := result.Data.(model.RelationalIntegrityCheckData)
@@ -1226,7 +1226,7 @@ func TestCheckUsersFileInfoIntegrity(t *testing.T) {
t.Run("should generate a report with one record", func(t *testing.T) {
user := createUser(ss)
userId := user.Id
info := createFileInfo(ss, model.NewId(), userId)
info := createFileInfo(ss, model.NewId(), model.NewId(), userId)
dbmap.Exec(`DELETE FROM Users WHERE Id=?`, user.Id)
result := checkUsersFileInfoIntegrity(store)
require.NoError(t, result.Err)