【问题标题】:Speed Up A Large Insert From Select Query With Multiple Joins使用多个连接加速从选择查询中的大型插入
【发布时间】:2020-12-19 15:07:10
【问题描述】:

我正在尝试将我拥有的几个 MySQL 表非规范化为一个新表,我可以使用它来加速一些具有大量业务逻辑的复杂查询。我遇到的问题是我需要将 230 万条记录添加到新表中,为此我需要从多个表中提取数据并进行一些转换。这是我的查询(名称已更改)

INSERT INTO database_name.log_set_logs
(offload_date, vehicle, jurisdiction, baselog_path, path,
 baselog_index_guid, new_location, log_set_name, index_guid) 
 (
select  STR_TO_DATE(logset_logs.offload_date, '%Y.%m.%d') as offload_date,
        logset_logs.vehicle, jurisdiction, baselog_path, path,
        baselog_trees.baselog_index_guid, new_location, logset_logs.log_set_name,
        logset_logs.index_guid
    from  
    (
        SELECT  SUBSTRING_INDEX(SUBSTRING_INDEX(path, '/', 7), '/', -1) as offload_date,
                SUBSTRING_INDEX(SUBSTRING_INDEX(path, '/', 8), '/', -1) as vehicle,
                SUBSTRING_INDEX(path, '/', 9) as baselog_path, index_guid,
                path, log_set_name
            FROM  database_name.baselog_and_amendment_guid_to_path_mappings 
    ) logset_logs
    left join  database_name.log_trees baselog_trees
         ON baselog_trees.original_location = logset_logs.baselog_path
    left join  database_name.baselog_offload_location location
         ON location.baselog_index_guid = baselog_trees.baselog_index_guid);

查询本身有效,因为我能够使用log_set_name 上的过滤器运行它,但是该过滤器的条件仅适用于不到总记录的 1%,因为 log_set_name 的值之一有 220 万条记录其中大部分是记录。因此,我无法使用其他方法将这个查询分解为我所看到的更小的块。问题是查询在 220 万条记录的其余部分上运行的时间太长,并且在几个小时后最终超时,然后事务被回滚,并且没有任何内容添加到 220 万条记录的新表中;只有 10 万条记录能够被处理,那是因为我可以添加一个过滤器,说明 log_set_name != 'value with the 2.2 million records' 的位置。

有没有办法让这个查询更高效?我是否试图一次进行太多连接,也许我应该在他们自己的单独查询中填充行的列?或者有什么方法可以分页这种类型的查询,以便 MySQL 批量执行它?我已经删除了log_set_logs 表上的所有索引,因为我读到这些索引会减慢插入速度。我还将我的 RDS 实例提升到 db.r4.4xlarge 写入节点。我也在使用 MySQL Workbench,所以我将它的所有超时值都增加到了最大值,给了它们所有的 9。所有这三个步骤都有帮助,并且对于我将 1% 的记录放入新表中是必要的,但仍然不足以在不超时的情况下获得 220 万条记录。感谢任何见解,因为我不擅长从选择中进行这种类型的批量插入。

'CREATE TABLE `log_set_logs` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `purged` tinyint(1) NOT NULL DEFAUL,
  `baselog_path` text,
  `baselog_index_guid` varchar(36) DEFAULT NULL,
  `new_location` text,
  `offload_date` date NOT NULL,
  `jurisdiction` varchar(20) DEFAULT NULL,
  `vehicle` varchar(20) DEFAULT NULL,
  `index_guid` varchar(36) NOT NULL,
  `path` text NOT NULL,
  `log_set_name` varchar(60) NOT NULL,
  `protected_by_retention_condition_1` tinyint(1) NOT NULL DEFAULT ''1'',
  `protected_by_retention_condition_2` tinyint(1) NOT NULL DEFAULT ''1'',
  `protected_by_retention_condition_3` tinyint(1) NOT NULL DEFAULT ''1'',
  `protected_by_retention_condition_4` tinyint(1) NOT NULL DEFAULT ''1'',
  `general_comments_about_this_log` text,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=1736707 DEFAULT CHARSET=latin1'


'CREATE TABLE `baselog_and_amendment_guid_to_path_mappings` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `path` text NOT NULL,
  `index_guid` varchar(36) NOT NULL,
  `log_set_name` varchar(60) NOT NULL,
  PRIMARY KEY (`id`),
  KEY `log_set_name_index` (`log_set_name`),
  KEY `path_index` (`path`(42))
) ENGINE=InnoDB AUTO_INCREMENT=2387821 DEFAULT CHARSET=latin1'

...

'CREATE TABLE `baselog_offload_location` (
  `baselog_index_guid` varchar(36) NOT NULL,
  `jurisdiction` varchar(20) NOT NULL,
  KEY `baselog_index` (`baselog_index_guid`),
  KEY `jurisdiction` (`jurisdiction`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1'


'CREATE TABLE `log_trees` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `baselog_index_guid` varchar(36) DEFAULT NULL,
  `original_location` text NOT NULL, -- This is what I have to join everything on and since it's text I cannot index it and the largest value is above 255 characters so I cannot change it to a vachar then index it either.
  `new_location` text,
  `distcp_returncode` int(11) DEFAULT NULL,
  `distcp_job_id` text,
  `distcp_stdout` text,
  `distcp_stderr` text,
  `validation_attempt` int(11) NOT NULL DEFAULT ''0'',
  `validation_result` tinyint(1) NOT NULL DEFAULT ''0'',
  `archived` tinyint(1) NOT NULL DEFAULT ''0'',
  `archived_at` timestamp NULL DEFAULT NULL,
  `created_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP,
  `updated_at` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  `dir_exists` tinyint(1) NOT NULL DEFAULT ''0'',
  `random_guid` tinyint(1) NOT NULL DEFAULT ''0'',
  `offload_date` date NOT NULL,
  `vehicle` varchar(20) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `baselog_index_guid` (`baselog_index_guid`)
) ENGINE=InnoDB AUTO_INCREMENT=1028617 DEFAULT CHARSET=latin1'

【问题讨论】:

  • 请编辑您的问题并包含这些表及其索引的 DDL - database_name.tableAdatabase_name.tableBdatabase_name.tableD
  • 那些看起来更像图片而不是 DDL
  • 我们希望看到每个表格的SHOW CREATE TABLE <name>text 输出,而不是屏幕截图。这为我们提供了有关表中索引的更多信息。使用文本而不是屏幕截图允许人们复制并粘贴到测试环境中以重现查询。 Stack Overflow guide 还建议不要使用代码图像。
  • 好的,DDL 已添加。我在log_trees.original_location 上留下了评论,我认为最终的问题是我必须加入那个文本字段,这意味着我不能索引它。它的最大值超过 255 个字符,所以我不能将它们更改为 varchars 然后索引它们。
  • 如果有人对此问题感兴趣,就开始悬赏。

标签: mysql mysql-workbench query-optimization amazon-rds


【解决方案1】:
  • baselog_offload_location 没有PRIMARY KEY;怎么了?

  • GUID/UUID 可能非常低效。部分解决方案是将它们转换为BINARY(16) 以缩小它们。更多细节在这里:http://localhost/rjweb/mysql/doc.php/uuid; (MySQL 8.0 也有类似的功能。)

  • 如果您为vehicle 设置一个单独的(可选冗余的)列,而不是需要这样做,这可能会更有效

      SUBSTRING_INDEX(SUBSTRING_INDEX(path, '/', 8), '/', -1) as vehicle
    
  • 为什么是JOIN baselog_offload_location?三似乎没有引用该表中的列。如果有,请务必对它们进行限定,以便我们知道什么在哪里。最好使用短别名。

  • baselog_index_guid 上缺少索引可能对性能至关重要。

  • 请为您的INSERT 中的SELECT 和原始(慢速)查询提供EXPLAIN SELECT ...

  • SELECT MAX(LENGTH(original_location)) FROM .. -- 看看它是否真的太大而无法索引。您使用的是什么版本的 MySQL?最近限制增加了。

  • 对于上述项目,我们可以谈论有一个“哈希”。

  • “分页查询”。我称之为“分块”。见http://mysql.rjweb.org/doc.php/deletebig#deleting_in_chunks。这谈到了删除,但它可以适应INSERT .. SELECT,因为你想“分块”选择。如果你使用分块,哈维尔的评论就会变得毫无意义。您的代码将分块选择,从而批量插入:

      Loop:
          INSERT .. SELECT .. -- of up to 1000 rows (see link)
      End loop
    

【讨论】:

  • 我想分页是最简单的解决方案,无需我更改任何其他内容。让我试试看。
猜你喜欢
  • 2018-04-21
  • 2021-01-09
  • 2013-11-15
  • 2023-03-20
  • 2016-08-26
  • 2016-11-27
  • 1970-01-01
  • 2014-08-31
  • 1970-01-01
相关资源
最近更新 更多