Files
yunke/yunke-server/migrations/versions/e6f7a8b9c0d1_message_channel_room_time_index.py
T
浅念 9d3cc8e76e fix(messages): 聊天记录大对话超时——DESC首屏+keyset游标+群聊直达索引
用户报:聊天记录多时「聊天记录加载失败:请求失败 (ECONNABORTED)」(2026-09-07)。
服务器 EXPLAIN ANALYZE 实测两级根因:
- 单聊 1 万条:ASC 末页 OFFSET 10486 逐条回表 2515ms(DESC LIMIT 15ms,167 倍);
- 群聊 16,290 条:查询刻意不带 owner(chat_group 去重后 owner 错位),现有
  (channel,owner,room,timestamp) 索引被 owner 挡住,退化到 room 单列索引+
  filter+filesort——count 4.7s/首页 5.1s/末页 10.7s;真凶群 53132846289@chatroom
  40,863 条,三段串行 ~40s 必撞前端 axios 15s 超时。

修法(不动共享 MySQL 配置,沿用既往修复模式):
1. 迁移 e6f7a8b9c0d1:message 补 (channel, room_id, timestamp) 索引,群查询
   seek+排序直达+覆盖 count;模型侧双声明防 autogenerate 反 DROP。
2. /api/messages 加 order=asc|desc(默认 asc 不变)与 before_ts/before_id
   keyset 游标(成对,单传 422)——排序列 (timestamp,msg_id) 全序,平手按
   msg_id 决胜,游标页恒 O(page_size)。
3. 前端 ChatMain/ChatTimeline 首屏改「order=desc 取最新 50 反转」单请求
   (原 page1 拿 total+深 OFFSET 末页两请求),「加载更多」改游标上翻;
   hasMoreOlder=已加载<总数。SessionPane 搜索等 order=asc 默认路径不受影响。

本地实测(37,532 条群):desc 首页 0.39s、游标上翻 0.24s,浏览器真机
渲染+上翻 150 条无错。测试 8 增(desc/游标/平手边界/422/窗口叠加/
前端流程不变式),全量 601 绿。

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-09-07 16:59:04 +08:00

67 lines
2.4 KiB
Python

"""message 补 (channel, room_id, timestamp) 索引(群聊消息窗超时修复)
Revision ID: e6f7a8b9c0d1
Revises: d9e0f1a2b3c4
Create Date: 2026-09-07
内容
----
群聊消息窗(/api/messages?channel=&room_id=)**刻意不带 owner**——chat_group
按 room_id 去重后表的 owner 被最后写入的员工覆盖,与消息实际归属错位,带上
会查空(2026-08-22 实测)。于是 ix_message_channel_owner_room_time 的
owner 前缀用不上,优化器退化到 room 单列索引(ix_message_room)取全行再
filter channel + filesort:
- 16,290 条群: count 4.7s / 首页 5.1s / ASC 末页 10.7s(EXPLAIN ANALYZE
实测,Sort row IDs 全量 16k 行、128MB buffer pool 下逐行回表磁盘随机读);
- 真凶对话 53132846289@chatroom 40,863 条(count+首页+末页串行 ~40s),
前端 axios 15s 超时 → 聊天记录加载失败 ECONNABORTED(2026-09-07 用户报)。
本迁移补 (channel, room_id, timestamp) 三列索引:群查询与单聊同享
seek + 排序直达 + 覆盖 count。模型侧 models.py 同步双声明,防 alembic
autogenerate 反 DROP。窄列 BTREE,~94 万行在线建索引预计几十秒
(参照 c53f90b1e6a4 实测 86 万行 16s)。
配套:同日 /api/messages 加 order=desc + before_ts/before_id 游标参数,
聊天窗首屏「DESC 取最新一页 + 反转」、上翻游标,避开 ASC 深 OFFSET
逐行回表(单聊 1 万条末页实测 2.5s → DESC LIMIT 15ms)。
幂等:``sa.inspect`` 先查同名索引,存在即跳过。SQLite/MySQL 通用。
"""
from collections.abc import Sequence
import sqlalchemy as sa
from alembic import op
# revision identifiers, used by Alembic.
revision: str = 'e6f7a8b9c0d1'
down_revision: str | Sequence[str] | None = 'd9e0f1a2b3c4'
branch_labels: str | Sequence[str] | None = None
depends_on: str | Sequence[str] | None = None
_INDEX_NAME = 'ix_message_channel_room_time'
def _has_index(table: str, index: str) -> bool:
insp = sa.inspect(op.get_bind())
return index in {i['name'] for i in insp.get_indexes(table)}
def upgrade() -> None:
"""Upgrade schema."""
if _has_index('message', _INDEX_NAME):
return # 已建过(或先手工建过),幂等跳过
op.create_index(
_INDEX_NAME,
'message',
['channel', 'room_id', 'timestamp'],
)
def downgrade() -> None:
"""Downgrade schema."""
if not _has_index('message', _INDEX_NAME):
return
op.drop_index(_INDEX_NAME, table_name='message')