PAYATHON 2026

查询 Database 中仅有免费 Billing Services 的账户

数据阿明

结论

不必为每个免费产品分别维护一个 bool。可以按账户对数据库记录分组,再用 HAVING 同时筛选出符合以下条件的账户:

  • 至少有一个免费服务;
  • 没有任何非免费服务。

假设免费产品的 Product ID 是 999 和 1000,并且 Active、Suspended 状态都需要参与判断,一次查询即可找出目标账户。

推荐查询

SELECT
    c.id
FROM tblclients AS c
INNER JOIN tblhosting AS h
    ON h.userid = c.id
   AND h.domainstatus IN ('Active', 'Suspended')
WHERE c.status = 'Active'
GROUP BY c.id
HAVING
    SUM(CASE WHEN h.packageid IN (999, 1000) THEN 1 ELSE 0 END) > 0
    AND
    SUM(CASE WHEN h.packageid NOT IN (999, 1000) THEN 1 ELSE 0 END) = 0;

这里的两个 HAVING 条件分别是:

免费服务数量 > 0
非免费服务数量 = 0

查询结果如下:

账户中的产品是否返回
仅 999是
仅 1000是
同时有 999、1000是
有 999 和其他产品否
只有付费产品否
没有任何服务否

PHP 示例

原代码使用的 mysql_* API 已被 PHP 移除,可以改用 PDO。免费 Product ID 集中保存在一个数组中,也能避免相同的判断散落在多处。

<?php

require_once 'db.php'; // 假设该文件创建了 PDO 实例 $pdo

$freePackageIds = [999, 1000];
$placeholders = implode(',', array_fill(0, count($freePackageIds), '?'));

$sql = "
    SELECT
        c.id
    FROM tblclients AS c
    INNER JOIN tblhosting AS h
        ON h.userid = c.id
       AND h.domainstatus IN ('Active', 'Suspended')
    WHERE c.status = 'Active'
    GROUP BY c.id
    HAVING
        SUM(
            CASE
                WHEN h.packageid IN ($placeholders) THEN 1
                ELSE 0
            END
        ) > 0
        AND
        SUM(
            CASE
                WHEN h.packageid NOT IN ($placeholders) THEN 1
                ELSE 0
            END
        ) = 0
";

$stmt = $pdo->prepare($sql);
$stmt->execute([
    ...$freePackageIds,
    ...$freePackageIds,
]);

$clientIds = $stmt->fetchAll(PDO::FETCH_COLUMN);

foreach ($clientIds as $clientId) {
    echo "Client ID: {$clientId}\n";
}

免费产品列表在 SQL 中出现了两次,所以执行查询时也要绑定两遍参数。

使用 NOT EXISTS 的写法

如果想让查询直接表达”存在免费服务,但不存在其他服务”,可以改用两个相关子查询:

SELECT c.id
FROM tblclients AS c
WHERE c.status = 'Active'
  AND EXISTS (
      SELECT 1
      FROM tblhosting AS h
      WHERE h.userid = c.id
        AND h.domainstatus IN ('Active', 'Suspended')
        AND h.packageid IN (999, 1000)
  )
  AND NOT EXISTS (
      SELECT 1
      FROM tblhosting AS h
      WHERE h.userid = c.id
        AND h.domainstatus IN ('Active', 'Suspended')
        AND h.packageid NOT IN (999, 1000)
  );

这个版本通常更容易理解,而且账户有多条服务记录时也不会返回重复结果。具体性能取决于数据量、MySQL 版本和索引情况。

原查询中的问题

原查询的状态条件有运算符优先级问题:

WHERE userid = "123"
  AND domainstatus = "Active"
   OR domainstatus = "Suspended"

SQL 实际会按下面的方式解释:

WHERE (userid = "123" AND domainstatus = "Active")
   OR domainstatus = "Suspended"

这样会查出其他账户的所有 Suspended 服务。可以改成:

WHERE userid = ?
  AND domainstatus IN ('Active', 'Suspended')

原查询还只选择了 packageid:

SELECT packageid FROM tblhosting

因此,后续代码不能读取:

$product["id"]

如果需要服务记录的 ID,就要在查询中明确选择该字段:

SELECT id, packageid
FROM tblhosting

注意事项

  • 示例中的 999 和 1000 代表两个不同的免费 Product ID,请替换成数据库里的真实值。
  • 如果 Suspended 服务不应算作仍在使用,请从状态条件中删除它。
  • packageid NOT IN (...) 会把免费列表之外的所有服务都视为付费服务。如果系统里还有测试产品、内部产品或其他免费产品,也要把它们加入分类规则。
  • tblhosting 至少应为 userid 建立索引。数据量较大时,可以考虑包含 userid、domainstatus 和 packageid 的复合索引,再根据 EXPLAIN 的结果进行调整。
  • 先用 SELECT 检查查询结果。如果后续脚本会停用或删除服务,请把查询与变更分开,并记录待处理账户,以免错误地批量修改数据。

备注:内容仅供参考。