使用 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 进行人工复核。确认字段含义后,再依次加入:
- 付款状态和交易方向;
- 发票号;
- 公司主体和收款账户;
- 相邻付款的时间窗口;
- 对退款、冲销和失败记录的排除规则;
- 高、中、低风险分类。
经过几轮人工复核,可以将确认有效的条件固化为定期查询。可靠的重复付款检测通常需要一组逐步收紧、可以解释并方便复核的规则,而不是依赖一条适用于所有情况的 SQL。
备注:内容仅供参考。
