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
Этот коммит содержится в:
коммит произвёл
GitHub
родитель
1edbde8aa3
Коммит
56b18ca7bf
@@ -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)
|
||||
|
||||
Ссылка в новой задаче
Block a user