sql優化-派生表與inner-join


首先來說明一下派生表

外部的表查詢的結果集是從子查詢中生成的.如下形式:

select ... from (select ....) dt

如上形式中括號中的查詢的結果作為外面select語句的查詢源,派生表必須指定別名,因此后面的dt必須指定。派生表和臨時表差不多,但是在select語句中派生表比臨時表要容易,因為派生表不用創建。

一個有關派生表優化的實例。

開發同事發來一個sql優化,涉及到4張表,表中的數據都不是很大,但是查詢起來真的很慢。服務器性能又差,查詢總是超時。

四張表的表結構如下:

       Table: t_info_setting
Create Table: CREATE TABLE `t_info_setting` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `parent_key` varchar(32) NOT NULL,
  `column_name` varchar(32) NOT NULL,
  `column_key` varchar(32) NOT NULL,
  `storage_way` tinyint(4) DEFAULT '0',
  `check_way` tinyint(4) DEFAULT '0',
  `remark` varchar(500) DEFAULT '',
  `operator` varchar(128) DEFAULT '',
  `status` int(11) DEFAULT '1',
  `update_time` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `column_key` (`column_key`)
) ENGINE=InnoDB AUTO_INCREMENT=9 DEFAULT CHARSET=utf8
t_info_setting
       Table: t_articles_status
Create Table: CREATE TABLE `t_articles_status` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `linkId` varchar(36) NOT NULL,
  `column_key` varchar(32) NOT NULL,
  `status` int(11) DEFAULT '50000',
  `operator_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `article_status` (`linkId`,`column_key`)
) ENGINE=InnoDB AUTO_INCREMENT=22232 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
t_articles_status
       Table: t_article_operations
Create Table: CREATE TABLE `t_article_operations` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `linkId` varchar(36) NOT NULL,
  `column_key` varchar(32) NOT NULL DEFAULT '',
  `type` varchar(16) DEFAULT '',
  `operator` varchar(128) DEFAULT '',
  `operator_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  KEY `article_operation` (`linkId`,`column_key`),
  KEY `operator_time` (`operator_time`)
) ENGINE=InnoDB AUTO_INCREMENT=23316 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
t_article_operations
       Table: t_articles
Create Table: CREATE TABLE `t_articles` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `linkId` varchar(36) DEFAULT NULL,
  `source` int(11) DEFAULT '0',
  `title` varchar(150) NOT NULL,
  `author` varchar(150) NOT NULL,
  `tags` varchar(200) DEFAULT NULL,
  `abstract` varchar(512) DEFAULT NULL,
  `content` mediumtext,
  `thumbnail` varchar(256) DEFAULT NULL,
  `sourceId` varchar(24) DEFAULT '',
  `accessoryUrl` text,
  `relatedStock` text,
  `contentUrl` text,
  `secuInfo` text,
  `market` varchar(10) DEFAULT 'hk',
  `code` varchar(10) DEFAULT '',
  `updator` varchar(64) DEFAULT '',
  `createTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP COMMENT '建立時間',
  `updateTime` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間',
  PRIMARY KEY (`id`),
  UNIQUE KEY `linkId` (`linkId`)
) ENGINE=InnoDB AUTO_INCREMENT=15282 DEFAULT CHARSET=utf8
t_articles

上面四張表,由上面的自增字段的值可以知道表的數據並不是很大,最大的表也就2萬多行,表中的索引情況已經一目了然。開發同學給出的sql語句如下:

(
  SELECT
    'daily' AS category,
    e.linkId,
    e.title,
    e.updateTime
  FROM
    (
      SELECT DISTINCT
        b.column_key,
        b. STATUS,
        b.linkId
      FROM
        t_info_setting a
      inner JOIN t_articles_status b ON a.column_key = b.column_key
      inner JOIN t_article_operations c ON b.column_key = c.column_key
      WHERE
        a.parent_key = 'daily'
      AND a. STATUS = 1
      AND b. STATUS = 80000
      ORDER BY
        c.operator_time DESC
      LIMIT 1
    ) AS d
  inner JOIN t_articles e ON d.linkId = e.linkId
)
UNION ALL
  (
    SELECT
      'ipo' AS category,
      e.linkId,
      e.title,
      e.updateTime
    FROM
      (
        SELECT DISTINCT
          b.column_key,
          b. STATUS,
          b.linkId
        FROM
          t_info_setting a
        inner JOIN t_articles_status b ON a.column_key = b.column_key
        inner JOIN t_article_operations c ON b.column_key = c.column_key
        WHERE
          a.parent_key = 'ipo'
        AND a. STATUS = 1
        AND b. STATUS = 80000
        ORDER BY
          c.operator_time DESC
        LIMIT 1
      ) AS d
    inner JOIN t_articles e ON d.linkId = e.linkId
  )
UNION ALL
  (
    SELECT
      'research' AS category,
      e.linkId,
      e.title,
      e.updateTime
    FROM
      (
        SELECT DISTINCT
          b.column_key,
          b. STATUS,
          b.linkId
        FROM
          t_info_setting a
        inner JOIN t_articles_status b ON a.column_key = b.column_key
        inner JOIN t_article_operations c ON b.column_key = c.column_key
        WHERE
          a.parent_key = 'research'
        AND a. STATUS = 1
        AND b. STATUS = 80000
        ORDER BY
          c.operator_time DESC
        LIMIT 1
      ) AS d
    inner JOIN t_articles e ON d.linkId = e.linkId
  )
UNION ALL
  (
    SELECT
      'news' AS category,
      e.linkId,
      e.title,
      e.updateTime
    FROM
      (
        SELECT DISTINCT
          b.column_key,
          b. STATUS,
          b.linkId
        FROM
          t_info_setting a
        inner JOIN t_articles_status b ON a.column_key = b.column_key
        inner JOIN t_article_operations c ON b.column_key = c.column_key
        WHERE
          a.parent_key = 'news'
        AND a. STATUS = 1
        AND b. STATUS = 80000
        ORDER BY
          c.operator_time DESC
        LIMIT 1
      ) AS d
    inner JOIN t_articles e ON d.linkId = e.linkId
  )
開發給的sql

原sql很長大概有107行,但是分析這條sql發現了使用了三個union聯合查詢,然后每條聯合的sql語句基本是一模一樣的,只是改變了a.parent_key = 'research'這個條件。這說明我們只需要分析其中的一條sql即可。

        SELECT
            'research' AS category,
            e.linkId,
            e.title,
            e.updateTime
        FROM
            (                             -- 這里使用了派生表
                SELECT DISTINCT           --a
                    b.column_key,
                    b. STATUS,
                    b.linkId
                FROM
                    t_info_setting a
                inner JOIN t_articles_status b ON a.column_key = b.column_key
                inner JOIN t_article_operations c ON b.column_key = c.column_key           -- c
                WHERE
                    a.parent_key = 'research'
                AND a. STATUS = 1
                AND b. STATUS = 80000
                ORDER BY
                    c.operator_time DESC
                LIMIT 1
            ) AS d
        inner JOIN t_articles e ON d.linkId = e.linkId           -- b

首先:這條sql語句中使用了派生表,分析里面的子查詢,最后有一個limit 1也就是只查出一條數據,並且是按照operator_time 進行排序,那么distinct的去重復就是不需要的。再看子查詢中查詢出了三個字段,但是在b處和e表進行聯合查詢的時候只使用了linkId 這一個字段,因此子查詢中多余的兩個字段需要去掉。

在表t_article_operations上有一個符合索引,我們知道mysql在使用復合索引時,采用最左原則,因此在c處的聯合查詢我們需要加上linkId ,根據上面分析,改寫sql如下:

select
    'research' as category,
    e.linkId,
    e.title,
    e.updateTime
from (
    select b.linkId -- 去除不必要的列、distinct操作
    from t_info_setting a
    inner join t_articles_status b
        on a.column_key=b.column_key
    inner join t_article_operations c
        on b.linkId=c.linkId and b.column_key=c.column_key -- 關聯條件應包含linkId
    where
        a.parent_key='research'
        and a.status=1
        and b.status=80000
    order by c.operator_time desc
    limit 1
) d
inner join t_articles e
    on d.linkId=e.linkId;

然后查看下改寫前后兩個sql的執行計划。

 

 

改寫后的執行計划:

+----+-------------+------------+--------+-------------------+----------------+---------+-----------------------------------------------------------+-------+-------------+
| id | select_type | table      | type   | possible_keys     | key            | key_len | ref                                                       | rows  | Extra       |
+----+-------------+------------+--------+-------------------+----------------+---------+-----------------------------------------------------------+-------+-------------+
|  1 | PRIMARY     | <derived2> | system | NULL              | NULL           | NULL    | NULL                                                      |     1 | NULL        |
|  1 | PRIMARY     | e          | const  | linkId            | linkId         | 111     | const                                                     |     1 | NULL        |
|  2 | DERIVED     | c          | index  | article_operation | operator_time  | 5       | NULL                                                      | 14711 | NULL        |
|  2 | DERIVED     | a          | ref    | column_key        | column_key     | 98      | wlb_live_contents.c.column_key                            |     1 | Using where |
|  2 | DERIVED     | b          | ref    | article_status    | article_status | 208     | wlb_live_contents.c.linkId,wlb_live_contents.c.column_key |     1 | Using where |
+----+-------------+------------+--------+-------------------+----------------+---------+-----------------------------------------------------------+-------+-------------+

改寫之后的單個sql很快就有了結果,大概0.12秒就有了結束,而原來的sql會超時結束的。

在原sql語句中使用了union,因為我們最后的結果並不要求去重復,只是四個結果集的聯合,因此這里我們可以使用union all代替上面的union。

改寫后的執行計划DERIVED表示使用了派生表,我們看到在e表與派生表進行inner查詢的使用了索引。

分析:

之前看到一種說法是,在數據表和派生表聯合進行查詢時,不能使用索引,但是上面的的執行計划說明使用了索引(e表和派生表聯合查詢,e表使用了索引)。【究竟要怎么用還需進一步研究】

改寫sql

上面使用了派生表,其實數據量比較大時,派生表的效率並不是很高的,上面的查詢我們試着用4張表的聯合查詢來改寫。

改寫之后的sql如下:

SELECT
    'research' AS category,
    e.linkId,
    e.title,
    e.updateTime
FROM
    t_info_setting a
INNER JOIN t_articles_status b ON a.column_key = b.column_key
INNER JOIN t_article_operations c ON b.linkId = c.linkId
AND b.column_key = c.column_key
INNER JOIN t_articles e ON c.linkId = e.linkId
WHERE
    a.parent_key = 'research'
AND a. STATUS = 1
AND b. STATUS = 80000
ORDER BY
    c.operator_time DESC
LIMIT 1

查看執行計划:

+----+-------------+-------+-------+-------------------+----------------+---------+-----------------------------------------------------------+------+-------------+
| id | select_type | table | type  | possible_keys     | key            | key_len | ref                                                       | rows | Extra       |
+----+-------------+-------+-------+-------------------+----------------+---------+-----------------------------------------------------------+------+-------------+
|  1 | SIMPLE      | c     | index | article_operation | operator_time  | 5       | NULL                                                      |    1 | NULL        |
|  1 | SIMPLE      | a     | ref   | column_key        | column_key     | 98      | wlb_live_contents.c.column_key                            |    1 | Using where |
|  1 | SIMPLE      | b     | ref   | article_status    | article_status | 208     | wlb_live_contents.c.linkId,wlb_live_contents.c.column_key |    1 | Using where |
|  1 | SIMPLE      | e     | ref   | linkId            | linkId         | 111     | wlb_live_contents.c.linkId                                |    1 | NULL        |
+----+-------------+-------+-------+-------------------+----------------+---------+-----------------------------------------------------------+------+-------------+

根據執行計划,這個inner join的執行計划是要比上面的使用派生表的執行計划要高一些。

說明:

1:在使用聯合查詢的時候,可以考慮聯合查詢的鍵上創建索引,效率可能會高點。

2:可以考慮在order by的鍵上創建索引。

3:根據數據可以知道,t_article_operations本質上是一個流水表,記錄日志類信息,不應出現在日常查詢中。解決此種查詢的辦法:operator_time保存在t_articles_status中,查詢徹底移除t_article_operations,或臨時方法:t_article_operations只保留短期數據,歷史記錄定期遷移至其他表。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM