discourse/app/models/flag.rb
Alan Guo Xiang Tan 684569c30e
PERF: Avoid slow preloading in SiteSerializer (#33283)
`SiteSerializer#post_action_types` and `SiteSerializer#topic_flag_types`
both call the `Flag.used_flag_ids` method which executes queries that
becomes very slow on sites with alot of records in either the
`post_actions` or `reviewable_scores` table.

On the `post_actions` table, we execute `SELECT DISTINCT
post_action_type_id FROM post_actions`. On the `reviewable_scores`
table, we execute `SELECT DISTINCT reviewable_scope_type FROM
reviewable_scopes`. The problem with both queries is that it requires
the PG planner to scan through every single row in those tables.

For our use case, all we actually need is to check if a flag is being
referenced by a record in either the `post_actions` or
`reviewable_scores` tables. This commit updates the `Flag.used_flag_ids`
to accept a `flag_ids`
argument and use the argument to check whether the flag ids are
referenced in the `post_action_type_id` or `reviewable_scope_type`
foreign keys.
2025-06-25 15:03:34 +08:00

94 lines
2.7 KiB
Ruby

# frozen_string_literal: true
class Flag < ActiveRecord::Base
# TODO(2025-01-15): krisk remove
self.ignored_columns = ["custom_type"]
DEFAULT_VALID_APPLIES_TO = %w[Post Topic]
MAX_SYSTEM_FLAG_ID = 1000
MAX_NAME_LENGTH = 200
MAX_DESCRIPTION_LENGTH = 1000
scope :enabled, -> { where(enabled: true) }
scope :system, -> { where("id < 1000") }
scope :custom, -> { where("id >= 1000") }
before_save :set_position
before_save :set_name_key
after_commit { reset_flag_settings! if !skip_reset_flag_callback }
attr_accessor :skip_reset_flag_callback
default_scope do
order(:position).where(score_type: false).where.not(id: PostActionType::LIKE_POST_ACTION_ID)
end
def used?
PostAction.exists?(post_action_type_id: self.id) ||
ReviewableScore.exists?(reviewable_score_type: self.id)
end
def self.valid_applies_to_types
Set.new(DEFAULT_VALID_APPLIES_TO | DiscoursePluginRegistry.flag_applies_to_types)
end
def self.reset_flag_settings!
# Flags are cached in Redis for better performance. After the update,
# we need to reload them in all processes.
PostActionType.reload_types
end
def self.used_flag_ids(flag_ids)
sql =
flag_ids
.reduce([]) do |queries, flag_id|
queries << "(SELECT #{flag_id} FROM post_actions WHERE post_action_type_id = #{flag_id} LIMIT 1)"
queries << "(SELECT #{flag_id} FROM reviewable_scores WHERE reviewable_score_type = #{flag_id} LIMIT 1)"
queries
end
.join(" UNION ")
DB.query_single(sql)
end
def system?
self.id.present? && self.id < MAX_SYSTEM_FLAG_ID
end
def applies_to?(type)
self.applies_to.include?(type)
end
private
def reset_flag_settings!
self.class.reset_flag_settings!
end
def set_position
self.position = Flag.maximum(:position).to_i + 1 if !self.position
end
def set_name_key
prefix = self.system? ? "" : "custom_"
self.name_key = "#{prefix}#{self.name.squeeze(" ").gsub(" ", "_").gsub(/[^\w]/, "").downcase}"
end
end
# == Schema Information
#
# Table name: flags
#
# id :bigint not null, primary key
# name :string
# name_key :string
# description :text
# notify_type :boolean default(FALSE), not null
# auto_action_type :boolean default(FALSE), not null
# require_message :boolean default(FALSE), not null
# applies_to :string not null, is an Array
# position :integer not null
# enabled :boolean default(TRUE), not null
# created_at :datetime not null
# updated_at :datetime not null
# score_type :boolean default(FALSE), not null
#