MYSQL查询执行速度很慢

时间:2016-02-25 12:41:17

标签: php mysql sql yii database-performance

我开发了一个用户批量上传模块。有两种情况,当我在数据库没有记录时批量上传20 000条记录。它需要大约5个小时。但是当数据库已经有大约30 000条记录时,上传速度非常慢。上传20 000条记录大约需要11个小时。我只是通过fgetcsv方法读取CSV文件。

if (($handle = fopen($filePath, "r")) !== FALSE) {
            while (($peopleData = fgetcsv($handle, 10240, ",")) !== FALSE) {
                if (count($peopleData) == $fieldsCount) {

//inside i check if user already exist (firstName & lastName & DOB)
//if not, i check if email exist. if exist, update the records.
//other wise insert a new record.
}}}

以下是运行的查询。 (我正在使用Yii框架)

SELECT * 
FROM `AdvanceBulkInsert` `t` 
WHERE renameSource='24851_bulk_people_2016-02-25_LE CARVALHO 1.zip.csv' 
LIMIT 1

SELECT cf.*, ctyp.typeName, cfv.id as customId, cfv.customFieldId, 
       cfv.relatedId, cfv.fieldValue, cfv.createdAt 
FROM `CustomField` `cf` 
    INNER JOIN CustomType ctyp on ctyp.id = cf.customTypeId 
    LEFT OUTER JOIN CustomValue cfv on cf.id = cfv.customFieldId 
                and relatedId = 0 
    LEFT JOIN CustomFieldSubArea cfsa on cfsa.customFieldId = cf.id 
WHERE ((relatedTable = 'people' and enabled = '1') 
  AND (onCreate = '1')) 
  AND (cfsa.subarea='peoplebulkinsert') 
ORDER BY cf.sortOrder, cf.label

SELECT * 
FROM `User` `t` 
WHERE `t`.`firstName`='Franck' 
  AND `t`.`lastName`='ALLEGAERT ' 
  AND `t`.`dateOfBirth`='1971-07-29' 
  AND (userType NOT IN ("1")) 
LIMIT 1

如果存在,请更新用户:

UPDATE `User` SET `id`='51394', `address1`='49 GRANDE RUE', 
                  `mobile`='', `name`=NULL, `firstName`='Franck', 
                  `lastName`='ALLEGAERT ', `username`=NULL, 
                  `password`=NULL, `email`=NULL, `gender`=0, 
                  `zip`='60310', `countryCode`='DZ', 
                  `joinedDate`='2016-02-23 10:44:18', 
                  `signUpDate`='0000-00-00 00:00:00', 
                  `supporterDate`='2016-02-25 13:26:37', `userType`=3, 
                  `signup`=0, `isSysUser`=0, `dateOfBirth`='1971-07-29', 
                  `reqruiteCount`=0, `keywords`='70,71,72,73,74,75', 
                  `delStatus`=0, `city`='AMY', `isUnsubEmail`=0, 
                  `isManual`=1, `isSignupConfirmed`=0, `profImage`=NULL, 
                  `totalDonations`=NULL, `isMcContact`=NULL, 
                  `emailStatus`=NULL, `notes`=NULL, 
                  `addressInvalidatedAt`=NULL, 
                  `createdAt`='2016-02-23 10:44:18', 
                  `updatedAt`='2016-02-25 13:26:37', `longLat`=NULL 
WHERE `User`.`id`='51394'

如果用户不存在,请插入新记录。

表引擎类型是MYISAM。只有电子邮件列才有索引。

如何优化此项以缩短处理时间?

查询2,耗时0.4701秒,这意味着30 000条记录需要14103秒,约为235分钟。大约6个小时。

更新

CREATE TABLE IF NOT EXISTS `User` (
  `id` bigint(20) NOT NULL,
  `address1` text COLLATE utf8_unicode_ci,
  `mobile` varchar(15) COLLATE utf8_unicode_ci DEFAULT NULL,
  `name` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
  `firstName` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `lastName` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `username` varchar(20) COLLATE utf8_unicode_ci DEFAULT NULL,
  `password` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
  `email` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL,
  `gender` tinyint(2) NOT NULL DEFAULT '0' COMMENT '1 - female, 2-male, 0 - unknown',
  `zip` varchar(15) COLLATE utf8_unicode_ci DEFAULT NULL,
  `countryCode` varchar(3) COLLATE utf8_unicode_ci DEFAULT NULL,
  `joinedDate` datetime DEFAULT NULL,
  `signUpDate` datetime NOT NULL COMMENT 'User signed up date',
  `supporterDate` datetime NOT NULL COMMENT 'Date which user get supporter',
  `userType` tinyint(2) NOT NULL,
  `signup` tinyint(2) NOT NULL DEFAULT '0' COMMENT 'whether user followed signup process 1 - signup, 0 - not signup',
  `isSysUser` tinyint(1) NOT NULL DEFAULT '0' COMMENT '1 - system user, 0 - not a system user',
  `dateOfBirth` date DEFAULT NULL COMMENT 'User date of birth',
  `reqruiteCount` int(11) DEFAULT '0' COMMENT 'User count that he has reqruited',
  `keywords` text COLLATE utf8_unicode_ci COMMENT 'Kewords',
  `delStatus` tinyint(2) NOT NULL DEFAULT '0' COMMENT '0 - active, 1 - deleted',
  `city` varchar(50) COLLATE utf8_unicode_ci DEFAULT NULL,
  `isUnsubEmail` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 - ok, 1 - Unsubscribed form email',
  `isManual` tinyint(1) NOT NULL DEFAULT '0' COMMENT '0 - ok, 1 - Manualy add',
  `longLat` varchar(45) COLLATE utf8_unicode_ci DEFAULT NULL COMMENT 'Longitude and Latitude',
  `isSignupConfirmed` tinyint(4) NOT NULL DEFAULT '0' COMMENT 'Whether user has confirmed signup ',
  `profImage` tinytext COLLATE utf8_unicode_ci COMMENT 'Profile image name or URL',
  `totalDonations` float DEFAULT NULL COMMENT 'Total donations made by the user',
  `isMcContact` tinyint(1) DEFAULT NULL COMMENT '1 - Mailchimp contact',
  `emailStatus` tinyint(2) DEFAULT NULL COMMENT '1-bounced, 2-blocked',
  `notes` text COLLATE utf8_unicode_ci,
  `addressInvalidatedAt` datetime DEFAULT NULL,
  `createdAt` datetime NOT NULL,
  `updatedAt` datetime DEFAULT NULL
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `AdvanceBulkInsert` (
  `id` int(11) NOT NULL,
  `source` varchar(256) NOT NULL,
  `renameSource` varchar(256) DEFAULT NULL,
  `countryCode` varchar(3) NOT NULL,
  `userType` tinyint(2) NOT NULL,
  `size` varchar(128) NOT NULL,
  `errors` varchar(512) NOT NULL,
  `status` char(1) NOT NULL COMMENT '1:Queued, 2:In Progress, 3:Error, 4:Finished, 5:Cancel',
  `createdAt` datetime NOT NULL,
  `createdBy` int(11) NOT NULL
) ENGINE=MyISAM DEFAULT CHARSET=latin1;

CREATE TABLE IF NOT EXISTS `CustomField` (
  `id` int(11) NOT NULL,
  `customTypeId` int(11) NOT NULL,
  `fieldName` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `relatedTable` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `defaultValue` text COLLATE utf8_unicode_ci,
  `sortOrder` int(11) NOT NULL DEFAULT '0',
  `enabled` char(1) COLLATE utf8_unicode_ci DEFAULT '1',
  `listItemTag` char(1) COLLATE utf8_unicode_ci DEFAULT NULL,
  `required` char(1) COLLATE utf8_unicode_ci DEFAULT '0',
  `onCreate` char(1) COLLATE utf8_unicode_ci DEFAULT '1',
  `onEdit` char(1) COLLATE utf8_unicode_ci DEFAULT '1',
  `onView` char(1) COLLATE utf8_unicode_ci DEFAULT '1',
  `listValues` text COLLATE utf8_unicode_ci,
  `label` varchar(64) COLLATE utf8_unicode_ci DEFAULT NULL,
  `htmlOptions` text COLLATE utf8_unicode_ci
) ENGINE=MyISAM AUTO_INCREMENT=12 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `CustomFieldSubArea` (
  `id` int(11) NOT NULL,
  `customFieldId` int(11) NOT NULL,
  `subarea` varchar(256) COLLATE utf8_unicode_ci NOT NULL
) ENGINE=MyISAM AUTO_INCREMENT=43 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

CREATE TABLE IF NOT EXISTS `CustomValue` (
  `id` int(11) NOT NULL,
  `customFieldId` int(11) NOT NULL,
  `relatedId` int(11) NOT NULL,
  `fieldValue` text COLLATE utf8_unicode_ci,
  `createdAt` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP
) ENGINE=MyISAM AUTO_INCREMENT=86866 DEFAULT CHARSET=utf8 COLLATE=utf8_unicode_ci;

整个PHP代码在http://pastie.org/10737962

更新2

解释查询的输出

enter image description here

8 个答案:

答案 0 :(得分:12)

尝试使用这些内容来提高查询效果:

  • 在数据库结构中定义索引,并仅获取所需的列。
  • 请勿在选择查询中使用*。
  • 并且不要将ID放在User.id='51394'之类的引号中,而是User.id= 51394
  • 如果您使用引号提供ID,那么您的索引将无效。这种方法可以将查询性能提高20%。
  • 如果您使用ENGINE=MyISAM,则无法在数据库表之间定义索引,请将数据库引擎更改为ENGINE=InnoDB。并创建一些索引,如外键,全文索引。

答案 1 :(得分:12)

索引是你的朋友。

UPDATE User ... WHERE id = ... - 迫切需要一个ID索引,可能是PRIMARY KEY

同样适用于renameSource

SELECT * 
FROM `User` `t` 
WHERE `t`.`firstName`='Franck' 
  AND `t`.`lastName`='ALLEGAERT ' 
  AND `t`.`dateOfBirth`='1971-07-29' 
  AND (userType NOT IN ("1")) 
LIMIT 1;

需要INDEX(firstName, lastName, dateOfBirth);字段可以按任何顺序排列(在本例中)。

查看每个查询以查看其需要,然后将INDEX添加到表中。 Read my Cookbook on building indexes

答案 2 :(得分:5)

如果我理解,SELECT * FROM AdvanceBulkInsert的所有结果...您运行请求SELECT cf.*,而对于所有SELECT cf.*,您运行SELECT * FROM User

我认为问题在于你向基地发送了太多的请求。

我认为您应该只在一个大请求中合并所有选择请求。

为此:

然后在合并选择的所有结果上调用更新。

您应该逐个时间地查询您的请求,以查找哪些请求占用的时间最多,并且 你也应该use ANALYSE 找到请求的哪一部分需要时间。

修改

现在我看到了你的代码:

一些领导:

  • 是否为cf.customTypeId,cfv.customFieldId,cfsa.customFieldId,user编制索引。 dateOfBirth,用户。 firstName,user.lastName?

  • 如果你有一个使用CustomFieldSubArea的WHERE,那么你不需要做LEFT JOIN CustomFieldSubArea,只需要一个简单的JOIN CustomFieldSubArea。

  • 您将使用relatedId = 0大量启动查询2,也许您可​​以将结果保存在var?

  • 如果您不需要排序数据,请删除" ORDER BY cf.sortOrder,cf.label" 。 另外,在cf.sortOrder上添加索引,cf.label

答案 3 :(得分:3)

当您需要找出查询需要很长时间的原因时,您需要检查各个部分。正如您在问题Explain statement中所示,可以为您提供帮助。通常最重要的列是:

  • select_type - 这应该始终是简单的查询/子查询。相关的子查询会带来很多麻烦。幸运的是,你不能使用任何
  • 可能的键 - 此选择要按
  • 搜索的键
  • 行 - 由键/缓存和其他技术确定的候选行数。数字越小越好
  • 额外 - "使用"告诉你如何找到行,这是最有用的信息

查询分析

我会发布第一个和第三个查询的分析,但它们都是非常简单的查询。以下是查询的细分,为您提供麻烦:

EXPLAIN SELECT cf.*, ctyp.typeName, cfv.id as customId, cfv.customFieldId, 
   cfv.relatedId, cfv.fieldValue, cfv.createdAt 
FROM `CustomField` `cf` 
    INNER JOIN CustomType ctyp on ctyp.id = cf.customTypeId 
    LEFT OUTER JOIN CustomValue cfv on cf.id = cfv.customFieldId 
                and relatedId = 0 
    LEFT JOIN CustomFieldSubArea cfsa on cfsa.customFieldId = cf.id 
WHERE ((relatedTable = 'people' and enabled = '1') 
  AND (onCreate = '1')) 
  AND (cfsa.subarea='peoplebulkinsert') 
ORDER BY cf.sortOrder, cf.label
  • INNER JOIN ctyp.id上的CustomType ctyp = cf.customTypeId
  • LEFT OUTER JOIN cf.id = cfv.customFieldId 上的CustomValue cfv                 和 relatedId = 0
  • LEFT JOIN cfsa.customFieldId = cf.id
  • 上的CustomFieldSubArea cfsa
  • WHERE(( relatedTable =' people' and enabled =' 1')   AND( onCreate =' 1'))   AND( cfsa.subarea =' peoplebulkinsert')
  • ORDER BY cf.sortOrder cf.label

解决方案

让我解释一下上面的清单。 粗体列完全必须有索引。连接表是昂贵的操作,否则需要遍历两个表的所有行。如果在可连接列上创建索引,数据库引擎将找到更快更好的方法。这应该是任何数据库的通用做法

italic 列不是必需的索引,但是如果你有大量的行(20 000是大量的),你还应该有用于搜索的列的索引,它可能不会对处理速度产生这样的影响,但值得额外的时间。

所以你需要在theese专栏中添加标记

  • CustomType - id
  • CustomField - customTypeId,id,relatedTable,enabled,onCreate,sortOrder,label
  • CustomValue - customFieldId
  • CustomFieldSubArea - customFieldId,subarea

要验证结果,请在添加指标后再次运行explain语句(可能还有其他一些选择/插入/更新查询)。额外的专栏应该说"使用索引"和possible_keys列应列出使用的密钥(每个连接查询甚至两个或更多)。

附注:您的代码中存在一些拼写错误,您应该修复它们以防其他人需要处理您的代码:" reqruiteCount"作为表格列和" fileUplaod"作为引用代码中的数组索引。

答案 4 :(得分:1)

对于我的工作,我必须每天添加一个包含524列和10k记录的CSV。当我试图解析它并用php添加记录时,它太可怕了。

因此,我建议您查看有关LOAD DATA LOCAL INFILE

的文档

我复制/通过我自己的代码,但是让他适应你的需要

$dataload = 'LOAD DATA LOCAL INFILE "'.$filename.'"
                REPLACE
                INTO TABLE '.$this->csvTable.' CHARACTER SET "utf8"
                FIELDS TERMINATED BY "\t"
                IGNORE 1 LINES
            ';

$result = (bool)$this->db->query($dataload);

其中$ filename是CSV的本地路径(您可以使用dirname(__FILE__)获取它)

此SQL命令非常快(添加/更新所有CSV只需1或2秒)

编辑:阅读文档,但当然你需要在用户表上有一个uniq索引来“替换”作品。因此,您无需检查用户是否存在。而且您不需要使用php解析CSV文件。

答案 5 :(得分:0)

对于每条记录,您似乎有3次查询的可能性(概率?)。这3个查询将需要3次访问数据库(如果你使用yii存储yii对象中的记录,那么这可能会使事情变得更慢)。

您可以在名字/姓氏/ DOB和电子邮件地址上添加一个唯一的密钥吗?

如果是这样,您可以执行INSERT .... ON DUPLICATE KEY UPDATE。这会将它减少为每个记录的单个查询,从而大大加快了速度。

但是这种语法的最大优点是你可以一次插入/更新许多记录(我通常坚持使用大约250条记录),所以更少访问数据库。

你可以打开一个只传递记录的类,当你选择的记录数量时,它会插入。还要添加一个调用,在析构函数中插入记录以插入任何最终记录。

另一种选择是读取临时表中的所有内容,然后将其用作连接到用户表的源以执行更新/插入。这需要对索引进行一些努力,但是对临时表的批量加载很快,并且具有有用索引的更新将很快。使用它作为插入源也应该很快(如果排除已更新的记录)。

另一个问题似乎是您的以下查询,但不确定您执行此操作的位置。它似乎只需要执行一次,在这种情况下它可能并不重要。您尚未给出CustomType表的结构,但它已连接到Customfield,而字段customTypeId没有索引。因此,加入将是缓慢的。类似地,在CustomValue和CustomFieldSubArea连接基于customFieldId的连接,并且在该字段上都没有索引(希望是唯一索引,好像这些字段不是唯一的,您将获得大量返回的记录 - 每个可能的组合有1行)

SELECT cf.*, ctyp.typeName, cfv.id as customId, cfv.customFieldId, 
       cfv.relatedId, cfv.fieldValue, cfv.createdAt 
FROM `CustomField` `cf` 
    INNER JOIN CustomType ctyp on ctyp.id = cf.customTypeId 
    LEFT OUTER JOIN CustomValue cfv on cf.id = cfv.customFieldId 
                and relatedId = 0 
    LEFT JOIN CustomFieldSubArea cfsa on cfsa.customFieldId = cf.id 
WHERE ((relatedTable = 'people' and enabled = '1') 
  AND (onCreate = '1')) 
  AND (cfsa.subarea='peoplebulkinsert') 
ORDER BY cf.sortOrder, cf.label

答案 6 :(得分:0)

看到它你可以尝试减少查询并检查sql在线编译器检查时间段然后包含在项目下。

答案 7 :(得分:0)

始终在转化中进行批量导入

        $transaction = Yii::app()->db->beginTransaction();
        $curRow = 0;
        try
        {
            while (($peopleData = fgetcsv($handle, 10240, ",")) !== FALSE) {
            $curRow++;
            //process $peopleData
            //insert row
            //best to use INSERT ... ON DUPLICATE  KEY UPDATE
            // a = 1
            // b = 2;
            if ($curRow % 5000 == 0) {
               $transaction->commit();
               $transaction->beginTransaction();
            }
        }
        catch (Exception $ex)
        {
            $transaction->rollBack();
            $result = $e->getMessage();                    
        }
        //don't forget the remainder.
        $transaction->commit();

我看到通过简单地使用这种技术,导入程序加速了500%。我还看到了一个导入过程,它为每个行执行了600次查询(select,insert,update和show table结构的混合)。这项技术加快了30%的流程。