查询 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检查查询结果。如果后续脚本会停用或删除服务,请把查询与变更分开,并记录待处理账户,以免错误地批量修改数据。
备注:内容仅供参考。
