"""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')