MySQL中distinct语句去查询重复记录及相关的性能讨论
在MySQL查询中,可能会包含重复值。这并不成问题,不过,有时您也许希望仅仅列出不同(distinct)的值。
关键词DISTINCT用于返回唯一不同的值,就是去重啦。用法也很简单:
SELECTDISTINCT*FROMtableName
DISTINCT这个关键字来过滤掉多余的重复记录只保留一条。
另外,如果要对某个字段去重,可以试下:
SELECT*,COUNT(DISTINCTnowamagic)FROMtableGROUPBYnowamagic
这个用法,MySQL的版本不能太低。
在编写查询之前,我们甚至应该对过滤条件进行排序,真正高效的条件(可能有多个,涉到同的表)是查询的主要驱动力,低效条件只起辅助作用。那么定义高效过滤条件的准则是什呢?首先,要看过滤条件能否尽快减少必须处理的数据量。所以,我们必须倍加关注条件的写方式。
假设有四个表:customers、orders、orderdetail、articles,现在假设SQL要处理的问题是:找出最近六个月内居住在Gotham市、订购了蝙蝠车的所有客户。当然,编写这个查询有多种方法,ANSISQL的推崇者可能写出下列语句:
selectdistinctc.custname fromcustomersc joinorderso ono.custid=c.custid joinorderdetailod onod.ordid=o.ordid joinarticlesa ona.artid=od.artid wherec.city='GOTHAM' anda.artname='BATMOBILE' ando.ordered>=somefunc
其中,somefunc是个函数,返回距今六个月前的具体日期。注意上面用了distinct,因为考虑到某个客户可以是大买家,最近订购了好几台蝙蝠车。
暂不考虑优化器将如何改写此查询,我们先看一下这段代码的含义。首先,来自customers表的数据应只保留城市名为Gotham的记录。接着,搜索orders表,这意味着custid字段最好有索引,否则只有通过排序、合并或扫描orders表建立一个哈希表才能保证查询速度。对orders表,还要针对订单日期进行过滤:如果优化器比较聪明,它会在连接(join)前先过滤掉一些数据,从而减少后面要处理的数据量;不太聪明的优化器则可能会先做连接,再作过滤,这时在连接中指定过滤条件利于提高性能,例如:
joinorderso ono.custid=c.custid anda.ordered>=somefunc
注意,如果是:
leftouterjoinordersoon o.custid=c.custid anda.ordered>=somefunc
此处关于left表的筛选条件将失效,因为是左外连接,左表的所有列都将出现在这次连接结果集中)。
即使过滤条件与连接(join)无关,优化器也会受到过滤条件的影响。例如,若orderdetail的主键为(ordid,artid),即ordid为索引的第一个属性,那么我们可以利用索引找到与订单相关的记录。但如果主键是(artid,ordid)就太不幸了(注意,就关系理论而言,无论哪个版本都是完全一样),此时的访问效率比(ordid,artid)作为索引时要差,甚至一些数据库产品无法使用该索引(注3),唯一的希望就是在ordid上加独立索引了。
连接了表orderdetail和orders之后,来看articles表,这不会有问题,因为表order包括artid字段。最后,检查articles中的值是否为Batmobile。查询就这样结束了,因为用了distinct,通过层层筛选的客户名还必须要排序,以剔除重复项目。
避免在最高层使用distinct应该是一条基本规则。原因在于,即使我们遗漏了连接的某个条件,distinct也会使查询"看似正确"地执行——无可否认,发现重复数据容易,发现数据不准确很难,所以避免在最高层使用distinct应该是一条基本规则。
发现结果不正确更难,例如,如果恰巧有多位客户都叫"Wayne",distinct不但会剔除由同个客户的多张订单产生的重复项目,也会剔除由名字相同的不同客户产生的重复项目。事实上,应该同时返回具唯一性的客户ID和客户名,以保证得到蝙蝠车买家的完整清单。
要摆脱distinct,可考虑以下思路:客户在Gohtam市,而且满足存在性测试,即在最近六个月订购过蝙蝠车。注意,多数(但非全部)SQL方言支持以下语法:
selectc.custname fromcustomersc wherec.city='GOTHAM' andexists(selectnull fromorderso, orderdetailod, articlesa wherea.artname='BATMOBILE' anda.artid=od.artid andod.ordid=o.ordid ando.custid=c.custid ando.ordered>=somefunc)
上例的存在性测试,同一个名字可能出现多次,但每个客户只出现一次,不管他有多少订单。有人认为我对ANSISQL语法的挑剔有点苛刻(指"蝙蝠车买主"的例子),因为上面代码中customers表的地位并没有降低。其实,关键区别在于,新查询中customers表是查询结果的唯一来源(嵌套的子查询会负责找出客户子集),而先前的查询却用了join。
这个嵌套的子查询与外层的select关系十分密切。如代码第11行所示(粗体部分),子查询参照了外层查询的当前记录,因此,内层子查询就是所谓的关联子查询(correlatedsubquery)。
此类子查询有个弱点,它无法在确定当前客户之前执行。如果优化器不改写此查询,就必须先找出每个客户,然后逐一检查是否满足存在性测试,当来自Gotham市的客户非常少时执行效率倒是很高,否则情况会很糟(此时,优秀的优化器应尝试其他执行查询的方式)。
selectcustname fromcustomers wherecity='GOTHAM' andcustidin (selecto.custid fromorderso, orderdetailod, articlesa wherea.artname='BATMOBILE' anda.artid=od.artid andod.ordid=o.ordid ando.ordered>=somefunc)
在这个例子中,内层查询不再依赖外层查询,它已变成了非关联子查询(uncorrelatedsubquery),只须执行一次。很显然,这段代码采用了原有的执行流程。在本节的前一个例子中,必须先搜寻符合地点条件的客户(如均来自GOTHAM),接着依次检查各个订单。而现在,订购了蝙蝠车的客户,可以通过内层查询获得。
不过,如果更仔细地分析一下,前后两个版本的代码还有些更微妙的差异。含关联子查询的代码中,至关重要的是orders表中的custid字段要有索引,而这对另一段代码并不重要,因为这时要用到的索引(如果有的话)是表customers的主键索引。
你或许注意到,新版的查询中执行了隐式的distinct。的确,由于连接操作,子查询可能会返回有关一个客户的多条记录。但重复项目不会有影响,因为in条件只检查该项目是否出现在子查询返回的列表中,且in不在乎某值在列表中出现了一次还是一百次。但为了一致性,作为整体,应该对子查询和主查询应用相同的规则,也就是在子查询中也加入存在性测试:
selectcustname fromcustomers wherecity='GOTHAM' andcustidin (selecto.custid fromorderso whereo.ordered>=somefunc andexists(selectnull fromorderdetailod, articlesa wherea.artname='BATMOBILE' anda.artid=od.artid andod.ordid=o.ordid))
或者
selectcustname fromcustomers wherecity='GOTHAM' andcustidin (selectcustid fromorders whereordered>=somefunc andordidin(selectod.ordid fromorderdetailod, articlesa wherea.artname='BATMOBILE' anda.artid=od.artid)
尽管嵌套变得更深、也更难懂了,但子查询内应选择exists还是in的选择规则相同:此选择取决于日期与商品条件的有效性。除非过去六个月的生意非常清淡,否则商品名称应为最有效的过滤条件,因此子查询中用in比exists好,这是因为,先找出所有蝙蝠车的订单、再检查销售是否发生在最近六个月,比反过来操作要快。如果表orderdetail的artid字段有索引,这个方法会更快,否则,这个聪明巧妙的举措就会黯然失色。
每当对大量记录做存在性检查时,选择in还是exists须斟酌。
利于多数SQL方言,非关联子查询可以被改写成from子句中的内嵌视图。然而,一定要记住的是,in会隐式地剔除重复项目,当子查询改写为from子句中的内嵌视图时,必须要显式地消除重复项目。例如:
selectcustname fromcustomers wherecity='GOTHAM' andcustidin (selecto.custid fromorderso, (selectdistinctod.ordid fromorderdetailod, articlesa wherea.artname='BATMOBILE' anda.artid=od.artid)x whereo.ordered>=somefunc andx.ordid=o.ordid)
总结:保证SQL语句返回正确结果,只是建立最佳SQL语句的第一步。