Search code examples
mysqlmysql-workbenchquery-optimizationamazon-rds

Speed Up A Large Insert From Select Query With Multiple Joins


I'm trying to denormalize a few MySQL tables I have into a new table that I can use to speed up some complex queries with lots of business logic. The problem that I'm having is that there are 2.3 million records I need to add to the new table and to do that I need to pull data from several tables and do a few conversions too. Here's my query (with names changed)

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);

The query itself works because I was able to run it using a filter on log_set_name however that filter's condition will only work for less than 1% of the total records because one of the values for log_set_name has 2.2 million records in it which is the majority of the records. So there is nothing else I can use to break this query up into smaller chunks from what I can see. The problem is that the query is taking too long to run on the rest of the 2.2 million records and it ends up timing out after a few hours and then the transaction is rolled back and nothing is added to the new table for the 2.2 million records; only the 0.1 million records were able to be processed and that was because I could add a filter that said where log_set_name != 'value with the 2.2 million records'.

Is there a way to make this query more performant? Am I trying to do too many joins at once and perhaps I should populate the row's columns in their own individual queries? Or is there some way I can page this type of query so that MySQL executes it in batches? I already got rid of all my indexes on the log_set_logs table because I read that those will slow down inserts. I also jacked my RDS instance up to a db.r4.4xlarge write node. I am also using MySQL Workbench so I increased all of it's timeout values to their maximums giving them all nines. All three of these steps helped and were necessary in order for me to get the 1% of the records into the new table but it still wasn't enough to get the 2.2 million records without timing out. Appreciate any insights as I'm not adept to this type of bulk insert from a select.

'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'

Solution

    • baselog_offload_location has not PRIMARY KEY; what's up?

    • GUIDs/UUIDs can be terribly inefficient. A partial solution is to convert them to BINARY(16) to shrink them. More details here: http://localhost/rjweb/mysql/doc.php/uuid ; (MySQL 8.0 has similar functions.)

    • It would probably be more efficient if you have a separate (optionally redundant) column for vehicle rather than needing to do

        SUBSTRING_INDEX(SUBSTRING_INDEX(path, '/', 8), '/', -1) as vehicle
      
    • Why JOIN baselog_offload_location? Three seems to be no reference to columns in that table. If there, be sure to qualify them so we know what is where. Preferably use short aliases.

    • The lack of an index on baselog_index_guid may be critical to performance.

    • Please provide EXPLAIN SELECT ... for the SELECT in your INSERT and for the original (slow) query.

    • SELECT MAX(LENGTH(original_location)) FROM .. -- to see if it really is too big to index. What version of MySQL are you using? The limit increased recently.

    • For the above item, we can talk about having a 'hash'.

    • "paging the query". I call it "chunking". See http://mysql.rjweb.org/doc.php/deletebig#deleting_in_chunks . That talks about deleting, but it can be adapted to INSERT .. SELECT since you want to "chunk" the select. If you go with chunking, Javier's comment becomes moot. Your code would be chunking the selects, hence batching the inserts:

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