9d3cc8e76e
用户报:聊天记录多时「聊天记录加载失败:请求失败 (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>
67 lines
2.4 KiB
Python
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')
|