在 WHERE 子句中使用可选条件的正确方法

Posted

技术标签:

【中文标题】在 WHERE 子句中使用可选条件的正确方法【英文标题】:Right way to use optional condition in WHERE clause 【发布时间】:2014-09-03 17:02:09 【问题描述】:

我在 SQL 查询的 WHERE 子句中有一个可选条件。如果参数opt_y由用户提供,则检查条件cond2 <> opt_y,否则跳过。另一个条件 (cond1 = x) 保持不变。

以下方法中,哪个更高效 & secure(避免SQL注入)。我正在使用 Django 原始查询管理器来运行我的 SQL(我了解性能取决于具体情况,但如果存在一些明显的缺陷,例如 db 引擎无法优化特定技术或无法有效使用缓存,请突出显示)。当然,如果有更好的方法,请分享。

预期查询

MyTable.objects.raw("""
  SELECT id, RANK() OVER (PARTITION BY name ORDER BY age)
  FROM mytable
  WHERE cond1 = %s AND cond2 <> %s """, [x, opt_y])
)

方法一: CASE 语句

MyTable.objects.raw("""
  SELECT id, RANK() OVER (PARTITION BY name ORDER BY age)
  FROM mytable
  WHERE cond1 = %s AND 
    CASE WHEN %s IS NULL THEN true ELSE cond2 <> %s END""", [x, opt_y, opt_y])
)

方法二:组合字符串

query  = """SELECT id, RANK() OVER (PARTITION BY name ORDER BY age)
            FROM mytable
            WHERE cond1 = %s """
params = [x]
if opt_y:
    q += """AND cond2 <> %s"""
    params.append(opt_y)
MyTable.objects.raw(query, params)

方法三:逻辑运算

MyTable.objects.raw("""
  SELECT id, RANK() OVER (PARTITION BY name ORDER BY age)
  FROM mytable
  WHERE cond1 = %s AND (%s IS NULL OR cond2 <> %s)""", [x, opt_y, opt_y])
)

更新,增加1个方法:

方法 4: 这更像是一种 hack

if not opt_y:
    opt_y = -1 #Or some value that will NEVER appear in a column 

MyTable.objects.raw("""
  SELECT id, RANK() OVER (PARTITION BY name ORDER BY age)
  FROM mytable
  WHERE cond1 = %s AND cond2 <> %s""", [x, opt_y])
)

使用 Django 1.6 和 PostgreSQL 9.3

【问题讨论】:

【参考方案1】:

CASE 或逻辑操作很好,如果您不介意冗长 - 但是,如果您使用服务器端准备好的语句,它们将不会得到优化,因此它们可能导致非最佳计划选择。

如果您使用客户端参数化查询,客户端驱动程序会替换参数,它们会很好。您可以通过查看 PostgreSQL 查询日志来判断您正在使用哪个 - 如果它记录您的语句,例如:

... WHERE $1 = 'fred' AND ...

那么您正在使用服务器端参数绑定。

因此,遗憾的是,将谓词添加到 SQL 字符串并将额外的参数添加到查询参数列表可能是许多应用程序最有效的方法。这是关于 SQL IMO 最可怕的事情之一。


如果您对 PostgreSQL 的核心内容不感兴趣,请立即停止阅读

如果您想知道如何判断特定结构是否被优化,您可以检查 PostgreSQL 的低级查询解析树、重写和查询计划结构。如果您对优化器效果感兴趣,那么查询计划结构就是您想要的。

假设我对是否对表感兴趣:

                                 Table "public.manufacturers"
 Column  |         Type          |                         Modifiers                          
---------+-----------------------+------------------------------------------------------------
 id      | integer               | not null default nextval('manufacturers_id_seq'::regclass)
 name    | character varying(30) | not null
 country | character varying(40) | 
Indexes:
    "manufacturers_pkey" PRIMARY KEY, btree (id)
    "manufacturers_name_key" UNIQUE CONSTRAINT, btree (name)

一个(相当愚蠢的)查询,例如:

select * from manufacturers 
where case when null is null then true else id = id end
order by id;

CASE 是否已优化。我会:

SET debug_print_plan = on;
SET client_min_messages = debug1;
select * from manufacturers 
    where case when null is null then true else id = id end
    order by id;

psql 会打印:

LOG:  statement: select * from manufacturers where case when null is null then true else id = id end order by id;
LOG:  plan:
DETAIL:     PLANNEDSTMT 
   :commandType 1 
   :queryId 0 
   :hasReturning false 
   :hasModifyingCTE false 
   :canSetTag true 
   :transientPlan false 
   :planTree 
      INDEXSCAN 
      :startup_cost 0.15 
      :total_cost 53.85 
      :plan_rows 380 
      :plan_width 180 
      :targetlist (
         TARGETENTRY 
         :expr 
            VAR 
            :varno 1 
            :varattno 1 
            :vartype 23 
            :vartypmod -1 
            :varcollid 0 
            :varlevelsup 0 
            :varnoold 1 
            :varoattno 1 
            :location 7
            
         :resno 1 
         :resname id 
         :ressortgroupref 1 
         :resorigtbl 104875 
         :resorigcol 1 
         :resjunk false
         
         TARGETENTRY 
         :expr 
            VAR 
            :varno 1 
            :varattno 2 
            :vartype 1043 
            :vartypmod 34 
            :varcollid 100 
            :varlevelsup 0 
            :varnoold 1 
            :varoattno 2 
            :location 7
            
         :resno 2 
         :resname name 
         :ressortgroupref 0 
         :resorigtbl 104875 
         :resorigcol 2 
         :resjunk false
         
         TARGETENTRY 
         :expr 
            VAR 
            :varno 1 
            :varattno 3 
            :vartype 1043 
            :vartypmod 44 
            :varcollid 100 
            :varlevelsup 0 
            :varnoold 1 
            :varoattno 3 
            :location 7
            
         :resno 3 
         :resname country 
         :ressortgroupref 0 
         :resorigtbl 104875 
         :resorigcol 3 
         :resjunk false
         
      )
      :qual <> 
      :lefttree <> 
      :righttree <> 
      :initPlan <> 
      :extParam (b)
      :allParam (b)
      :scanrelid 1 
      :indexid 104879 
      :indexqual <> 
      :indexqualorig <> 
      :indexorderby <> 
      :indexorderbyorig <> 
      :indexorderdir 1
      
   :rtable (
      RTE 
      :alias <> 
      :eref 
         ALIAS 
         :aliasname manufacturers 
         :colnames ("id" "name" "country")
         
      :rtekind 0 
      :relid 104875 
      :relkind r 
      :lateral false 
      :inh false 
      :inFromCl true 
      :requiredPerms 2 
      :checkAsUser 0 
      :selectedCols (b 9 10 11)
      :modifiedCols (b)
      
   )
   :resultRelations <> 
   :utilityStmt <> 
   :subplans <> 
   :rewindPlanIDs (b)
   :rowMarks <> 
   :relationOids (o 104875)
   :invalItems <> 
   :nParamExec 0
   

阅读计划树需要一些实践和对 PostgreSQL 内部结构的理解。如果大部分内容根本没有意义,请不要强调。这里主要感兴趣的是用于读取表的索引扫描的qual(限定符或where 子句)是空的。 PostgreSQL 不仅优化了CASE,还注意到id = id 始终是true,并且完全优化了where 子句。

【讨论】:

以上是关于在 WHERE 子句中使用可选条件的正确方法的主要内容,如果未能解决你的问题,请参考以下文章

SQL查询where子句如果没有匹配记录则省略

Laravel 4:可选 where 子句

使用可选参数对 Where 子句进行续集

在 mysql 中创建一个可选的 Where 子句

WHERE 子句中的动态可选参数

sql Where子句中的可选参数