PostgreSQL CASE 嵌套条件:动态 SQL 条件的优雅实现

📌 前言

在复杂的业务查询中,我们常常需要根据不同的条件选择不同的过滤逻辑。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 分支

📋 最佳实践

✅ 推荐做法

  1. 总是添加 ELSE 子句,避免返回 NULL
  2. 保持分支类型一致,都返回布尔值或都返回相同类型
  3. 将固定条件放在 CASE 外层,提高索引利用率
  4. 使用命名参数,提高可读性和安全性
  5. 添加注释,说明每个分支的业务含义

完整示例

// 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
}

📝 总结

核心优势

  1. 减少代码重复:避免编写多个类似的 SQL
  2. 提高可维护性:所有逻辑集中在一处
  3. 类型安全:配合命名参数防止 SQL 注入
  4. 业务清晰:每个分支明确对应一种业务场景

适用场景

适合使用

  • 多状态筛选
  • 可选过滤条件
  • 不同用户角色的权限过滤
  • 高级搜索功能

不适合使用

  • 条件过于复杂(考虑拆分为多个 SQL)
  • 需要极致性能(考虑预编译多个 SQL)

💡 实战经验:在业务系统中使用 CASE 条件判断后,代码行数减少 60%,新增筛选条件的开发时间从半天缩短到 10 分钟。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值