PAYATHON 2026

Monthly billing system 查询截至今日未缴费客户

支付小周

结论

两张表应通过 Customers.CID = Payments.CID 关联,再用 NOT EXISTS 查找当前账期内没有付款记录的客户。

不能直接对 Payments.DateTimeRecieved 使用 NOT BETWEEN。欠费判断关注的是某个客户在指定账期内是否完全没有付款记录,而不是该客户是否存在日期落在区间外的付款记录。

下面的查询会找出本月已经到期,但截至今天仍没有本账期付款记录的客户。

查询本月截至今天仍未缴费的客户

以下写法适用于 SQL Server 2012 及以上版本,也处理了 2 月没有 29、30、31 日等情况。如果 C_BillingDay 大于当月总天数,就以当月最后一天作为到期日。

DECLARE @Today date = CONVERT(date, GETDATE());
DECLARE @Tomorrow date = DATEADD(day, 1, @Today);

SELECT
    c.CID,
    c.C_Name,
    c.C_EmailID,
    c.C_MobileNo,
    c.C_BillingDay,
    c.Balance,
    d.PreviousDueDate,
    d.CurrentDueDate
FROM dbo.Customers AS c
CROSS APPLY
(
    SELECT
        CurrentMonthEnd  = EOMONTH(@Today),
        PreviousMonthEnd = EOMONTH(DATEADD(month, -1, @Today))
) AS m
CROSS APPLY
(
    SELECT
        CurrentDueDate =
            DATEFROMPARTS(
                YEAR(@Today),
                MONTH(@Today),
                CASE
                    WHEN c.C_BillingDay > DAY(m.CurrentMonthEnd)
                        THEN DAY(m.CurrentMonthEnd)
                    ELSE c.C_BillingDay
                END
            ),
        PreviousDueDate =
            DATEFROMPARTS(
                YEAR(DATEADD(month, -1, @Today)),
                MONTH(DATEADD(month, -1, @Today)),
                CASE
                    WHEN c.C_BillingDay > DAY(m.PreviousMonthEnd)
                        THEN DAY(m.PreviousMonthEnd)
                    ELSE c.C_BillingDay
                END
            )
) AS d
WHERE c.C_BillingDay BETWEEN 1 AND 31
  -- 只检查本月已经到期的客户
  AND d.CurrentDueDate <= @Today
  -- 当前账期内没有任何付款记录
  AND NOT EXISTS
  (
      SELECT 1
      FROM dbo.Payments AS p
      WHERE p.CID = c.CID
        AND p.DateTimeRecieved >= DATEADD(day, 1, d.PreviousDueDate)
        AND p.DateTimeRecieved < @Tomorrow
  );

假设某客户的 C_BillingDay = 4:

  • 上月 4 日是上一个到期日;
  • 当前账期从上月 5 日开始;
  • 本月 4 日是本次到期日;
  • 如果今天是本月 4 日或更晚,并且该客户从上月 5 日至今没有付款记录,就会出现在查询结果中。

这里用 @Tomorrow 作为右侧开区间,因此可以包含今天全天的付款记录,包括发生在 23:59:59 的付款。

如果要查询”没有在到期日前付款”的客户

如果客户后来补缴,仍要被统计为”未按时付款”,付款截止时间就应限定为本月到期日,而不是今天:

AND NOT EXISTS
(
    SELECT 1
    FROM dbo.Payments AS p
    WHERE p.CID = c.CID
      AND p.DateTimeRecieved >= DATEADD(day, 1, d.PreviousDueDate)
      AND p.DateTimeRecieved < DATEADD(day, 1, d.CurrentDueDate)
)

这两种统计口径有所不同:

  • “截至今天仍未缴费”表示到今天为止没有付款。客户迟交后,不再出现在结果中。
  • “没有按时缴费”表示到期日前没有付款。即使后来补缴,仍属于逾期客户。

原查询为什么得不到预期结果

原查询有几个问题:

SELECT *
FROM Customers, Payments
WHERE Payments.DateTimeRecieved
      NOT BETWEEN GETDATE()
          AND DATEADD(MONTH, -1, GETDATE());

首先,BETWEEN 的起始值应早于结束值。这里的 GETDATE() 晚于一个月前的日期,两个边界写反了。正确顺序至少应该是:

BETWEEN DATEADD(month, -1, GETDATE()) AND GETDATE()

其次,Customers, Payments 之间没有关联条件,会产生笛卡尔积,也就是每个客户都会与每条付款记录组合。至少应明确写出关联条件:

p.CID = c.CID

还有一个更直接的问题:NOT BETWEEN 只能找出日期位于区间外的付款记录。如果某个已经付款的客户还有其他历史付款记录,他仍可能被错误地列为欠费客户。查询真正需要判断的是,该客户在指定账期内是否完全没有付款记录,所以应使用 NOT EXISTS。

更可靠的数据表设计

只靠当前两张表,无法准确确认一笔付款对应哪个月的账单。比如,客户补交了上个月的欠款,而付款日期恰好落在本月账期内,这笔付款就可能被当成本月已经缴费。

更稳妥的做法是增加账单表,并让每条付款记录明确对应某张账单:

CREATE TABLE dbo.Invoices
(
    InvoiceID  int IDENTITY(1,1) PRIMARY KEY,
    CID        int NOT NULL,
    BillingYear int NOT NULL,
    BillingMonth int NOT NULL,
    DueDate    date NOT NULL,
    AmountDue  decimal(18,2) NOT NULL,
    PaidAmount decimal(18,2) NOT NULL DEFAULT 0,
    Status     varchar(20) NOT NULL
);

然后在付款表中增加对应关系:

ALTER TABLE dbo.Payments
ADD InvoiceID int NULL;

欠费查询便可以直接根据账单状态和到期日进行判断:

SELECT
    c.CID,
    c.C_Name,
    i.InvoiceID,
    i.DueDate,
    i.AmountDue,
    i.PaidAmount
FROM dbo.Customers AS c
INNER JOIN dbo.Invoices AS i
    ON i.CID = c.CID
WHERE i.DueDate <= CONVERT(date, GETDATE())
  AND i.PaidAmount < i.AmountDue;

这种设计可以正确处理部分付款、补缴、一次支付多个月账单以及历史欠费。金额字段建议使用 decimal,不要使用 float,以免产生不适合财务计算的精度误差。

备注:内容仅供参考。