PAYATHON 2026

使用 SQL 检测大型数据库中的重复付款

支付老李

结论

可以用 SQL 的 GROUP BY、HAVING 和窗口函数筛查疑似重复付款。可以先按”供应商 + 金额 + 付款日期”分组,找出出现两次及以上的记录,再加入发票号、币种、银行账号、公司主体、付款状态等条件,逐步减少误报。

金额和日期相同只能说明付款记录可疑,不能直接认定为重复付款。分期付款、批量结算、周期性费用,以及同一天开具的多张同额发票,都可能属于正常业务。

第一步:确认可用字段

付款表最好包含以下字段:

  • payment_id:付款记录唯一标识
  • supplier_id:供应商标识
  • invoice_number:发票号
  • payment_date:付款日期
  • amount:付款金额
  • currency_code:币种
  • bank_account:收款账户
  • company_id:付款公司或法人主体
  • payment_status:付款状态
  • created_at:记录创建时间

实际字段名通常会有所不同,需要替换为数据库中的真实名称。

判断供应商时,优先使用 supplier_id,不要只依赖供应商名称。名称可能有大小写、空格、简称或拼写上的差异。如果只能使用名称,可以先做标准化处理,但仍无法完全避免误判。

基础查询:查找同一天的同额付款

下面的查询按供应商、日期、金额和币种分组,找出出现两次及以上的组合:

SELECT
    supplier_id,
    payment_date,
    amount,
    currency_code,
    COUNT(*) AS payment_count,
    MIN(payment_id) AS first_payment_id,
    MAX(payment_id) AS last_payment_id
FROM payments
WHERE payment_date >= '2026-01-01'
  AND payment_date <  '2026-04-01'
  AND payment_status = 'PAID'
GROUP BY
    supplier_id,
    payment_date,
    amount,
    currency_code
HAVING COUNT(*) >= 2
ORDER BY
    payment_count DESC,
    supplier_id,
    payment_date;

时间范围采用左闭右开的写法:

payment_date >= '2026-01-01'
AND payment_date <  '2026-04-01'

这种写法同样适用于包含具体时间的字段,不会漏掉季度最后一天晚间产生的记录。

如果只想查出现三次及以上的付款,可以把条件改为:

HAVING COUNT(*) >= 3

查看每笔疑似重复付款的明细

汇总结果只能指出哪些组合存在疑点。实际核查时,通常还要查看完整记录。可以先在子查询中找出重复组合,再连接回付款表:

WITH duplicate_groups AS (
    SELECT
        supplier_id,
        payment_date,
        amount,
        currency_code,
        COUNT(*) AS payment_count
    FROM payments
    WHERE payment_date >= '2026-01-01'
      AND payment_date <  '2026-04-01'
      AND payment_status = 'PAID'
    GROUP BY
        supplier_id,
        payment_date,
        amount,
        currency_code
    HAVING COUNT(*) >= 2
)
SELECT
    p.*,
    d.payment_count
FROM payments AS p
JOIN duplicate_groups AS d
  ON p.supplier_id = d.supplier_id
 AND p.payment_date = d.payment_date
 AND p.amount = d.amount
 AND p.currency_code = d.currency_code
WHERE p.payment_date >= '2026-01-01'
  AND p.payment_date <  '2026-04-01'
  AND p.payment_status = 'PAID'
ORDER BY
    p.supplier_id,
    p.payment_date,
    p.amount,
    p.payment_id;

如果数据库不支持 WITH 公用表表达式,可以将 duplicate_groups 改写为普通派生表。

有发票号时,优先按发票识别

如果发票号的数据质量可靠,检查同一供应商、同一发票号是否被支付多次,通常比检查同日同额付款更容易找到真正的重复记录:

SELECT
    supplier_id,
    invoice_number,
    currency_code,
    COUNT(*) AS payment_count,
    SUM(amount) AS total_paid,
    MIN(payment_date) AS first_payment_date,
    MAX(payment_date) AS last_payment_date
FROM payments
WHERE payment_date >= '2026-01-01'
  AND payment_date <  '2026-04-01'
  AND payment_status = 'PAID'
  AND invoice_number IS NOT NULL
GROUP BY
    supplier_id,
    invoice_number,
    currency_code
HAVING COUNT(*) >= 2
ORDER BY
    payment_count DESC,
    supplier_id,
    invoice_number;

发票号中经常混有空格、连字符,大小写也可能不一致。如果数据库允许,可以先做有限的标准化:

UPPER(REPLACE(TRIM(invoice_number), '-', ''))

例如,将标准化后的结果用作分组字段:

SELECT
    supplier_id,
    UPPER(REPLACE(TRIM(invoice_number), '-', '')) AS normalized_invoice_number,
    COUNT(*) AS payment_count
FROM payments
WHERE payment_date >= '2026-01-01'
  AND payment_date <  '2026-04-01'
  AND invoice_number IS NOT NULL
GROUP BY
    supplier_id,
    UPPER(REPLACE(TRIM(invoice_number), '-', ''))
HAVING COUNT(*) >= 2;

不同数据库对 TRIM、REPLACE 和大小写转换函数的语法支持可能略有区别,需要按实际数据库调整。

检测相隔数日的同额付款

重复付款不一定发生在同一天。可以使用窗口函数,将每笔付款与同一供应商此前的一笔同额付款进行比较:

WITH ordered_payments AS (
    SELECT
        payment_id,
        supplier_id,
        payment_date,
        amount,
        currency_code,
        invoice_number,
        LAG(payment_id) OVER (
            PARTITION BY supplier_id, amount, currency_code
            ORDER BY payment_date, payment_id
        ) AS previous_payment_id,
        LAG(payment_date) OVER (
            PARTITION BY supplier_id, amount, currency_code
            ORDER BY payment_date, payment_id
        ) AS previous_payment_date
    FROM payments
    WHERE payment_date >= '2026-01-01'
      AND payment_date <  '2026-04-01'
      AND payment_status = 'PAID'
)
SELECT *
FROM ordered_payments
WHERE previous_payment_id IS NOT NULL
  AND payment_date <= previous_payment_date + INTERVAL '7' DAY
ORDER BY
    supplier_id,
    amount,
    payment_date;

这个示例用于查找七天内向同一供应商支付的同币种、同金额款项。各数据库的日期加法语法不同:

  • PostgreSQL 常用 previous_payment_date + INTERVAL '7 days'
  • SQL Server 常用 DATEADD(day, 7, previous_payment_date)
  • MySQL 常用 DATE_ADD(previous_payment_date, INTERVAL 7 DAY)
  • Oracle 可使用 previous_payment_date + 7

如果不清楚底层数据库的类型,可以先通过 Excel 插件确认数据源名称,或咨询数据库管理员。

为疑似记录建立风险等级

在大型数据库中,直接列出所有同额付款往往会返回大量记录。可以按证据强弱设置风险等级:

  • 高风险:供应商、发票号、金额和币种均相同,而且存在多个已完成付款。
  • 中风险:供应商、金额和币种相同,付款日期相隔较近,但发票号不同或缺失。
  • 低风险:只有供应商和金额相同,付款间隔较长。
  • 需要排除:已撤销、失败、退款、冲销或测试状态的记录。

如果数据库支持 CASE,可以生成简单的风险标签:

SELECT
    supplier_id,
    invoice_number,
    amount,
    currency_code,
    COUNT(*) AS payment_count,
    CASE
        WHEN invoice_number IS NOT NULL AND COUNT(*) >= 3 THEN 'HIGH'
        WHEN invoice_number IS NOT NULL AND COUNT(*) = 2 THEN 'HIGH'
        WHEN COUNT(*) >= 3 THEN 'MEDIUM'
        ELSE 'REVIEW'
    END AS risk_level
FROM payments
WHERE payment_date >= '2026-01-01'
  AND payment_date <  '2026-04-01'
  AND payment_status = 'PAID'
GROUP BY
    supplier_id,
    invoice_number,
    amount,
    currency_code
HAVING COUNT(*) >= 2;

这只是一套初始分类规则,最终仍需业务人员确认。

常见误报

以下情况虽然看起来像重复付款,但可能属于正常业务:

  • 同一供应商在同一天提交了多张同额发票。
  • 一张发票按合同约定分期支付。
  • 房租、订阅费、服务费等周期性付款采用固定金额。
  • 付款失败或撤销后被重新发起。
  • 原付款与退款记录金额相同,但交易方向相反。
  • 不同公司主体向同一供应商付款。
  • 金额相同,但使用了不同币种。
  • 数据库同时保存付款指令和银行实际交易,使同一笔业务产生两条技术记录。
  • 发票号被重复使用,或供应商的发票编号规则本身不唯一。

因此,查询时最好加入交易方向、公司主体、币种、状态和记录类型等字段。

大型数据库的性能注意事项

不要一开始就在整个历史数据库上执行复杂的窗口查询。可以先选取一个月或一个季度的数据,验证结果后再扩大时间范围。

如果常用的过滤和分组字段建有索引,查询通常会更快。可以请数据库管理员评估以下索引组合是否合适:

(payment_date, payment_status, supplier_id)

或者:

(supplier_id, invoice_number, currency_code, payment_date)

索引能否生效取决于数据库类型、数据分布和现有执行计划,不能只看查询文本来判断。如果只有只读权限,应交由管理员评估,不要盲目要求添加大量索引。

还应避免直接对时间过滤字段使用函数,例如:

WHERE YEAR(payment_date) = 2026

这种写法在部分数据库中会影响索引的使用。通常更适合使用明确的范围条件:

WHERE payment_date >= '2026-01-01'
  AND payment_date <  '2027-01-01'

建议的实施顺序

先选取一个较短的时间段,运行按”供应商 + 日期 + 金额 + 币种”分组的基础查询,并将明细导入 Excel 进行人工复核。确认字段含义后,再依次加入:

  1. 付款状态和交易方向;
  2. 发票号;
  3. 公司主体和收款账户;
  4. 相邻付款的时间窗口;
  5. 对退款、冲销和失败记录的排除规则;
  6. 高、中、低风险分类。

经过几轮人工复核,可以将确认有效的条件固化为定期查询。可靠的重复付款检测通常需要一组逐步收紧、可以解释并方便复核的规则,而不是依赖一条适用于所有情况的 SQL。

备注:内容仅供参考。