×
¥
查看详情

1) 代码分析摘要

  • 功能:在给定店铺、时间窗内汇总订单,输出订单基础信息、商品明细聚合(件数/数量/金额)、用户维度统计(在窗口内的订单数)、以及用户全量取消单数量,按创建时间倒序分页。
  • 现状特征:
    • 多次创建临时表,并在临时表间做二次统计与关联。
    • 大量相关子查询(per-row 子查询)对 order_items、order_status_log、orders 进行重复扫描。
    • 使用 DATE(o.created_at) 及 OR 条件降低索引可用性。
    • 临时表使用 CREATE TEMPORARY TABLE IF NOT EXISTS … AS SELECT,存在“表已存在时不刷新数据”的风险。
    • 未对临时表加索引,连接/EXISTS 子句可能退化为全表扫描。

2) 性能问题诊断

  1. 非SARGable谓词

    • WHERE 子句使用 DATE(o.created_at) BETWEEN …,对列包裹函数,导致无法使用基于 created_at 的范围索引。
    • 条件 (o.shop_id = p_shop_id OR p_shop_id IS NULL) 使优化器难以选择合适索引,常导致低效执行计划。
  2. N+1 型相关子查询

    • 在 tmp_orders 中对每条订单计算:
      • (SELECT SUM(oi.quantity*oi.price) FROM order_items oi WHERE oi.order_id=o.id)
      • (SELECT MAX(log.created_at) FROM order_status_log log WHERE log.order_id=o.id)
    • 在 tmp_user_metric 中对每个用户计算:
      • (SELECT COUNT(1) FROM orders o2 WHERE o2.user_id=t.user_id AND o2.status='CANCELLED') 这些会造成对大表 order_items、order_status_log、orders 的重复访问。
  3. 重复扫描与不必要的计算

    • tmp_orders 已计算 order_amount,但随后又构建 tmp_order_items 对 order_items 再扫一遍;重复工作。
    • COUNT(DISTINCT t.order_id) 在 tmp_orders 中本就是每单一行,DISTINCT 多余。
  4. 临时表管理与索引

    • CREATE TEMPORARY TABLE IF NOT EXISTS … AS SELECT:若会话中表已经存在,则不会刷新数据,存在脏数据风险。
    • 临时表未加索引,后续 JOIN/EXISTS 可能走全表扫描。
  5. 统计口径与可优化点

    • cancel_cnt 为“用户全量取消单数”,应避免对 orders 做 per-user 子查询;宜改为“先取用户集合,再一次性按索引统计”。

3) 优化建议

  • 谓词与语义

    • 将日期过滤改为列上无函数的范围谓词:o.created_at >= DATE(v_start) AND o.created_at < DATE(v_end) + INTERVAL 1 DAY,保持原“按天”语义且可用索引。
    • 将 shop_id 判定改为分支两条 SQL(p_shop_id IS NULL 与非空分支),避免 OR 条件。
  • 去除相关子查询,改为分组一次性聚合+JOIN

    • 对 order_items、order_status_log:仅对“候选订单集合”聚合一次,避免 N+1。
    • 对 cancel_cnt:先取“候选用户集合”,再与 orders 使用 status='CANCELLED' 的索引统计后回连。
  • 临时表与索引

    • 严格使用 DROP TEMPORARY TABLE IF EXISTS + CREATE TEMPORARY TABLE … AS SELECT,避免 IF NOT EXISTS;创建后立即为临时表添加必要索引:
      • tmp_orders:PRIMARY KEY(order_id), KEY(user_id), KEY(created_at)
      • tmp_oi_agg:PRIMARY KEY(order_id)
      • tmp_status_agg:PRIMARY KEY(order_id)
      • tmp_user_metric:PRIMARY KEY(user_id)
      • tmp_cancel_cnt:PRIMARY KEY(user_id)
    • 先建 tmp_orders 并加索引,再据此汇总其他临时表,减少大表扫描范围。
  • 基表索引建议(一次性执行)

    • orders
      • idx_orders_shop_created(shop_id, created_at) 用于“指定店铺+时间窗”
      • idx_orders_created(created_at) 用于“无店铺+时间窗”
      • idx_orders_status_user(status, user_id) 用于按 status='CANCELLED' 聚合用户取消数
    • order_items
      • idx_order_items_order(order_id) 用于按订单聚合
    • order_status_log
      • idx_order_status_log_order_created(order_id, created_at) 用于每订单 MAX(created_at)
  • 其他

    • 去掉 tmp_user_metric 中 COUNT(DISTINCT t.order_id) 的 DISTINCT。
    • 如果业务允许改变统计口径,可进一步“先 LIMIT 再计算”以缩小处理面;但为保持原语义(用户的 order_cnt 基于时间窗口内所有订单),本次重构不前置 LIMIT。

4) 重构代码示例

以下为等价语义的重构版(MySQL 8+),去除了相关子查询,修复谓词,按阶段性聚合,显式索引临时表。

原始关键片段(问题点示例):

  • DATE(o.created_at) BETWEEN DATE(v_start) AND DATE(v_end)
  • (o.shop_id = p_shop_id OR p_shop_id IS NULL)
  • 每行订单对 order_items/order_status_log 做子查询
  • tmp_user_metric 内对每个用户做取消单子查询

优化后完整过程: DELIMITER // CREATE PROCEDURE sp_order_summary( IN p_shop_id BIGINT, IN p_start_date DATETIME, IN p_end_date DATETIME, IN p_limit INT ) BEGIN DECLARE v_start DATE; DECLARE v_end DATE; DECLARE v_limit INT;

SET v_limit = IFNULL(p_limit, 500);
SET v_start = DATE(IFNULL(p_start_date, NOW() - INTERVAL 30 DAY));
SET v_end   = DATE(IFNULL(p_end_date, NOW()));

IF v_end < v_start THEN
    SET v_end = v_start;
END IF;

-- 1) 候选订单集(时间窗 + 可选店铺)
DROP TEMPORARY TABLE IF EXISTS tmp_orders;
IF p_shop_id IS NULL THEN
    CREATE TEMPORARY TABLE tmp_orders AS
    SELECT o.id AS order_id, o.user_id, o.created_at, o.status
    FROM orders o
    WHERE o.created_at >= v_start
      AND o.created_at < v_end + INTERVAL 1 DAY;
ELSE
    CREATE TEMPORARY TABLE tmp_orders AS
    SELECT o.id AS order_id, o.user_id, o.created_at, o.status
    FROM orders o
    WHERE o.shop_id = p_shop_id
      AND o.created_at >= v_start
      AND o.created_at < v_end + INTERVAL 1 DAY;
END IF;

ALTER TABLE tmp_orders
    ADD PRIMARY KEY (order_id),
    ADD KEY idx_tmp_orders_user (user_id),
    ADD KEY idx_tmp_orders_created (created_at);

-- 2) 订单商品聚合(一次性按订单聚合)
DROP TEMPORARY TABLE IF EXISTS tmp_oi_agg;
CREATE TEMPORARY TABLE tmp_oi_agg AS
SELECT
    oi.order_id,
    COUNT(*)                         AS item_count,
    SUM(oi.quantity)                 AS total_qty,
    SUM(oi.quantity * oi.price)      AS total_amount
FROM order_items oi
JOIN tmp_orders t ON t.order_id = oi.order_id
GROUP BY oi.order_id;
ALTER TABLE tmp_oi_agg ADD PRIMARY KEY (order_id);

-- 3) 订单状态日志聚合(一次性取每单最新状态时间)
DROP TEMPORARY TABLE IF EXISTS tmp_status_agg;
CREATE TEMPORARY TABLE tmp_status_agg AS
SELECT
    l.order_id,
    MAX(l.created_at) AS last_status_change
FROM order_status_log l
JOIN tmp_orders t ON t.order_id = l.order_id
GROUP BY l.order_id;
ALTER TABLE tmp_status_agg ADD PRIMARY KEY (order_id);

-- 4) 用户在时间窗内的订单数与GMV(可选,GMV如不需要可省略以进一步降负)
DROP TEMPORARY TABLE IF EXISTS tmp_user_metric;
CREATE TEMPORARY TABLE tmp_user_metric AS
SELECT
    t.user_id,
    COUNT(*) AS order_cnt,
    SUM(COALESCE(i.total_amount,0)) AS gm_value
FROM tmp_orders t
LEFT JOIN tmp_oi_agg i ON i.order_id = t.order_id
GROUP BY t.user_id;
ALTER TABLE tmp_user_metric ADD PRIMARY KEY (user_id);

-- 5) 用户全量取消单数(仅针对候选用户集合统计)
DROP TEMPORARY TABLE IF EXISTS tmp_cancel_cnt;
CREATE TEMPORARY TABLE tmp_cancel_cnt AS
SELECT u.user_id, COUNT(*) AS cancel_cnt
FROM orders o
JOIN tmp_user_metric u ON u.user_id = o.user_id
WHERE o.status = 'CANCELLED'
GROUP BY u.user_id;
ALTER TABLE tmp_cancel_cnt ADD PRIMARY KEY (user_id);

-- 6) 最终结果
SELECT
    t.order_id,
    t.user_id,
    t.created_at,
    t.status,
    COALESCE(i.item_count, 0)      AS item_count,
    COALESCE(i.total_qty, 0)       AS total_qty,
    COALESCE(i.total_amount, 0)    AS order_amount,
    COALESCE(u.order_cnt, 0)       AS order_cnt,
    COALESCE(c.cancel_cnt, 0)      AS cancel_cnt,
    s.last_status_change
FROM tmp_orders t
LEFT JOIN tmp_oi_agg     i ON i.order_id = t.order_id
LEFT JOIN tmp_user_metric u ON u.user_id = t.user_id
LEFT JOIN tmp_cancel_cnt  c ON c.user_id = t.user_id
LEFT JOIN tmp_status_agg  s ON s.order_id = t.order_id
ORDER BY t.created_at DESC
LIMIT v_limit;

-- 7) 清理
DROP TEMPORARY TABLE IF EXISTS tmp_cancel_cnt;
DROP TEMPORARY TABLE IF EXISTS tmp_user_metric;
DROP TEMPORARY TABLE IF EXISTS tmp_status_agg;
DROP TEMPORARY TABLE IF EXISTS tmp_oi_agg;
DROP TEMPORARY TABLE IF EXISTS tmp_orders;

END // DELIMITER ;

一次性索引(在业务低峰期执行):

  • CREATE INDEX idx_orders_shop_created ON orders(shop_id, created_at);
  • CREATE INDEX idx_orders_created ON orders(created_at);
  • CREATE INDEX idx_orders_status_user ON orders(status, user_id);
  • CREATE INDEX idx_order_items_order ON order_items(order_id);
  • CREATE INDEX idx_order_status_log_order_created ON order_status_log(order_id, created_at);

5) 预期效果评估

  • 去除相关子查询:

    • 原:对每个订单扫描 order_items 与 order_status_log;对每个用户扫描 orders。
    • 新:仅对“候选订单/用户集合”各扫描一次聚合,复杂度从 O(N)×(子表) 降为 O(N)。
    • 在订单量较大(10万级别)、每单含多商品/多状态日志的场景,常见 3x~20x 的时延下降。
  • 谓词可索引化:

    • 将 DATE(o.created_at) 替换为范围谓词,结合创建的时间/店铺复合索引,可显著降低 I/O。
    • 移除 OR 条件通过分支执行,命中不同场景下的最优索引。
    • 预期扫描行数显著下降,CPU 与磁盘读取减少。
  • 临时表索引化:

    • tmp_orders、tmp_oi_agg、tmp_status_agg、tmp_user_metric、tmp_cancel_cnt 添加主键/索引,避免后续 JOIN/过滤产生全表扫描。
    • 在 10万~100万行的临时集下,ORDER BY + LIMIT 的回表与连接成本显著降低。
  • 资源节省:

    • I/O:按行估算可减少 50%~90% 的回表与随机读。
    • CPU:聚合与连接由“多次小聚合”变为“单次聚合”,分支谓词减少不必要计算,预计 CPU 开销减少 30%~70%。

注:具体收益需基于你的数据分布、并发与硬件进行 EXPLAIN/EXPLAIN ANALYZE 与基准验证。

6) 实施注意事项

  • 正确性与回归

    • 当前重构保持原语义:用户的 order_cnt/GMV 以时间窗口内订单统计,用户 cancel_cnt 为全量取消单数。
    • 若未来希望进一步提速,可讨论“先选 Top-N 订单再汇总”的口径变化;该优化会改变统计语义,需审慎评估。
  • 会话与临时表

    • 不使用 CREATE TEMPORARY TABLE IF NOT EXISTS … AS SELECT;始终 DROP 后重建,避免同会话重复调用产生脏数据。
    • 临时表大小受 tmp_table_size/max_heap_table_size 影响,超限会落盘,监控以避免意外的磁盘 I/O 放大。
  • 索引上线

    • 在业务低峰期创建基表索引,并对关键 SQL 做 EXPLAIN/EXPLAIN ANALYZE 确认命中。
    • 注意 InnoDB 无“包含列索引”,避免过宽索引;建议以过滤列为主(shop_id, created_at, status, user_id, order_id)。
  • 参数与边界

    • p_start_date/p_end_date 为空时使用默认区间;当 p_end_date < p_start_date 时已做归一处理。
    • LIMIT 默认为 500,可通过 v_limit 控制;极端大 LIMIT 可能造成排序压力,必要时考虑覆盖索引与分页优化方案。
  • 监控与回滚

    • 上线前后记录慢查询日志、执行计划与耗时指标;必要时可预留开关在旧版/新版过程间切换。
    • 建议准备样本数据做 A/B 基准,包含高并发下的响应时间与资源使用曲线。

以上方案遵循 MySQL 最佳实践:去函数化谓词、避免 OR 导致的索引失效、用一次性聚合替代相关子查询、为临时表添加索引与减少重复扫描,通常可获得显著且稳定的性能提升。

代码分析摘要

  • 功能:根据批处理日期和可选商户ID,筛选未结算的支付交易,计算手续费,入账到结算台账,并标记交易为已结算;最后输出当日结算汇总。
  • 当前结构:
    • 使用临时表 #to_settle 预筛选交易。
    • 基于游标逐行计算手续费、插入台账并更新支付交易表。
    • 汇总台账数据输出报表。
  • 性能特征与复杂度:
    • 游标逐行处理(Row-by-Row)导致高上下文切换和函数调用开销。
    • 非SARGable谓词 CONVERT(DATE, t.tx_time) <= @BatchDate 破坏索引使用。
    • NOLOCK 可能导致脏读并引入不一致。
    • EXISTS 检查依赖于明细表的索引质量。
    • 临时表与游标使代码结构复杂、维护成本高。

性能问题诊断

  1. 游标与标量函数逐行调用

    • 每行执行 dbo.fn_calc_fee 和两条DML(INSERT/UPDATE),严重增加 CPU/IO 和锁开销。
    • 标量UDF在旧版兼容级别下不能内联,导致额外的执行开销。
  2. 非SARGable时间过滤

    • CONVERT(DATE, t.tx_time) <= @BatchDate 会导致对 tx_time 的索引失效(扫描而非查找)。
  3. NOLOCK 的数据一致性风险

    • 可能读取未提交或已回滚的数据,尤其在财务场景下不适用。
  4. 临时表与ORDER BY

    • #to_settle 仅用于游标遍历(按时间排序),对业务结果无影响但增加资源消耗。
  5. 并发与重复结算风险

    • 多会话并发运行时,可能出现重复插入台账与重复更新的竞态,需要在更新时获得适当锁或事务保证。
  6. 索引使用不明确

    • payment_tx 缺少针对筛选条件的高效覆盖/过滤索引。
    • payment_tx_detail 对 EXISTS 需要 tx_id 上的索引。
    • settlement_ledger 汇总需要按 settle_date/merchant_id 访问优化。

优化建议

  • 结构性优化(代码简化与集合化处理)

    1. 去除游标与临时表,采用单次集合化“更新+输出”模式:
      • 使用 UPDATE ... OUTPUT 将已更新的 payment_tx 行直接写入 settlement_ledger。
      • 使用 WHERE EXISTS 保持与原逻辑一致(避免明细行重复导致台账重复)。
    2. 将时间过滤改为 SARGable:
      • t.tx_time < DATEADD(day, 1, @BatchDate)
  • UDF调用优化

    1. 保持 UDF 调用但改为集合化一次性计算(CROSS APPLY 派生列)。
    2. 如可行,将 dbo.fn_calc_fee 重写为内联表值函数(ITVF),或启用兼容级别150(SQL Server 2019+)以利用UDF inlining,减少函数调用开销。
  • 并发与一致性

    1. 在更新目标(payment_tx)上使用合适的锁提示(UPDLOCK, ROWLOCK)或在事务中执行,确保行级并发安全,避免重复结算。
    2. 避免 NOLOCK;建议采用默认 READ COMMITTED 或启用 RCSI(只读场景)。
    3. 加入事务与错误处理,保证“更新交易与写入台账”的原子性。
  • 索引优化(示例DDL)

    1. payment_tx:为未结算交易建立过滤覆盖索引
      • CREATE INDEX IX_payment_tx_unsettled ON dbo.payment_tx (tx_time, merchant_id) INCLUDE (id, amount, pay_channel, settle_batch_date) WHERE settled = 0;
      • 如查询更常以 settled 优先过滤:CREATE INDEX IX_payment_tx_settled_time ON dbo.payment_tx (settled, tx_time, merchant_id) INCLUDE (id, amount, pay_channel, settle_batch_date);
    2. payment_tx_detail:确保存在支持 EXISTS 的索引
      • CREATE INDEX IX_payment_tx_detail_txid ON dbo.payment_tx_detail (tx_id);
      • 若 tx_id 唯一,考虑 UNIQUE。
    3. settlement_ledger:支持汇总与筛选
      • CREATE INDEX IX_settlement_ledger_date_merchant ON dbo.settlement_ledger (settle_date, merchant_id) INCLUDE (gross_amount, fee);
  • 参数敏感性

    • 如 @MerchantId 的选择性差异较大,可在主更新语句上使用 OPTION (RECOMPILE) 或 OPTIMIZE FOR (@MerchantId UNKNOWN)(按实际负载评估,非必需)。

重构代码示例

下方提供两种等价的集合化实现。优先推荐“单语句更新+输出”版本(更简洁、原子性好)。

  1. 推荐版:UPDATE + OUTPUT(无游标、无临时表,原子性强)
CREATE OR ALTER PROCEDURE dbo.SettleTransactions
    @BatchDate DATE,
    @MerchantId BIGINT = NULL
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    DECLARE @feeRate DECIMAL(5,4) = 0.005;
    DECLARE @EndOfDay DATETIME = DATEADD(DAY, 1, @BatchDate);

    BEGIN TRAN;

    -- 集合化:更新未结算交易并将结果写入台账
    UPDATE t
    SET t.settled = 1,
        t.settle_batch_date = @BatchDate
    OUTPUT
        inserted.id AS tx_id,
        inserted.merchant_id,
        inserted.amount AS gross_amount,
        f.fee AS fee,
        inserted.amount - f.fee AS net_amount,
        @BatchDate AS settle_date
    INTO dbo.settlement_ledger (tx_id, merchant_id, gross_amount, fee, net_amount, settle_date)
    FROM dbo.payment_tx AS t WITH (UPDLOCK, ROWLOCK)
    CROSS APPLY (SELECT dbo.fn_calc_fee(t.amount, t.pay_channel, @feeRate) AS fee) AS f
    WHERE t.settled = 0
      AND t.tx_time < @EndOfDay
      AND (@MerchantId IS NULL OR t.merchant_id = @MerchantId)
      AND EXISTS (SELECT 1 FROM dbo.payment_tx_detail AS d WHERE d.tx_id = t.id);
      -- 可按需评估 OPTION (RECOMPILE)

    COMMIT TRAN;

    -- 汇总报表
    SELECT
        m.merchant_name,
        COUNT(*) AS tx_cnt,
        SUM(l.gross_amount) AS total_amt,
        SUM(l.fee) AS total_fee
    FROM dbo.settlement_ledger AS l
    JOIN dbo.merchant AS m ON m.id = l.merchant_id
    WHERE l.settle_date = @BatchDate
    GROUP BY m.merchant_name
    ORDER BY total_amt DESC;
END
  1. 备选版:两步法(插入台账 -> 更新交易),便于分步调试
CREATE OR ALTER PROCEDURE dbo.SettleTransactions
    @BatchDate DATE,
    @MerchantId BIGINT = NULL
AS
BEGIN
    SET NOCOUNT ON;
    SET XACT_ABORT ON;

    DECLARE @feeRate DECIMAL(5,4) = 0.005;
    DECLARE @EndOfDay DATETIME = DATEADD(DAY, 1, @BatchDate);

    DECLARE @SettledIds TABLE (id BIGINT PRIMARY KEY);

    BEGIN TRAN;

    INSERT INTO dbo.settlement_ledger (tx_id, merchant_id, gross_amount, fee, net_amount, settle_date)
    OUTPUT inserted.tx_id INTO @SettledIds(id)
    SELECT
        t.id,
        t.merchant_id,
        t.amount,
        f.fee,
        t.amount - f.fee,
        @BatchDate
    FROM dbo.payment_tx AS t
    CROSS APPLY (SELECT dbo.fn_calc_fee(t.amount, t.pay_channel, @feeRate) AS fee) AS f
    WHERE t.settled = 0
      AND t.tx_time < @EndOfDay
      AND (@MerchantId IS NULL OR t.merchant_id = @MerchantId)
      AND EXISTS (SELECT 1 FROM dbo.payment_tx_detail AS d WHERE d.tx_id = t.id);

    UPDATE t
    SET t.settled = 1,
        t.settle_batch_date = @BatchDate
    FROM dbo.payment_tx AS t
    JOIN @SettledIds AS s ON s.id = t.id;

    COMMIT TRAN;

    SELECT
        m.merchant_name,
        COUNT(*) AS tx_cnt,
        SUM(l.gross_amount) AS total_amt,
        SUM(l.fee) AS total_fee
    FROM dbo.settlement_ledger AS l
    JOIN dbo.merchant AS m ON m.id = l.merchant_id
    WHERE l.settle_date = @BatchDate
    GROUP BY m.merchant_name
    ORDER BY total_amt DESC;
END

说明:

  • 均保持原有业务语义:仅结算明细存在的未结算交易。
  • 移除了 NOLOCK 与游标,代码显著简化。
  • 时间过滤改为 SARGable。
  • 事务保证台账插入与交易更新的一致性。

预期效果评估

  • CPU与IO:
    • 去除游标与逐行 DML,改为单次集合化处理。对于 N 条交易,DML与函数调用从 O(N)次的往返开销降为单/双语句批量执行。典型场景可降低 CPU 30%-70%,IO 20%-50%。
  • 锁与并发:
    • 集合化更新减少锁持有时间与锁数量;使用行级更新锁降低阻塞概率。
  • 计划与索引命中:
    • SARGable 时间条件与过滤索引(WHERE settled = 0)可显著提升筛选效率,避免大范围扫描。
  • 报表查询:
    • 按 settle_date 的索引可将汇总的基数控制在当日范围,减少聚合代价。
  • 代码可维护性:
    • 代码行数与结构简化,去除临时表与游标;逻辑更直观,出错面更小。

实施注意事项

  • UDF优化与兼容级别:
    • 若数据库兼容级别 < 150,标量 UDF 可能无法内联,建议评估升级到 150 或将 fn_calc_fee 重写为内联TVF,以进一步提升集合化调用性能。
  • 锁与隔离级别:
    • 不建议使用 NOLOCK。推荐使用默认 READ COMMITTED 或启用 READ_COMMITTED_SNAPSHOT(RCSI)以减少读写阻塞。
    • 在并发结算场景下,保留 UPDLOCK/ROWLOCK 或在事务中以适当隔离级别执行,确保不会重复结算同一交易。
  • 索引维护成本:
    • 新增索引会带来写入维护成本。建议在非高峰时段创建索引,并基于真实数据量进行基准测试,必要时仅保留过滤索引(settled=0),减小体量。
  • 费用计算一致性:
    • 集合化计算与游标逐行计算语义一致,但需确保 fn_calc_fee 对相同输入返回一致值,且无外部副作用。
  • 选项提示:
    • OPTION (RECOMPILE) 可缓解参数嗅探,但会增加编译开销。仅在 @MerchantId 选择性差异较大、执行计划稳定性差时使用。
  • 变更部署:
    • 在灰度环境进行性能基准与回归测试;验证并发下的重复结算与台账一致性;确认报表数据与原版本一致后再全量发布。

1) 代码分析摘要

  • 功能:按日生成用户维度的汇总报表。步骤包括:筛选用户集(tmp_users) → 统计当日会话(tmp_sessions) → 统计当日订单(tmp_orders) → 获取当日最后一次事件(tmp_last_event) → 左连接入报表表。
  • 主要访问模式:
    • users:按创建时间与分群(segment)筛选。
    • sessions:按起始时间的“某天范围”筛选并按用户聚合。
    • orders:按创建时间的“某天范围”筛选并按用户聚合,并带状态条件的聚合过滤。
    • events:按发生时间的“某天范围”筛选,按用户取当日最新一条(DISTINCT ON + ORDER BY occurred_at DESC)。
  • 当前潜在性能问题集中在:
    • 使用函数包裹时间列(date_trunc、date())导致索引无法使用(不可SARGable)。
    • 基表上缺少对“时间范围 + user_id(或 segment)”的复合索引支撑。
    • DISTINCT ON + ORDER BY + random() 不利于索引顺序使用。
    • 临时表用于半连接/连接,但未对 join 键(user_id)建立索引,且统计信息不足。

2) 性能问题诊断

  • 不可利用索引的谓词:
    • sessions:WHERE date_trunc('day', s.start_time) = p_date → 将导致扫描大量行或全表扫描。
    • orders:WHERE date(o.created_at) = p_date → 同上。
    • events:WHERE date(e.occurred_at) = p_date → 同上。
    • users:WHERE date_trunc('day', u.created_at) <= p_date → 同上。
  • 连接/半连接形式与索引匹配:
    • sessions/orders/events 使用 EXISTS/IN 关联 tmp_users,没有利用显式连接优化器的 Hash/Semi-Join 优势;且 tmp_users(user_id) 缺少索引。
  • DISTINCT ON 取最新事件:
    • ORDER BY e.user_id, e.occurred_at DESC, random() 迫使排序包含随机列,基本无法用到“按(user_id, occurred_at DESC)有序”的索引,仅能回退到排序/物化。
  • 统计信息与执行计划稳定性:
    • 临时表创建后未 ANALYZE,优化器对临时表行数与分布估计可能不准,影响连接算法选择与索引选择。

3) 优化建议(以“索引优化”为主,附必要的查询改写确保索引可用)

A. 将日期函数改写为可走索引的范围谓词

  • 用半开区间替代 date_trunc/date:
    • s.start_time >= p_date AND s.start_time < p_date + interval '1 day'
    • o.created_at >= p_date AND o.created_at < p_date + interval '1 day'
    • e.occurred_at >= p_date AND e.occurred_at < p_date + interval '1 day'
  • users 创建时间条件改写:
    • u.created_at < p_date + interval '1 day' 代替 date_trunc('day', u.created_at) <= p_date

B. 基表索引设计(优先级从高到低)

  • sessions(大表、强时间序访问)
    • 方案S1(按日过滤选择性高时优先):CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sessions_start_time_user ON sessions (start_time, user_id) INCLUDE (duration);
      • 优点:高效按日扫描,并就地按 user_id 聚合;INCLUDE 可减少回表(索引仅在可用 index-only 的场景下显著收益)。
    • 方案S2(当 tmp_users 很小、按用户过滤更强时考虑):CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sessions_user_start_time ON sessions (user_id, start_time) INCLUDE (duration);
      • 二选一为主,避免双索引冗余。建议先上线 S1,观察计划与命中率,如 tmp_users 通常很小再评估 S2。
  • orders
    • 首选:CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_created_user ON orders (created_at, user_id) INCLUDE (status, amount);
      • 支撑按日扫描并按用户聚合;INCLUDE 列配合索引仅扫描可减少回表。
    • 不建议为 status='PAID' 单独建部分索引(partial index),因为本查询同时需要“全部订单”和“已支付订单”两类聚合,单个扫描难以同时利用两个不同索引。
  • events
    • 首选:CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_events_user_occ_desc ON events (user_id, occurred_at DESC);
      • 与 DISTINCT ON (user_id) ... ORDER BY user_id, occurred_at DESC 完美匹配,可在允许移除 random() 时实现“每用户取最新”索引扫描。
    • 如果保留 random() 作为并列时间戳的随机打散,索引仍能过滤时间范围与用户,但无法完全避免排序;仍然显著优于无索引。
  • users
    • 若 segment 选择性好:CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_segment_created ON users (segment, created_at);
    • 若常常 segment='all',优先:CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_created ON users (created_at);
    • 二者只保留一种,依据实际查询比例选择,避免冗余。
  • 辅助建议(可选,面向超大时间序表)
    • 如果 sessions/orders/events 物理上基本按时间递增插入,且数据量巨大,可评估 BRIN 索引以节省存储并加速范围过滤(例如:CREATE INDEX ... USING BRIN(start_time))。需确认表物理顺序与块相关性良好。

C. 临时表索引与统计信息

  • 为连接键添加轻量索引(仅在行数较大时执行):
    • CREATE INDEX ON tmp_users(user_id);
    • CREATE UNIQUE INDEX ON tmp_sessions(user_id);
    • CREATE UNIQUE INDEX ON tmp_orders(user_id);
    • CREATE UNIQUE INDEX ON tmp_last_event(user_id);
  • 在创建并填充临时表后执行 ANALYZE,提高优化器估计精度:
    • ANALYZE tmp_users; ANALYZE tmp_sessions; ANALYZE tmp_orders; ANALYZE tmp_last_event;

D. 连接与半连接写法

  • 将 EXISTS/IN 改为显式 JOIN,通常可触发 Hash/Semi-Join 更稳定的计划,并与上述索引更好匹配。

E. 报表目标表(可选)

  • 若需要防止重复插入或便于查询:为 daily_user_report 建立唯一索引
    • CREATE UNIQUE INDEX IF NOT EXISTS ux_daily_user_report ON daily_user_report(date_key, user_id);

4) 重构代码示例

仅展示与索引生效相关的重写(范围谓词、JOIN、临时表索引与ANALYZE);业务逻辑保持不变。

-- 推荐的永久索引(一次性部署,使用 CONCURRENTLY 降低锁影响) CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_created ON users (created_at); -- 或:如果 segment 过滤选择性更强,改为: -- CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_users_segment_created ON users (segment, created_at);

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_sessions_start_time_user ON sessions (start_time, user_id) INCLUDE (duration);

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_orders_created_user ON orders (created_at, user_id) INCLUDE (status, amount);

CREATE INDEX CONCURRENTLY IF NOT EXISTS idx_events_user_occ_desc ON events (user_id, occurred_at DESC);

-- 可选(报表成品表避免重复) -- CREATE UNIQUE INDEX IF NOT EXISTS ux_daily_user_report ON daily_user_report(date_key, user_id);

-- 存储过程(仅展示核心SQL的“前后对比式”重构) CREATE OR REPLACE PROCEDURE sp_daily_user_report(p_date DATE, p_segment TEXT DEFAULT 'all') LANGUAGE plpgsql AS $$ BEGIN -- 1) tmp_users:改写时间谓词为范围,避免对列使用函数;必要时建索引与ANALYZE CREATE TEMP TABLE tmp_users AS SELECT u.id AS user_id, u.created_at, u.segment, u.region FROM users u WHERE u.created_at < p_date + interval '1 day' AND (p_segment = 'all' OR u.segment = p_segment);

CREATE INDEX ON tmp_users(user_id);
ANALYZE tmp_users;

-- 2) tmp_sessions:改为范围谓词 + 显式JOIN
CREATE TEMP TABLE tmp_sessions AS
SELECT s.user_id,
       count(*) AS session_cnt,
       sum(s.duration) AS total_duration
FROM sessions s
JOIN tmp_users tu ON tu.user_id = s.user_id
WHERE s.start_time >= p_date
  AND s.start_time < p_date + interval '1 day'
GROUP BY s.user_id;

CREATE UNIQUE INDEX ON tmp_sessions(user_id);
ANALYZE tmp_sessions;

-- 3) tmp_orders:范围谓词 + JOIN
CREATE TEMP TABLE tmp_orders AS
SELECT o.user_id,
       count(*) FILTER (WHERE o.status='PAID') AS paid_cnt,
       sum(o.amount) FILTER (WHERE o.status='PAID') AS gm_value,
       sum(o.amount) AS order_amt
FROM orders o
JOIN tmp_users tu ON tu.user_id = o.user_id
WHERE o.created_at >= p_date
  AND o.created_at < p_date + interval '1 day'
GROUP BY o.user_id;

CREATE UNIQUE INDEX ON tmp_orders(user_id);
ANALYZE tmp_orders;

-- 4) tmp_last_event:范围谓词 + JOIN
-- 若业务允许放弃随机并列打散,可去掉 random(),显著提升利用 idx_events_user_occ_desc 的能力
CREATE TEMP TABLE tmp_last_event AS
SELECT DISTINCT ON (e.user_id)
    e.user_id, e.event_name, e.occurred_at
FROM events e
JOIN tmp_users tu ON tu.user_id = e.user_id
WHERE e.occurred_at >= p_date
  AND e.occurred_at < p_date + interval '1 day'
ORDER BY e.user_id, e.occurred_at DESC, random();  -- 如可移除 random(),更佳

CREATE UNIQUE INDEX ON tmp_last_event(user_id);
ANALYZE tmp_last_event;

-- 5) 最终装载
INSERT INTO daily_user_report(date_key, user_id, segment, session_cnt, total_duration, paid_orders, gm_value, last_event, last_event_time)
SELECT
    p_date,
    u.user_id,
    u.segment,
    COALESCE(s.session_cnt,0),
    COALESCE(s.total_duration,0),
    COALESCE(o.paid_cnt,0),
    COALESCE(o.gm_value,0),
    le.event_name,
    le.occurred_at
FROM tmp_users u
LEFT JOIN tmp_sessions s ON s.user_id = u.user_id
LEFT JOIN tmp_orders o ON o.user_id = u.user_id
LEFT JOIN tmp_last_event le ON le.user_id = u.user_id;

RAISE NOTICE 'Report loaded for %, segment=%', p_date, p_segment;

DROP TABLE IF EXISTS tmp_last_event;
DROP TABLE IF EXISTS tmp_orders;
DROP TABLE IF EXISTS tmp_sessions;
DROP TABLE IF EXISTS tmp_users;

END; $$;

5) 预期效果评估

  • 范围谓词替代函数谓词:
    • sessions/orders/events/users 均可命中时间或(time,user_id)复合索引,避免全表扫描。
    • 在典型“当日数据仅占总量极小比例”的电商场景中,扫描行数可从“全表/全分区”降至“当日范围”,I/O 与CPU显著下降(数量级级别)。
  • 复合索引与聚合匹配:
    • (start_time, user_id) 与 (created_at, user_id) 索引将使聚合前的过滤高效,且便于按 user_id 分组。
  • DISTINCT ON 最新事件:
    • 移除 random() 时,(user_id, occurred_at DESC) 可实现“索引有序获取每用户首行”,避免排序与海量回表;保留 random() 时仍可利用索引做高效过滤,排序代价也将因输入集显著缩小而降低。
  • 临时表索引与 ANALYZE:
    • 在 tmp_* 行数较大时,能显著降低最终三次 LEFT JOIN 的成本,并使连接算法更稳定。

综合估计:在数据量较大的在线业务中,上述改造通常可带来数量级的查询时间缩短(例如从秒级/十秒级降至百毫秒-秒级,视数据量与硬件而定),并显著降低缓冲命中压力与I/O。

6) 实施注意事项

  • 索引上线方式:
    • 生产环境建议使用 CREATE INDEX CONCURRENTLY,避免长时间阻塞写入;上线后执行 ANALYZE 或等待 autovacuum 收敛统计信息。
  • 索引选择与冗余控制:
    • sessions 上的 S1 与 S2 不建议同时长期保留。先以 S1 为基线,通过 pg_stat_user_indexes 与 EXPLAIN 验证,再决定是否替换为 S2。
  • 观察与回归确认:
    • 使用 EXPLAIN (ANALYZE, BUFFERS) 对关键子查询在大日期样本上比对前后执行计划与耗时。
  • 随机打散的业务确认:
    • 如允许去掉 ORDER BY ... random(),可进一步提升 events 子查询性能;否则保持现状以保证业务语义正确。
  • 临时表索引的条件执行:
    • 若 tmp_* 行数很小(如 < 数万),临时索引的收益可能有限,可按阈值条件化创建,避免不必要的开销。
  • 数据增长策略:
    • 若数据持续高速增长,后续可评估基于时间字段的分区策略(范围分区),与上述索引方案并行使用,进一步缩小扫描范围。此为后续演进方向,非本次必需。

示例详情

解决的问题

让 AI 扮演资深数据库优化顾问,面向电商订单、财务结算、报表统计、用户分析等高频业务,专注提升 SQL 存储过程的执行效率与稳定性。通过标准化的诊断流程,完成从问题定位—成因分析—优化方案—重构示例—收益评估—上线注意事项的全链路交付。在不改变业务逻辑的前提下,帮助团队显著缩短响应时间、降低资源消耗、减少扩容支出,并沉淀可复用的优化规范与最佳实践。用户只需粘贴存储过程代码、说明优化目标与数据库类型,即可获得定制化、可直接落地的优化建议与代码片段。最终实现性能与成本的双向提升,让关键流程跑得更快、更稳、更省钱。

适用用户

数据库管理员(DBA)

快速审计现有存储过程,定位慢点与热点,产出索引与参数调整方案,给出实施与回滚计划,在不影响业务的前提下稳步提速。

后端开发工程师

在功能迭代中一键审查存储过程,获取可直接套用的重写示例与注意事项,减少反复试错与手工调优时间,缩短上线周期。

系统架构师

评估关键链路数据库负载,设定性能目标并获得取舍建议,沉淀统一的优化规范与评审模板,提升跨团队协作效率。

特征总结

一键导入存储过程,自动体检全流程,生成易懂诊断报告与优化清单
秒级定位慢点与资源浪费,识别热点语句和索引机会,给出可落地方案
智能重写查询与流程,自动生成前后对比示例,直观看到性能提升幅度
面向电商、报表与分析等场景,提供场景化优化策略与落地最佳实践
内置风险守护,规避破坏数据完整性的激进调整,附安全替代建议方案
按目标调优响应时间、吞吐与成本,自动权衡取舍,给出最优组合建议
模板化输出结构,便于评审与协作,快速拆分任务并对接上线流程闭环
一键生成索引与参数建议,附实施注意点与回滚指引,降低变更风险
持续评估优化效果,给出预期指标与基准测试方法,形成改进闭环机制
兼容多种数据库方言,自动适配差异,减少跨库迁移与协作的沟通成本

如何使用购买的提示词模板

1. 直接在外部 Chat 应用中使用

将模板生成的提示词复制粘贴到您常用的 Chat 应用(如 ChatGPT、Claude 等),即可直接对话使用,无需额外开发。适合个人快速体验和轻量使用场景。

2. 发布为 API 接口调用

把提示词模板转化为 API,您的程序可任意修改模板参数,通过接口直接调用,轻松实现自动化与批量处理。适合开发者集成与业务系统嵌入。

3. 在 MCP Client 中配置使用

在 MCP client 中配置对应的 server 地址,让您的 AI 应用自动调用提示词模板。适合高级用户和团队协作,让提示词在不同 AI 工具间无缝衔接。

SQL存储过程性能优化专家

5
1
Dec 30, 2025
本提示词专门用于分析和优化SQL存储过程,通过深度解析存储过程代码结构,识别性能瓶颈和优化机会点。系统采用多维度分析方法,包括查询执行计划分析、索引使用评估、代码重构建议等,提供具体的性能优化方案。能够有效提升存储过程的执行效率,降低系统资源消耗,改善代码可维护性,适用于数据库开发、性能调优和系统运维等多种业务场景。
成为会员,解锁全站资源
复制与查看不限次 · 持续更新权益
提示词宝典 · 终身会员

一次支付永久解锁,全站资源与持续更新;商业项目无限次使用

420 +
品类
8200 +
模板数量
17000 +
会员数量