使用复杂过滤优化SQL查询

时间:2016-01-15 22:35:01

标签: mysql sql

请注意,以下问题专门针对 MySQL

想象一个名为Cars的表,其结构如下(我们可以忽略缺少正确的键约束等,因为它与我的问题无关):

CREATE TABLE Cars
(
  id Integer,
  maker_id Integer,
  status_id Integer,
  notes Varchar(100)
);

现在想象一下加载一些测试数据:

INSERT INTO Cars
(id, maker_id, status_id, notes)
VALUES
(1, 1001, 0, 'test1'),
(2, 1001, 0, 'test2'),
(3, 1001, 0, 'test3'),
(4, 1002, 0, 'test4'),
(5, 1002, 0, 'test5'),
(6, 1002, 1, 'test6'),
(7, 1002, 1, 'test7'),
(8, 1002, 2, 'test8'),
(9, 1003, 3, 'test9'),
(10, 1003, 3, 'test10'),
(11, 1003, 4, 'test11'),
(12, 1003, 4, 'test12'),
(13, 1003, 5, 'test13'),
(14, 1003, 5, 'test14')

共有14条记录,DISTINCT(1001,1002,1003)中有3 maker_id个值,DISTINCT中有status_id个值{0,1,2} ,3,4,5)。

现在,想象一下DISTINCT对(maker_idstatus_id)。

SELECT DISTINCT maker_id, status_id FROM Cars;

以下是SQL Fiddle中示例的链接:http://sqlfiddle.com/#!9/cb1c7/2

这会产生以下记录(maker_idstatus_id):

  • (1001,0)
  • (1002,0)
  • (1002,1)
  • (1002,2)
  • (1003,3)
  • (1003,4)
  • (1003,5)

我需要返回的逻辑如下:

如果给定的maker_id值(例如,1001)仅对其对应的DISTINCTmaker_idstatus_id)对有1个不同的记录,则只需返回它。在这个例子中:(1001,0)。

如果给定的maker_id值与其对应的DISTINCTmaker_idstatus_id)对的超过1 个不同记录,则返回全部其中除了 status_id值为0的那个。在这个例子中:(1002,1),(1002,2),(1003,3),(1003,4) ,和(1003,5)。

请注意,我们遗漏了(1002,0)。

任何人都可以想到一个更简洁/更高效(在运行时方面)编写此查询的方式吗?在现实世界中,我的桌子有数百万条记录。

我想出了以下内容:

SELECT
  subq.maker_id,
  subq.status_id
FROM
(
  SELECT DISTINCT
    maker_id,
    status_id,
    (SELECT COUNT(*) FROM Cars WHERE maker_id = c.maker_id AND status_id != 0 GROUP BY maker_id) AS counter
  FROM Cars AS c
) AS subq

WHERE
  subq.counter IS NULL
  OR (subq.counter IS NOT NULL AND subq.status_id != 0)
;

以下是SQL Fiddle中的一个示例:http://sqlfiddle.com/#!9/cb1c7/3

4 个答案:

答案 0 :(得分:2)

有几种查询模式可以返回指定的结果。有些看起来比其他看起来更复杂。性能上可能存在差异。

在庞大的集合上执行GROUP BY操作可能成本很高(就资源和已用时间而言,如果MySQL无法使用索引来优化该操作,则可能会花费很多时间。(使用GROUP BY操作是获得status_id每个maker_id计数的一种方法。)

当重复执行相关子查询时,相关子查询可能很昂贵。当需要执行的次数有限时,我通常只能从相关子查询中看到更好的性能。

我认为获得良好表现的最好机会是:

未经过测试

 SELECT c.maker_id
      , c.status_id
   FROM Cars c
  WHERE c.status_id > 0

 UNION ALL

 SELECT d.maker_id
      , d.status_id
   FROM Cars d
   LEFT
   JOIN Cars e
     ON e.maker_id = d.maker_id
    AND e.status_id > 0
  WHERE e.maker_id IS NULL  
    AND d.status_id = 0

至于是否比其他查询方法更有效或更简洁,我们需要测试。

但是对于使用此查询获得良好性能的任何镜头,我们将需要一个索引。

..  ON Cars (maker_id, status_id)

我们希望EXPLAIN输出会在Extra列中显示“使用索引”。而且我们并不期待“使用filesort”。

这种方法的一个重大缺点是,有效地将两次通过表(或索引)。

第一个SELECT非常简单......把我status_id不为零的所有行。我们需要所有这些行。它可能是一个索引,例如

... ON Cars (status_id, maker_id)

可能对该查询有益。但是,如果我们返回表格的很大一部分,我会向甜甜圈投注美元,以便对其他索引的完整扫描速度一样快或更快。

第二个SELECT使用反连接模式。这样做是让所有行status_id等于零的行,并从该集合“过滤掉”有另一行的任何行,对于maker_id status_id行{ 1}}除了零。

我们使用外部联接操作(LEFT JOIN)进行过滤,以返回status_id=0的所有行,以及所有匹配的行。 技巧WHERE子句中的谓词,用于过滤掉所有匹配的行。所以我们留下的是没有找到匹配的行。也就是说,maker_id的值只有 status_id=0行。

我们可以使用NOT EXISTS谓词而不是反连接获得等效结果。但根据我的经验,有时表现并不好。我们可以重写第二个SELECT(在UNION ALL操作之后)

 SELECT d.maker_id
      , d.status_id
   FROM Cars d
  WHERE d.status_id = 0
    AND NOT EXISTS
        ( SELECT 1
            FROM Cars e
           WHERE e.maker_id = d.maker_id
             AND e.status_id > 0
        )

该查询的性能将取决于合适的索引,就像反连接一样。

重要提示:省略ALL关键字。 UNION ALL操作只是连接两个查询的结果。如果我们省略ALL关键字,那么我们要求MySQL执行“排序唯一”操作以消除重复的行。

注意:UNION ALL而不是OR条件的原因是我通常会使用UNION ALL获得更好的查询计划。当谓词在不同的列和条件上时,MySQL优化器似乎对OR做得不好,并且谓词可用于“驱动”执行计划。使用UNION ALL,将其分为两个查询,我们通常可以为这两个部分制定一个好的计划。

答案 1 :(得分:1)

此查询将有助于:)

select
    distinct c1.maker_id, c1.status_id
from
    Cars AS c1
where
    c1.status_id!=0
    or c1.maker_id not in (
        select distinct c2.maker_id
        from Cars AS c2
        where c2.status_id!=0
    )

答案 2 :(得分:1)

作为我第一个回答的完全不同的方法......

鉴于一种不可能的情况......桌面上没有索引,也没有创建索引的可能性......我们基本上留下了两个非常糟糕的选择:一种巨大的集合和一些通行证通过它,或通过一张巨大的桌子大量通过。

两个邪恶中较小的一个似乎是那种。我们无法提供索引,但我们可以在查询中动态构建索引。还有一大堆磁盘io到/ tmp。 SSD上安装了一个散热器,对吗?

我们只对整个翻转组进行排序,然后重复几次。

  SELECT t.maker_id
       , t.status_id
    FROM ( SELECT IF(s.status_id=0 AND s.maker_id=@p_maker,NULL,s.status_id) AS status_id
                , @p_maker := s.maker_id AS maker_id
             FROM ( SELECT @p_maker := NULL ) i
            CROSS
             JOIN ( SELECT c.maker_id
                         , c.status_id
                      FROM Cars c
                     GROUP BY c.maker_id DESC, c.status_id DESC 
                     ORDER BY c.maker_id DESC, c.status_id DESC
                  ) s
         ) t
   WHERE t.status_id IS NOT NULL

答案 3 :(得分:1)

我对表现并不完全确定..但我喜欢优雅:

  SELECT maker_id,
         status_id
    FROM cars  
   WHERE status_id != 0
GROUP BY maker_id, status_id
   UNION ALL
  SELECT maker_id,
         MAX(status_id) max_status_id
    FROM cars  
GROUP BY maker_id
  HAVING max_status_id = 0