Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
PERF: improve performance of unread queries
Figuring out what unread topics a user has is a very expensive operation over time. Users can easily accumulate 10s of thousands of tracking state rows (1 for every topic they ever visit) When figuring out what a user has that is unread we need to join the tracking state records to the topic table. This can very quickly lead to cases where you need to scan through the entire topic table. This commit optimises it so we always keep track of the "first" date a user has unread topics. Then we can easily filter out all earlier topics from the join. We use pg functions, instead of nested queries here to assist the planner.
- Loading branch information
1 parent
6eb6c25
commit 29fac1a
Showing
12 changed files
with
227 additions
and
39 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
module Jobs | ||
class UpdateFirstTopicUnreadAt < Jobs::Scheduled | ||
every 1.day | ||
|
||
def execute(args) | ||
UserStat.update_first_topic_unread_at! | ||
end | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
class AddUnreadTrackingColumns < ActiveRecord::Migration | ||
def up | ||
add_column :user_stats, :first_topic_unread_at, :datetime, null: false, default: "epoch" | ||
add_column :topics, :last_unread_at, :datetime, null: false, default: "epoch" | ||
|
||
execute <<SQL | ||
UPDATE topics SET last_unread_at = ( | ||
SELECT MAX(created_at) | ||
FROM posts | ||
WHERE topics.id = posts.topic_id | ||
) | ||
SQL | ||
|
||
execute <<SQL | ||
UPDATE user_stats SET first_topic_unread_at = COALESCE(( | ||
SELECT MIN(last_unread_at) FROM topics | ||
JOIN users u ON u.id = user_stats.user_id | ||
JOIN topic_users tu ON tu.user_id = user_stats.user_id AND topics.id = tu.topic_id | ||
WHERE notification_level > 1 AND last_read_post_number < CASE WHEN moderator OR admin | ||
THEN topics.highest_staff_post_number | ||
ELSE topics.highest_post_number | ||
END | ||
AND topics.deleted_at IS NULL AND topics.archetype <> 'private_message' | ||
), 'epoch') | ||
SQL | ||
|
||
add_index :topics, [:last_unread_at] | ||
|
||
# we need this function for performance reasons | ||
execute <<SQL | ||
CREATE FUNCTION first_unread_topic_for(user_id int) | ||
RETURNS timestamp AS | ||
$$ | ||
SELECT COALESCE(first_topic_unread_at, 'epoch'::timestamp) | ||
FROM users u | ||
JOIN user_stats ON user_id = u.id | ||
WHERE u.id = $1 | ||
$$ | ||
LANGUAGE SQL STABLE | ||
SQL | ||
end | ||
|
||
def down | ||
execute "DROP FUNCTION first_unread_topic_for(int)" | ||
remove_column :user_stats, :first_topic_unread_at | ||
remove_column :topics, :last_unread_at | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
This is a bit iffy. Is there no better way to do it? At the very least a constant would make this a lot more readable.
USER_ID_PLACEHOLDER
?