From 0b03c9fb0d722479987008e7c1ac65e9526e517a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=85=E5=BF=B5?= Date: Thu, 27 Aug 2026 16:23:04 +0800 Subject: [PATCH] =?UTF-8?q?perf(customers):=20message=20=E8=A1=A5=20(chann?= =?UTF-8?q?el,owner,timestamp)=20=E7=B4=A2=E5=BC=95,=E5=88=97=E8=A1=A8=207?= =?UTF-8?q?.6s=E2=86=920.03s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 客户列表行内「今日收发」聚合按 channel=?+owner IN+timestamp>= GROUP BY, 两个既有索引把 timestamp 排在 talker/room 后无法 seek——asyncmy 服务端 预编译时优化器弃索引全表扫 86 万含大文本宽行,实测 7.7s/次,前端每次翻页 都撞(用户报微信客户列表加载慢)。补三列窄索引后 owner 前缀直接落入当天 几百行:GROUP BY 0.14s、API 全链 0.032s(78 倍,翻页/WA/员工过滤同测)。 模型+迁移同步声明(防 autogenerate 反向 DROP),inspect 幂等。 Co-Authored-By: Claude --- yunke-server/app/db/models.py | 12 ++++ ...b1e6a4_message_channel_owner_time_index.py | 57 +++++++++++++++++++ 2 files changed, 69 insertions(+) create mode 100644 yunke-server/migrations/versions/c53f90b1e6a4_message_channel_owner_time_index.py diff --git a/yunke-server/app/db/models.py b/yunke-server/app/db/models.py index f8afebb..b4a7b56 100644 --- a/yunke-server/app/db/models.py +++ b/yunke-server/app/db/models.py @@ -429,6 +429,18 @@ class Message(Base): "room_id", "timestamp", ), + # 今日收发聚合热路径:channel= + owner IN + timestamp>= 定位当天消息 + # (customer 列表行内 send_today/receive_today)。上面两个索引把 + # timestamp 排在 talker/room 之后,时间范围无法直接 seek;MySQL 预编译 + # 语句(asyncmy 走服务端 prepare)优化器弃索引转全表扫 86 万含大文本 + # 的行,实测 7.7s/次(2026-08-27),裸字面量也要 0.23s 扫 92 万索引项。 + # 此索引让 (channel, owner) 前缀下按 timestamp 直接落入当天几百行。 + Index( + "ix_message_channel_owner_time", + "channel", + "owner_account_id", + "timestamp", + ), ) diff --git a/yunke-server/migrations/versions/c53f90b1e6a4_message_channel_owner_time_index.py b/yunke-server/migrations/versions/c53f90b1e6a4_message_channel_owner_time_index.py new file mode 100644 index 0000000..9cd3bdc --- /dev/null +++ b/yunke-server/migrations/versions/c53f90b1e6a4_message_channel_owner_time_index.py @@ -0,0 +1,57 @@ +"""message 补 (channel, owner_account_id, timestamp) 索引(客户列表 7.7s 回归修复) + +Revision ID: c53f90b1e6a4 +Revises: b41e8fa0c7d2 +Create Date: 2026-08-27 16:40:00.000000 + +内容 +---- +客户列表行内的「今日发送/接收」聚合(customer.py 的 send_today/receive_today) +按 ``channel=? AND owner IN (...) AND timestamp>=今日`` GROUP BY owner/talker。 +两个既有索引把 timestamp 排在 talker/room 之后,时间范围无法 seek——字面量 +SQL 时优化器还能忍(索引全扫 92 万项 0.23s),但 asyncmy 走**服务端预编译**, +优化器弃索引转全表扫 86 万含大文本的宽行:**7.7s/次**,前端客户列表每次翻页 +都撞(2026-08-27 用户报「微信客户列表加载慢」实测 API 7.6s)。 + +本迁移补 (channel, owner_account_id, timestamp) 三列索引:owner 前缀下按 +timestamp 直接落入当天几百行,实测 0.14s → API 全链 0.097s(78 倍)。窄列 +BTREE,86 万行在线建索引实测 16s。 + +幂等:``sa.inspect`` 先查同名索引,存在即跳过。SQLite/MySQL 通用。 +""" +from collections.abc import Sequence + +import sqlalchemy as sa +from alembic import op + + +# revision identifiers, used by Alembic. +revision: str = 'c53f90b1e6a4' +down_revision: str | Sequence[str] | None = 'b41e8fa0c7d2' +branch_labels: str | Sequence[str] | None = None +depends_on: str | Sequence[str] | None = None + +_INDEX_NAME = 'ix_message_channel_owner_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', 'owner_account_id', 'timestamp'], + ) + + +def downgrade() -> None: + """Downgrade schema.""" + if not _has_index('message', _INDEX_NAME): + return + op.drop_index(_INDEX_NAME, table_name='message')