PAYATHON 2026

按 UserID 分组查询不存在 Refund 的交易记录

支付小周

结论

可以查出尚未退款的 Purchase,但不能靠排序或 GROUP BY 来判断。更直接的做法是使用 NOT EXISTS:逐条检查 Purchase 是否存在 OrderNumber 相同的 Refund,并返回没有匹配记录的交易。

SELECT p.*
FROM Transactions AS p
WHERE p.Type = 'Purchase'
  AND NOT EXISTS (
      SELECT 1
      FROM Transactions AS r
      WHERE r.Type = 'Refund'
        AND r.OrderNumber = p.OrderNumber
  );

对于示例数据,查询结果为:

TransactionID | UserID | Type     | OrderNumber | Amount
3             | 234    | Purchase | 101         | 100
4             | 345    | Purchase | 102         | 100

订单 100 已有 Refund,所以不会再次进入退款范围。

为什么不建议用排序和分组

ORDER BY 只能改变结果的显示顺序,无法判断关联记录是否存在。

GROUP BY 主要用于数据聚合。它虽然可以结合条件聚合完成筛选,但这里要解决的问题是:“是否存在另一条符合条件的记录?” NOT EXISTS 更准确地表达了这一含义。

至于”按 UserID 分组”,需要先明确具体需求:

  • 如果只是想让结果按用户排列,应使用 ORDER BY UserID。
  • 如果要汇总每个用户的金额,或者每个用户只生成一条汇总记录,才需要使用 GROUP BY UserID。

按用户排列待退款交易

如果后续程序需要按用户依次处理,可以在查询末尾添加排序:

SELECT p.*
FROM Transactions AS p
WHERE p.Type = 'Purchase'
  AND NOT EXISTS (
      SELECT 1
      FROM Transactions AS r
      WHERE r.Type = 'Refund'
        AND r.OrderNumber = p.OrderNumber
  )
ORDER BY p.UserID, p.OrderNumber;

查询仍会保留每条原始 Purchase,只是将同一用户的记录排在一起。

按用户汇总退款金额

如果退款任务只需要每个用户的待退款总额,可以先排除已退款订单,再按用户分组:

SELECT
    p.UserID,
    COUNT(*) AS RefundCount,
    SUM(p.Amount) AS RefundAmount
FROM Transactions AS p
WHERE p.Type = 'Purchase'
  AND NOT EXISTS (
      SELECT 1
      FROM Transactions AS r
      WHERE r.Type = 'Refund'
        AND r.OrderNumber = p.OrderNumber
  )
GROUP BY p.UserID
ORDER BY p.UserID;

汇总后,单笔交易的 TransactionID 和 OrderNumber 将不再保留。如果创建 Refund 时必须关联具体订单,应先查询明细,不能只使用汇总结果。

OrderNumber 是否需要结合 UserID

上面的写法假设 OrderNumber 在整张表中全局唯一。如果不同用户可能使用相同的订单号,匹配时还必须包含 UserID:

SELECT p.*
FROM Transactions AS p
WHERE p.Type = 'Purchase'
  AND NOT EXISTS (
      SELECT 1
      FROM Transactions AS r
      WHERE r.Type = 'Refund'
        AND r.UserID = p.UserID
        AND r.OrderNumber = p.OrderNumber
  )
ORDER BY p.UserID, p.OrderNumber;

如果系统允许一个订单分多次退款,只判断 Refund 是否存在就不够了。此时应比较累计退款金额和购买金额,例如:

SELECT p.*
FROM Transactions AS p
WHERE p.Type = 'Purchase'
  AND p.Amount > COALESCE(
      (
          SELECT SUM(r.Amount)
          FROM Transactions AS r
          WHERE r.Type = 'Refund'
            AND r.UserID = p.UserID
            AND r.OrderNumber = p.OrderNumber
      ),
      0
  );

这段查询的前提是 Refund 的 Amount 以正数保存。如果退款金额以负数保存,需要相应调整计算方式。

还要防止并发重复退款

仅靠”先查询,再插入”无法彻底避免重复退款。两个任务可能同时查到某个订单尚未退款,随后各自插入一条 Refund。

如果业务规则规定每个订单最多只能有一条 Refund,应在数据库层添加唯一约束。具体语法取决于所用数据库。例如,支持部分唯一索引的数据库可以采用类似设计:

CREATE UNIQUE INDEX ux_transactions_refund_order
ON Transactions (UserID, OrderNumber)
WHERE Type = 'Refund';

查询和创建退款也最好放在同一个事务中,并妥善处理唯一约束冲突。即使多个任务同时运行,数据库仍能阻止同一订单产生重复 Refund。

还可以为匹配字段建立索引,以提高查询效率:

CREATE INDEX ix_transactions_type_user_order
ON Transactions (Type, UserID, OrderNumber);

匹配条件和约束方式需要根据实际使用的数据库、订单号的唯一范围,以及系统是否允许部分退款来确定。

备注:内容仅供参考。