mirror of
https://github.com/basecamp/once-campfire.git
synced 2026-10-09 00:00:12 +09:00
Find the messages a refresh replaces through an index
Every room visit and every reconnection asks for the messages updated since the page was rendered. That query filtered the room by updated_at and sorted it by created_at, so SQLite walked every message in the room: about 220-360 ms in a room of a million messages. An index on (room_id, updated_at) finds the few updated messages directly. Sorting on +created_at keeps SQLite on that index once messages are also indexed by (room_id, created_at): with both, the planner otherwise walks the room in creation order looking for updated rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JVFo3Lt9T8M5NR7KxVvsZ2
This commit is contained in:
@@ -36,7 +36,10 @@ module Message::Pagination
|
|||||||
scope :page_after, ->(message) { after(message).first_page }
|
scope :page_after, ->(message) { after(message).first_page }
|
||||||
|
|
||||||
scope :page_created_since, ->(time) { where("created_at > ?", time).first_page }
|
scope :page_created_since, ->(time) { where("created_at > ?", time).first_page }
|
||||||
scope :page_updated_since, ->(time) { where("updated_at > ?", time).last_page }
|
# Sorting on +created_at, which no index covers, has SQLite find the few messages updated since
|
||||||
|
# through (room_id, updated_at) and sort just those, instead of walking the whole room by
|
||||||
|
# (room_id, created_at) looking for them.
|
||||||
|
scope :page_updated_since, ->(time) { Page.load(where("updated_at > ?", time).reorder(Arel.sql("+messages.created_at")), :last, PAGE_SIZE) }
|
||||||
end
|
end
|
||||||
|
|
||||||
class_methods do
|
class_methods do
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
class AddRoomIdAndUpdatedAtIndexToMessages < ActiveRecord::Migration[8.2]
|
||||||
|
def change
|
||||||
|
add_index :messages, %i[ room_id updated_at ]
|
||||||
|
end
|
||||||
|
end
|
||||||
Generated
+2
-1
@@ -10,7 +10,7 @@
|
|||||||
#
|
#
|
||||||
# It's strongly recommended that you check this file into your version control system.
|
# It's strongly recommended that you check this file into your version control system.
|
||||||
|
|
||||||
ActiveRecord::Schema[8.2].define(version: 2025_12_12_154340) do
|
ActiveRecord::Schema[8.2].define(version: 2026_10_05_022000) do
|
||||||
create_table "accounts", force: :cascade do |t|
|
create_table "accounts", force: :cascade do |t|
|
||||||
t.datetime "created_at", null: false
|
t.datetime "created_at", null: false
|
||||||
t.text "custom_styles"
|
t.text "custom_styles"
|
||||||
@@ -101,6 +101,7 @@ ActiveRecord::Schema[8.2].define(version: 2025_12_12_154340) do
|
|||||||
t.integer "room_id", null: false
|
t.integer "room_id", null: false
|
||||||
t.datetime "updated_at", null: false
|
t.datetime "updated_at", null: false
|
||||||
t.index ["creator_id"], name: "index_messages_on_creator_id"
|
t.index ["creator_id"], name: "index_messages_on_creator_id"
|
||||||
|
t.index ["room_id", "updated_at"], name: "index_messages_on_room_id_and_updated_at"
|
||||||
t.index ["room_id"], name: "index_messages_on_room_id"
|
t.index ["room_id"], name: "index_messages_on_room_id"
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,31 @@
|
|||||||
|
require "test_helper"
|
||||||
|
|
||||||
|
class Message::PaginationTest < ActiveSupport::TestCase
|
||||||
|
test "messages updated since a time are found through the room and update time index" do
|
||||||
|
assert_match(/USING INDEX index_messages_on_room_id_and_updated_at \(room_id=\? AND updated_at>\?\)/, plan_of_page_updated_since)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "messages updated since a time are the newest page of them, in creation order" do
|
||||||
|
room = rooms(:watercooler)
|
||||||
|
first, second, third = room.messages.ordered.first(3)
|
||||||
|
|
||||||
|
travel 1.minute do
|
||||||
|
[ third, first, second ].each(&:touch)
|
||||||
|
|
||||||
|
stub_const(Message::Pagination, :PAGE_SIZE, 2) do
|
||||||
|
assert_equal [ second, third ], room.messages.page_updated_since(30.seconds.ago).to_a
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
def plan_of_page_updated_since
|
||||||
|
statements = []
|
||||||
|
callback = ->(*, payload) { statements << payload[:sql] if payload[:sql].start_with?(%(SELECT "messages")) }
|
||||||
|
ActiveSupport::Notifications.subscribed(callback, "sql.active_record") do
|
||||||
|
rooms(:watercooler).messages.page_updated_since(1.minute.ago)
|
||||||
|
end
|
||||||
|
|
||||||
|
Message.connection.select_rows("EXPLAIN QUERY PLAN #{statements.sole}").map(&:last).join(" | ")
|
||||||
|
end
|
||||||
|
end
|
||||||
Reference in New Issue
Block a user