📌 前言
在复杂的业务查询中,我们常常需要根据不同的条件选择不同的过滤逻辑。PostgreSQL 的 CASE 表达式配合命名参数,可以实现优雅的动态条件判断,避免拼接多个 SQL 语句。
本文将深入探讨**“条件中的条件”**写法,即在 WHERE 子句中使用 CASE 表达式实现多分支逻辑判断。
🎯 核心概念:条件中的条件
传统方式 vs CASE 方式
-- ❌ 传统方式:需要拼接多个 SQL
if (orderStatus === '全部') {
sql = "SELECT * FROM orders WHERE end_date IS NULL OR end_date IS NOT NULL"
} else if (orderStatus === '进行中') {
sql = "SELECT * FROM orders WHERE end_date IS NULL"
} else if (orderStatus === '已完成') {
sql = "SELECT * FROM orders WHERE end_date BETWEEN :start AND :end"
}
-- ✅ CASE 方式:一个 SQL 搞定
SELECT * FROM orders
WHERE CASE :orderStatus
WHEN '全部' THEN TRUE
WHEN '进行中' THEN end_date IS NULL
WHEN '已完成' THEN end_date BETWEEN :startDate AND :endDate
ELSE FALSE
END
🔍 实战案例分析
案例背景
业务系统中需要根据订单状态筛选数据:
- 全部:筛选有效时段与查询时段有交集的所有订单
- 进行中:只筛选未结束的订单
- 已完成:只筛选结束日期在查询时段内的订单
完整实现
SELECT
o.id,
o.name,
o.start_date,
o.end_date
FROM "public".orders o
LEFT JOIN "public".branch b ON o.branch_id = b.id
LEFT JOIN "public".department d ON o.department_id = d.id
WHERE
o.deleted_at IS NULL
AND o.status_level IN ('B', 'C')
-- 🔑 核心:根据 orderStatus 参数选择不同的过滤逻辑
AND CASE :orderStatus
WHEN '全部' THEN
-- 使用 OVERLAPS 检测有效时段与查询时段是否有交集
(o.start_date, COALESCE(o.end_date, (:endDate || ' 23:59:59')::timestamp))
OVERLAPS ((:startDate || ' 00:00:00')::timestamp,
(:endDate || ' 23:59:59')::timestamp)
WHEN '进行中' THEN
-- 结束日期为空
o.end_date IS NULL
WHEN '已完成' THEN
-- 结束日期在查询时段内
o.end_date IS NOT NULL
AND CAST(o.end_date AS DATE) >= :startDate::DATE
AND CAST(o.end_date AS DATE) <= :endDate::DATE
ELSE FALSE
END
-- 其他动态条件
AND CASE
WHEN :departmentIds = '' THEN o.department_id IS NOT NULL
ELSE o.department_id = ANY(string_to_array(:departmentIds, ',')::int[])
END
AND CASE
WHEN :branchIds = '' THEN TRUE
ELSE o.branch_id = ANY(string_to_array(:branchIds, ',')::int[])
END
AND b.enable = true
AND d.enable = true;
代码结构拆解
1️⃣ 主条件判断(行 115-129)
AND CASE :orderStatus
WHEN '全部' THEN
(o.start_date, COALESCE(o.end_date, (:endDate || ' 23:59:59')::timestamp))
OVERLAPS ((:startDate || ' 00:00:00')::timestamp,
(:endDate || ' 23:59:59')::timestamp)
WHEN '进行中' THEN
o.end_date IS NULL
WHEN '已完成' THEN
o.end_date IS NOT NULL
AND CAST(o.end_date AS DATE) >= :startDate::DATE
AND CAST(o.end_date AS DATE) <= :endDate::DATE
ELSE FALSE
END
关键点:
CASE :orderStatus:直接对参数进行匹配- 每个
WHEN分支返回布尔值 ELSE FALSE:默认不匹配任何记录
2️⃣ 数组参数的动态处理(行 131-134)
AND CASE
WHEN :departmentIds = '' THEN o.department_id IS NOT NULL
ELSE o.department_id = ANY(string_to_array(:departmentIds, ',')::int[])
END
技巧:
- 传递空字符串表示"不筛选"
- 非空时使用
string_to_array转换为数组
3️⃣ 可选过滤条件(行 136-139)
AND CASE
WHEN :branchIds = '' THEN TRUE
ELSE o.branch_id = ANY(string_to_array(:branchIds, ',')::int[])
END
🎨 CASE 表达式的两种形式
形式1:简单 CASE(值匹配)
-- 语法
CASE test_expression
WHEN value1 THEN result1
WHEN value2 THEN result2
ELSE default_result
END
-- 示例
SELECT
name,
CASE status
WHEN 'active' THEN '活跃'
WHEN 'inactive' THEN '不活跃'
ELSE '未知'
END AS status_text
FROM users;
形式2:搜索 CASE(条件判断)
-- 语法
CASE
WHEN condition1 THEN result1
WHEN condition2 THEN result2
ELSE default_result
END
-- 示例
SELECT
name,
CASE
WHEN age < 18 THEN '未成年'
WHEN age BETWEEN 18 AND 60 THEN '成年'
ELSE '老年'
END AS age_group
FROM users;
在 WHERE 中的应用对比
-- ✅ 简单 CASE:适用于参数值匹配
WHERE CASE :filterType
WHEN 'active' THEN status = 'active'
WHEN 'all' THEN TRUE
ELSE FALSE
END
-- ✅ 搜索 CASE:适用于复杂条件
WHERE CASE
WHEN :minAge IS NULL THEN TRUE
ELSE age >= :minAge
END
💡 高级应用技巧
1. 多参数组合条件
SELECT * FROM orders
WHERE
-- 条件1:日期范围(可选)
CASE
WHEN :startDate IS NULL THEN TRUE
ELSE order_date >= :startDate
END
AND CASE
WHEN :endDate IS NULL THEN TRUE
ELSE order_date <= :endDate
END
-- 条件2:金额范围(可选)
AND CASE
WHEN :minAmount IS NULL THEN TRUE
ELSE amount >= :minAmount
END
-- 条件3:状态(必选)
AND CASE :status
WHEN 'all' THEN TRUE
WHEN 'paid' THEN payment_status = 'paid'
WHEN 'pending' THEN payment_status IN ('pending', 'processing')
ELSE FALSE
END;
2. 嵌套 CASE 实现复杂逻辑
SELECT * FROM orders
WHERE
CASE :filterMode
WHEN 'simple' THEN
-- 简单模式:仅判断结束状态
end_date IS NULL
WHEN 'advanced' THEN
-- 高级模式:嵌套判断
CASE :orderStatus
WHEN '进行中' THEN end_date IS NULL
WHEN '已完成' THEN end_date BETWEEN :start AND :end
ELSE TRUE
END
ELSE TRUE
END;
3. 结合子查询
SELECT * FROM customers
WHERE
CASE :includeRelated
WHEN TRUE THEN
-- 包含相关客户
id IN (
SELECT customer_id FROM related_customers
WHERE relation_type = :relationType
)
ELSE
-- 仅本人
id = :customerId
END;
🚀 性能优化
1. 避免全表扫描
-- ❌ 不推荐:所有分支都可能全表扫描
WHERE CASE :filterType
WHEN 'id' THEN id = :value
WHEN 'name' THEN name LIKE :value || '%'
WHEN 'all' THEN TRUE
END
-- ✅ 推荐:提前排除不必要的数据
WHERE
deleted_at IS NULL -- 先用索引过滤
AND CASE :filterType
WHEN 'id' THEN id = :value
WHEN 'name' THEN name LIKE :value || '%'
WHEN 'all' THEN TRUE
END
2. 索引友好的写法
-- ✅ 保持索引列的可用性
WHERE
-- 使用索引
order_date >= '2025-01-01'
AND order_date < '2025-12-31'
-- 动态条件
AND CASE :statusFilter
WHEN 'paid' THEN status = 'paid'
WHEN 'all' THEN TRUE
ELSE FALSE
END;
-- ❌ 破坏索引
WHERE CASE :dateFilter
WHEN 'thisYear' THEN EXTRACT(YEAR FROM order_date) = 2025
ELSE TRUE
END
3. 使用执行计划分析
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE
deleted_at IS NULL
AND CASE :orderStatus
WHEN '全部' THEN TRUE
WHEN '进行中' THEN end_date IS NULL
ELSE FALSE
END;
⚠️ 注意事项
1. 类型一致性
-- ❌ 错误:THEN 分支返回类型不一致
WHERE CASE :filterType
WHEN 'id' THEN id = 123 -- 返回 boolean
WHEN 'name' THEN 'John' -- 返回 text ❌
ELSE FALSE
END
-- ✅ 正确:所有分支返回相同类型
WHERE CASE :filterType
WHEN 'id' THEN id = 123
WHEN 'name' THEN name = 'John'
ELSE FALSE
END
2. NULL 值处理
-- ⚠️ CASE 返回 NULL 时不会匹配
WHERE CASE :status
WHEN 'active' THEN TRUE
-- 如果 :status 不是 'active',返回 NULL(不是 FALSE)
END
-- ✅ 添加 ELSE 子句
WHERE CASE :status
WHEN 'active' THEN TRUE
ELSE FALSE -- 明确返回 FALSE
END
3. 短路求值
PostgreSQL 的 CASE 是短路求值的:
-- 只会执行匹配的分支
WHERE CASE :mode
WHEN 'expensive' THEN (SELECT COUNT(*) FROM big_table) > 100 -- 耗时查询
WHEN 'simple' THEN TRUE
ELSE FALSE
END
-- 如果 :mode = 'simple',不会执行 expensive 分支
📋 最佳实践
✅ 推荐做法
- 总是添加 ELSE 子句,避免返回 NULL
- 保持分支类型一致,都返回布尔值或都返回相同类型
- 将固定条件放在 CASE 外层,提高索引利用率
- 使用命名参数,提高可读性和安全性
- 添加注释,说明每个分支的业务含义
完整示例
// TypeScript/JavaScript 代码
async function getOrders(params: {
orderStatus: '全部' | '进行中' | '已完成',
startDate: string,
endDate: string,
departmentIds?: number[],
branchIds?: number[]
}) {
const { orderStatus, startDate, endDate, departmentIds = [], branchIds = [] } = params
// 参数转换
const departmentIdsParam = departmentIds.length > 0 ? departmentIds.join(',') : ''
const branchIdsParam = branchIds.length > 0 ? branchIds.join(',') : ''
const sql = `
SELECT
o.id,
o.name,
o.start_date,
o.end_date
FROM "public".orders o
LEFT JOIN "public".branch b ON o.branch_id = b.id
LEFT JOIN "public".department d ON o.department_id = d.id
WHERE
o.deleted_at IS NULL
-- 动态订单状态过滤
AND CASE :orderStatus
WHEN '全部' THEN
(o.start_date, COALESCE(o.end_date, :endDate::timestamp))
OVERLAPS (:startDate::timestamp, :endDate::timestamp)
WHEN '进行中' THEN
o.end_date IS NULL
WHEN '已完成' THEN
o.end_date IS NOT NULL
AND o.end_date::date BETWEEN :startDate::date AND :endDate::date
ELSE FALSE
END
-- 动态部门过滤
AND CASE
WHEN :departmentIds = '' THEN TRUE
ELSE o.department_id = ANY(string_to_array(:departmentIds, ',')::int[])
END
-- 动态分支过滤
AND CASE
WHEN :branchIds = '' THEN TRUE
ELSE o.branch_id = ANY(string_to_array(:branchIds, ',')::int[])
END
AND b.enable = true
AND d.enable = true
`
const result = await knex.raw(sql, {
orderStatus,
startDate,
endDate,
departmentIds: departmentIdsParam,
branchIds: branchIdsParam
})
return result.rows
}
📝 总结
核心优势
- 减少代码重复:避免编写多个类似的 SQL
- 提高可维护性:所有逻辑集中在一处
- 类型安全:配合命名参数防止 SQL 注入
- 业务清晰:每个分支明确对应一种业务场景
适用场景
✅ 适合使用:
- 多状态筛选
- 可选过滤条件
- 不同用户角色的权限过滤
- 高级搜索功能
❌ 不适合使用:
- 条件过于复杂(考虑拆分为多个 SQL)
- 需要极致性能(考虑预编译多个 SQL)
💡 实战经验:在业务系统中使用 CASE 条件判断后,代码行数减少 60%,新增筛选条件的开发时间从半天缩短到 10 分钟。
1963

被折叠的 条评论
为什么被折叠?



