favorites: merge favorites subtables.

Merge the 100 favorite subtables into a single table.

Previously the favorites table was partitioned by user id into 100
subtables to try to make searching by user id faster. This wasn't really
necessary and probably slower than just making an index on
(favorites.user_id, favorites.id) to satisfy ordfav searches. BTree
indexes are logarithmic so dividing an index by 100 doesn't make it 100
times faster to search; instead it just removes a layer or two from the
tree.

This also adds a uniqueness index on (user_id, post_id) to prevent
duplicate favorites. Previously we had to check for duplicates at the
application layer, which required careful locking to do it correctly.

Finally, this adds an index on favorites.id, which was surprisingly
missing before. This made ordering and deleting favorites by id really
slow because it degraded to a sequential scan.
This commit is contained in:
evazion
2021-10-08 10:22:57 -05:00
parent 73acc16271
commit 340e1008e9
6 changed files with 58 additions and 3422 deletions

View File

@@ -431,8 +431,7 @@ class PostQueryBuilder
favuser = User.find_by_name(username) favuser = User.find_by_name(username)
if favuser.present? && Pundit.policy!(current_user, favuser).can_see_favorites? if favuser.present? && Pundit.policy!(current_user, favuser).can_see_favorites?
favorites = Favorite.from("favorites_#{favuser.id % 100} AS favorites").where(user: favuser) Post.where(id: favuser.favorites.select(:post_id))
Post.where(id: favorites.select(:post_id))
else else
Post.none Post.none
end end
@@ -509,6 +508,8 @@ class PostQueryBuilder
relation = search_order(relation, "created_at_desc") relation = search_order(relation, "created_at_desc")
elsif find_metatag(:order) == "custom" elsif find_metatag(:order) == "custom"
relation = search_order_custom(relation, select_metatags(:id).map(&:value)) relation = search_order_custom(relation, select_metatags(:id).map(&:value))
elsif has_metatag?(:ordfav)
# no-op
else else
relation = search_order(relation, find_metatag(:order)) relation = search_order(relation, find_metatag(:order))
end end

View File

@@ -4,7 +4,7 @@ class Favorite < ApplicationRecord
belongs_to :post belongs_to :post
belongs_to :user belongs_to :user
scope :for_user, ->(user_id) { where("favorites.user_id % 100 = ? AND favorites.user_id = ?", user_id.to_i % 100, user_id) } scope :for_user, ->(user_id) { where(user_id: user_id) }
scope :public_favorites, -> { where(user: User.bit_prefs_match(:enable_private_favorites, false)) } scope :public_favorites, -> { where(user: User.bit_prefs_match(:enable_private_favorites, false)) }
def self.visible(user) def self.visible(user)

View File

@@ -138,7 +138,7 @@ class User < ApplicationRecord
has_many :forum_posts, -> {order("forum_posts.created_at, forum_posts.id")}, :foreign_key => "creator_id" has_many :forum_posts, -> {order("forum_posts.created_at, forum_posts.id")}, :foreign_key => "creator_id"
has_many :user_name_change_requests, -> {order("user_name_change_requests.created_at desc")} has_many :user_name_change_requests, -> {order("user_name_change_requests.created_at desc")}
has_many :favorite_groups, -> {order(name: :asc)}, foreign_key: :creator_id has_many :favorite_groups, -> {order(name: :asc)}, foreign_key: :creator_id
has_many :favorites, ->(rec) {where("user_id % 100 = #{rec.id % 100} and user_id = #{rec.id}").order("id desc")} has_many :favorites
has_many :ip_bans, foreign_key: :creator_id has_many :ip_bans, foreign_key: :creator_id
has_many :tag_aliases, foreign_key: :creator_id has_many :tag_aliases, foreign_key: :creator_id
has_many :tag_implications, foreign_key: :creator_id has_many :tag_implications, foreign_key: :creator_id

View File

@@ -50,10 +50,11 @@ class CreateFavorites < ActiveRecord::Migration[4.2]
end end
def self.down def self.down
drop_table "favorites"
0.upto(TABLE_COUNT - 1) do |i| 0.upto(TABLE_COUNT - 1) do |i|
drop_table "favorites_#{i}" drop_table "favorites_#{i}"
end end
drop_table "favorites"
execute "DROP FUNCTION favorites_insert_trigger"
end end
end end

View File

@@ -0,0 +1,31 @@
require_relative "20100211181944_create_favorites.rb"
class MergeFavoritesTables < ActiveRecord::Migration[6.1]
def up
execute "set statement_timeout = 0"
execute "CREATE TABLE favorites_copy AS SELECT id, user_id, post_id FROM favorites"
revert CreateFavorites
rename_table :favorites_copy, :favorites
add_index :favorites, [:user_id, :post_id], unique: true, if_not_exists: true
add_index :favorites, [:user_id, :id], if_not_exists: true
add_index :favorites, :post_id, if_not_exists: true
change_column_null :favorites, :user_id, false
change_column_null :favorites, :post_id, false
execute "ALTER TABLE favorites ADD PRIMARY KEY (id)"
max_id = Favorite.maximum(:id).to_i
execute "CREATE SEQUENCE IF NOT EXISTS favorites_id_seq START #{max_id+1} OWNED BY favorites.id"
execute "ALTER TABLE favorites ALTER COLUMN id SET DEFAULT nextval('favorites_id_seq')"
end
def down
execute "set statement_timeout = 0"
rename_table :favorites, :favorites_copy
run CreateFavorites
execute "INSERT INTO favorites(id, user_id, post_id) SELECT id, user_id, post_id FROM favorites_copy"
drop_table :favorites_copy
end
end

File diff suppressed because it is too large Load Diff