perf: IoT 卡 30M 行分页查询优化(P95 17.9s → <500ms)
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 7m6s
- 新增 is_standalone 物化列 + 触发器自动维护(迁移 056) - 并行查询拆分:多店铺 IN 查询拆为 per-shop goroutine 并行 Index Scan - 两阶段延迟 Join:深度分页(page≥50)走覆盖索引 Index Only Scan 取 ID 再回表 - COUNT 缓存:per-shop 并行 COUNT + Redis 30 分钟 TTL - 索引优化:删除有害全局索引、新增 partial composite indexes(迁移 057/058) - ICCID 模糊搜索路径隔离:trigram GIN 索引走独立查询路径 - 慢查询阈值从 100ms 调整为 500ms - 新增 30M 测试数据种子脚本和 benchmark 工具
This commit is contained in:
5
migrations/000000_create_legacy_tables.down.sql
Normal file
5
migrations/000000_create_legacy_tables.down.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- 回滚:删除遗留表(逆序删除,先删关联表)
|
||||
DROP TABLE IF EXISTS tb_role_permission;
|
||||
DROP TABLE IF EXISTS tb_role;
|
||||
DROP TABLE IF EXISTS tb_permission;
|
||||
DROP TABLE IF EXISTS tb_account;
|
||||
98
migrations/000000_create_legacy_tables.up.sql
Normal file
98
migrations/000000_create_legacy_tables.up.sql
Normal file
@@ -0,0 +1,98 @@
|
||||
-- 创建遗留表(原由 GORM AutoMigrate 自动创建,迁移文件中缺失)
|
||||
-- 这 4 张表是项目最早期通过 AutoMigrate 创建的,后续迁移 000001-000010 依赖它们的存在
|
||||
-- 本迁移还原 AutoMigrate 创建时的原始结构(不含后续 ALTER 添加的字段)
|
||||
|
||||
-- ============================================================
|
||||
-- 1. 账号表(tb_account)
|
||||
-- 注意:此处包含 parent_id 字段,该字段在 000002 迁移中被 DROP
|
||||
-- 注意:enterprise_id 由 000002 添加,is_primary 由 000010 添加,此处不包含
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS tb_account (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
creator BIGINT NOT NULL,
|
||||
updater BIGINT NOT NULL,
|
||||
username VARCHAR(50) NOT NULL,
|
||||
phone VARCHAR(20) NOT NULL,
|
||||
password VARCHAR(255) NOT NULL,
|
||||
user_type BIGINT NOT NULL,
|
||||
shop_id BIGINT,
|
||||
parent_id BIGINT,
|
||||
status BIGINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- 索引(与 GORM AutoMigrate 生成的一致)
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_account_username ON tb_account(username) WHERE deleted_at IS NULL;
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_account_phone ON tb_account(phone) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_account_user_type ON tb_account(user_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_account_shop_id ON tb_account(shop_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_account_deleted_at ON tb_account(deleted_at);
|
||||
|
||||
-- ============================================================
|
||||
-- 2. 权限表(tb_permission)
|
||||
-- 注意:platform 由 000003 添加,available_for_role_types 由 000009 添加,此处不包含
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS tb_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
creator BIGINT NOT NULL,
|
||||
updater BIGINT NOT NULL,
|
||||
perm_name VARCHAR(50) NOT NULL,
|
||||
perm_code VARCHAR(100) NOT NULL,
|
||||
perm_type BIGINT NOT NULL,
|
||||
url VARCHAR(255),
|
||||
parent_id BIGINT,
|
||||
sort BIGINT NOT NULL DEFAULT 0,
|
||||
status BIGINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_permission_code ON tb_permission(perm_code) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_permission_perm_type ON tb_permission(perm_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_permission_parent_id ON tb_permission(parent_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_permission_deleted_at ON tb_permission(deleted_at);
|
||||
|
||||
-- ============================================================
|
||||
-- 3. 角色表(tb_role)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS tb_role (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
creator BIGINT NOT NULL,
|
||||
updater BIGINT NOT NULL,
|
||||
role_name VARCHAR(50) NOT NULL,
|
||||
role_desc VARCHAR(255),
|
||||
role_type BIGINT NOT NULL,
|
||||
status BIGINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_role_role_type ON tb_role(role_type);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_role_deleted_at ON tb_role(deleted_at);
|
||||
|
||||
-- ============================================================
|
||||
-- 4. 角色权限关联表(tb_role_permission)
|
||||
-- ============================================================
|
||||
CREATE TABLE IF NOT EXISTS tb_role_permission (
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP WITH TIME ZONE,
|
||||
updated_at TIMESTAMP WITH TIME ZONE,
|
||||
deleted_at TIMESTAMP WITH TIME ZONE,
|
||||
creator BIGINT NOT NULL,
|
||||
updater BIGINT NOT NULL,
|
||||
role_id BIGINT NOT NULL,
|
||||
perm_id BIGINT NOT NULL,
|
||||
status BIGINT NOT NULL DEFAULT 1
|
||||
);
|
||||
|
||||
-- 索引
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_role_permission_role_id ON tb_role_permission(role_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_role_permission_perm_id ON tb_role_permission(perm_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_role_permission_unique ON tb_role_permission(role_id, perm_id) WHERE deleted_at IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_tb_role_permission_deleted_at ON tb_role_permission(deleted_at);
|
||||
@@ -1,5 +1,7 @@
|
||||
-- 创建企业卡授权表
|
||||
CREATE TABLE IF NOT EXISTS tb_enterprise_card_authorization (
|
||||
-- 重建企业卡授权表(替换 000010 中的旧版结构,字段从 iot_card_id 改为 card_id,移除 shop_id/status,新增 authorizer_type/revoked_by/revoked_at/remark)
|
||||
DROP TABLE IF EXISTS tb_enterprise_card_authorization;
|
||||
|
||||
CREATE TABLE tb_enterprise_card_authorization (
|
||||
-- 基础字段
|
||||
id BIGSERIAL PRIMARY KEY,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
|
||||
9
migrations/000056_add_is_standalone_to_iot_card.down.sql
Normal file
9
migrations/000056_add_is_standalone_to_iot_card.down.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
-- 回滚:删除 is_standalone 列和相关触发器
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_device_sim_binding_soft_delete ON tb_device_sim_binding;
|
||||
DROP TRIGGER IF EXISTS trg_device_sim_binding_standalone ON tb_device_sim_binding;
|
||||
DROP FUNCTION IF EXISTS fn_update_card_standalone_on_soft_delete();
|
||||
DROP FUNCTION IF EXISTS fn_update_card_standalone_on_bind();
|
||||
DROP INDEX IF EXISTS idx_iot_card_standalone_shop_status_created;
|
||||
DROP INDEX IF EXISTS idx_iot_card_standalone_shop_created;
|
||||
ALTER TABLE tb_iot_card DROP COLUMN IF EXISTS is_standalone;
|
||||
79
migrations/000056_add_is_standalone_to_iot_card.up.sql
Normal file
79
migrations/000056_add_is_standalone_to_iot_card.up.sql
Normal file
@@ -0,0 +1,79 @@
|
||||
-- 添加 is_standalone 物化列,消除 ListStandalone 查询中的 NOT EXISTS 子查询
|
||||
-- 30M 行表上 NOT EXISTS 子查询导致 9s+ 延迟,物化为布尔列后降至 <500ms
|
||||
|
||||
-- 1. 添加列(默认 true,因为大多数卡未绑定设备)
|
||||
ALTER TABLE tb_iot_card ADD COLUMN IF NOT EXISTS is_standalone BOOLEAN NOT NULL DEFAULT true;
|
||||
|
||||
COMMENT ON COLUMN tb_iot_card.is_standalone IS '是否为独立卡(未绑定设备) true=独立卡 false=已绑定设备,由触发器自动维护';
|
||||
|
||||
-- 2. 回填数据:将已绑定设备的卡标记为 false
|
||||
UPDATE tb_iot_card SET is_standalone = false
|
||||
WHERE id IN (
|
||||
SELECT DISTINCT iot_card_id FROM tb_device_sim_binding
|
||||
WHERE bind_status = 1 AND deleted_at IS NULL
|
||||
);
|
||||
|
||||
-- 3. 创建部分索引(仅包含独立卡 + 未删除的行)
|
||||
CREATE INDEX IF NOT EXISTS idx_iot_card_standalone_shop_created
|
||||
ON tb_iot_card (shop_id, created_at DESC)
|
||||
WHERE deleted_at IS NULL AND is_standalone = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_iot_card_standalone_shop_status_created
|
||||
ON tb_iot_card (shop_id, status, created_at DESC)
|
||||
WHERE deleted_at IS NULL AND is_standalone = true;
|
||||
|
||||
-- 4. 创建触发器函数:绑定时设置 is_standalone = false
|
||||
CREATE OR REPLACE FUNCTION fn_update_card_standalone_on_bind()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
-- INSERT 或 bind_status 变为 1(绑定)
|
||||
IF (TG_OP = 'INSERT' AND NEW.bind_status = 1) OR
|
||||
(TG_OP = 'UPDATE' AND NEW.bind_status = 1 AND (OLD.bind_status IS DISTINCT FROM 1)) THEN
|
||||
UPDATE tb_iot_card SET is_standalone = false WHERE id = NEW.iot_card_id;
|
||||
END IF;
|
||||
|
||||
-- bind_status 从 1 变为其他值(解绑),检查是否还有其他活跃绑定
|
||||
IF TG_OP = 'UPDATE' AND OLD.bind_status = 1 AND NEW.bind_status != 1 THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM tb_device_sim_binding
|
||||
WHERE iot_card_id = NEW.iot_card_id AND bind_status = 1
|
||||
AND deleted_at IS NULL AND id != NEW.id
|
||||
) THEN
|
||||
UPDATE tb_iot_card SET is_standalone = true WHERE id = NEW.iot_card_id;
|
||||
END IF;
|
||||
END IF;
|
||||
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
-- 5. 绑定表的触发器
|
||||
CREATE TRIGGER trg_device_sim_binding_standalone
|
||||
AFTER INSERT OR UPDATE OF bind_status ON tb_device_sim_binding
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION fn_update_card_standalone_on_bind();
|
||||
|
||||
-- 6. 软删除触发器:绑定记录被软删除时恢复独立状态
|
||||
CREATE OR REPLACE FUNCTION fn_update_card_standalone_on_soft_delete()
|
||||
RETURNS TRIGGER AS $$
|
||||
BEGIN
|
||||
IF OLD.deleted_at IS NULL AND NEW.deleted_at IS NOT NULL AND OLD.bind_status = 1 THEN
|
||||
IF NOT EXISTS (
|
||||
SELECT 1 FROM tb_device_sim_binding
|
||||
WHERE iot_card_id = NEW.iot_card_id AND bind_status = 1
|
||||
AND deleted_at IS NULL AND id != NEW.id
|
||||
) THEN
|
||||
UPDATE tb_iot_card SET is_standalone = true WHERE id = NEW.iot_card_id;
|
||||
END IF;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE TRIGGER trg_device_sim_binding_soft_delete
|
||||
AFTER UPDATE OF deleted_at ON tb_device_sim_binding
|
||||
FOR EACH ROW
|
||||
EXECUTE FUNCTION fn_update_card_standalone_on_soft_delete();
|
||||
|
||||
-- 7. 更新统计信息
|
||||
ANALYZE tb_iot_card;
|
||||
8
migrations/000057_optimize_iot_card_indexes.down.sql
Normal file
8
migrations/000057_optimize_iot_card_indexes.down.sql
Normal file
@@ -0,0 +1,8 @@
|
||||
-- 回滚:恢复全局索引,删除 carrier 复合索引
|
||||
DROP INDEX IF EXISTS idx_iot_card_standalone_shop_carrier_created;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_iot_card_global_created_at
|
||||
ON tb_iot_card (created_at DESC)
|
||||
WHERE deleted_at IS NULL;
|
||||
|
||||
ANALYZE tb_iot_card;
|
||||
14
migrations/000057_optimize_iot_card_indexes.up.sql
Normal file
14
migrations/000057_optimize_iot_card_indexes.up.sql
Normal file
@@ -0,0 +1,14 @@
|
||||
-- 优化 tb_iot_card 索引:修复 PG 优化器在 shop_id IN 场景下选错执行计划的问题
|
||||
-- 问题:idx_iot_card_global_created_at 导致优化器选择全表扫描+Filter,而不是更高效的 partial 索引
|
||||
-- 30M 行场景下 status+carrier_id 过滤查询从 4.5s 降至 <200ms
|
||||
|
||||
-- 1. 删除全局 created_at 索引(诱导优化器走错计划的元凶)
|
||||
DROP INDEX IF EXISTS idx_iot_card_global_created_at;
|
||||
|
||||
-- 2. 新建 carrier_id 复合 partial 索引(覆盖运营商+排序场景)
|
||||
CREATE INDEX IF NOT EXISTS idx_iot_card_standalone_shop_carrier_created
|
||||
ON tb_iot_card (shop_id, carrier_id, created_at DESC)
|
||||
WHERE deleted_at IS NULL AND is_standalone = true;
|
||||
|
||||
-- 3. 更新统计信息
|
||||
ANALYZE tb_iot_card;
|
||||
@@ -0,0 +1,4 @@
|
||||
-- 回滚:删除深度分页覆盖索引
|
||||
DROP INDEX IF EXISTS idx_iot_card_standalone_shop_created_id;
|
||||
|
||||
ANALYZE tb_iot_card;
|
||||
@@ -0,0 +1,15 @@
|
||||
-- 新增覆盖索引用于深度分页优化(两阶段延迟 Join)
|
||||
-- 问题:深度分页(page >= 50)需要 OFFSET 数千行,SELECT * 读取大量宽行数据(~2KB/行)
|
||||
-- 方案:Phase 1 仅扫描覆盖索引获取 ID,Phase 2 用 ID 批量回表取完整数据
|
||||
-- 预期:page 500 从 5.6s → <500ms
|
||||
--
|
||||
-- 生产环境建议手动执行带 CONCURRENTLY 的版本避免锁表:
|
||||
-- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_iot_card_standalone_shop_created_id
|
||||
-- ON tb_iot_card (shop_id, created_at DESC, id)
|
||||
-- WHERE deleted_at IS NULL AND is_standalone = true;
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_iot_card_standalone_shop_created_id
|
||||
ON tb_iot_card (shop_id, created_at DESC, id)
|
||||
WHERE deleted_at IS NULL AND is_standalone = true;
|
||||
|
||||
ANALYZE tb_iot_card;
|
||||
Reference in New Issue
Block a user