MYSQL 查询执行速度很慢
MYSQL query performs very slow
我开发了一个用户批量上传模块。有两种情况,当我批量上传 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'
如果用户不存在,插入新记录。
Table 引擎类型是 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
解释查询的输出
如果我理解,对于 SELECT * FROM AdvanceBulkInsert
的所有结果...您 运行 一个请求 SELECT cf.*
,对于所有 SELECT cf.*
,您 运行 SELECT * FROM User
我认为问题是您向基地发送了太多请求。
我认为您应该将所有 select 请求合并为一个大请求。
为此:
替换
SELECT * FROM AdvanceBulkInsert
由 EXISTS IN (SELECT * FROM AdvanceBulkInsert where ...)
或 JOIN
将 SELECT * FROM User
替换为 NOT EXISTS IN(SELECT * from User WHERE )
然后你调用所有合并结果的更新select。
你也应该一个一个地计算你的请求,找出哪个请求花费的时间最多,并且
你也应该 use ANALYSE
找到请求的哪一部分需要时间。
编辑:
现在我看到了你的代码:
一些线索:
您是否为 cf.customTypeId、cfv.customFieldId、cfsa.customFieldId 用户编制了索引。出生日期,用户。名字,user.lastName ?
如果您有使用 CustomFieldSubArea 的 WHERE,则不需要执行 LEFT JOIN CustomFieldSubArea,一个简单的 JOIN CustomFieldSubArea 就足够了。
您将使用 relatedId = 0 启动查询 2 很多时间,也许您可以将结果保存在 var 中?
如果您不需要排序数据,请删除 "ORDER BY cf.sortOrder, cf.label" 。 否则,在 cf.sortOrder、cf.label
上添加索引
尝试这些方法来提高查询性能:
- 在您的数据库结构中定义索引,并只获取您需要的列。
- 不要在 select 查询中使用 *。
- 并且不要将 ID 放在
User.id='51394'
之类的引号中,而是 User.id= 51394
.
- 如果您在引号中给出 ID,那么您的索引将不起作用。这种方法可以将查询性能提高 20%。
- 如果您正在使用
ENGINE=MyISAM
,则您无法在数据库 table 之间定义索引,请将数据库引擎更改为 ENGINE=InnoDB
。并创建一些索引,如外键、全文索引。
索引是你的朋友。
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
添加到 table。 Read my Cookbook on building indexes.
您似乎有可能(概率?)对每条记录进行 3 次查询。这 3 个查询将需要 3 次访问数据库(如果您使用 yii 将记录存储在 yii 对象中,那么这可能会进一步降低速度)。
你能在名字/姓氏/出生日期和电子邮件地址上添加一个唯一键吗?
如果是这样,您只需执行插入....在重复密钥更新时即可。这会将其减少为针对每条记录的单个查询,从而大大加快速度。
但这种语法的最大优点是您可以一次插入/更新多条记录(我通常坚持大约 250 条),因此访问数据库的次数更少。
您可以打开一个 class,您只需将记录传递给它,当记录数达到您的选择时,它就会插入。还添加调用以在析构函数中插入记录以插入任何最终记录。
另一种选择是将所有内容读入临时文件 table,然后将其用作加入您的用户 table 的源以进行更新/插入。这需要对索引做一些努力,但是批量加载到临时 table 很快,并且使用有用的索引进行更新会很快。使用它作为插入的来源也应该很快(如果您排除已经更新的记录)。
另一个问题似乎是您的以下查询,但不确定您在哪里执行此查询。它似乎只需要执行一次,在这种情况下可能无关紧要。您没有给出 CustomType table 的结构,但它已连接到 Customfield 并且字段 customTypeId 没有索引。因此,加入会很慢。类似地,在基于 customFieldId 的 CustomValue 和 CustomFieldSubArea 连接上,两者都没有该字段的索引(希望是唯一索引,就好像这些字段不是唯一的一样,您将返回大量记录 - 每个可能的组合 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
看到它你可以尝试减少查询并检查sql在线编译器检查时间段然后包含在项目下。
当您需要找出查询耗时较长的原因时,您需要检查各个部分。正如您在问题 Explain statement 中所显示的那样,可以为您提供很大帮助。通常最重要的列是:
- select_type - 这应该总是简单的 query/subquery。相关的子查询给了很多麻烦。幸运的是你没有使用任何
- 可能的键 - 这是什么键 select 将通过
搜索
- rows - 有多少候选行由 keys/cache 和其他技术确定。数字越小越好
- Extra - "using" 告诉你究竟是如何找到这些行的,这是最有用的信息
查询分析
我会为第一个和第三个查询发布分析,但它们都是非常简单的查询。以下是给您带来麻烦的查询的细分:
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 CustomType ctyp on ctyp.id = cf.customTypeId
- cf.id = cfv.customFieldId 上的左外连接自定义值 cfv
和 relatedId = 0
- LEFT JOIN CustomFieldSubArea cfsa on cfsa.customFieldId = cf.id
- WHERE ((relatedTable = 'people' and enabled = '1')
AND (onCreate = '1'))
AND (cfsa.subarea='peoplebulkinsert')
- 按 cf.sortOrder、cf.label
排序
解决方案
让我解释一下上面的列表。 Bold 列完全必须有索引。加入 tables 是一个昂贵的操作,否则需要遍历两个 tables 的所有行。如果您在可连接的列上建立索引,数据库引擎会找到更快更好的方法。这应该是任何数据库的常见做法
italic 列不是必须有索引,但是如果你有大量的行(20 000 是大量的)你也应该在你想要的列上有索引用于搜索,它可能不会对处理速度产生如此大的影响,但值得多花点时间。
因此您需要为这些列添加索引
- 自定义类型 - id
- CustomField - customTypeId、id、relatedTable、enabled、onCreate、sortOrder、label
- CustomValue - customFieldId
- CustomFieldSubArea - customFieldId,子区域
要验证结果,请在添加索引后再次尝试 运行 explain 语句(可能还有一些其他 select/insert/update 查询)。额外的列应该像 "Using Index" 和 possible_keys 列应该列出使用的键(每个连接查询甚至两个或更多)。
旁注:您的代码中有一些拼写错误,您应该修复它们以防其他人也需要处理您的代码:"reqruiteCount" 作为 table 列和 "fileUplaod"作为您引用代码中的数组索引。
对于我的工作,我必须每天添加一个包含 524 列和 10k 记录的 CSV。当我尝试解析它并使用 php 添加记录时,这太可怕了。
因此,我建议您查看有关 LOAD DATA LOCAL INFILE
的文档
我copy/past以我自己的代码为例,但根据您的需要调整他
$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 命令非常快(对于 add/update 所有 CSV 只需 1 或 2 秒)
编辑:阅读文档,但当然你需要在你的用户 table 上有一个 uniq 索引才能 "replace" 工作。因此,您无需检查用户是否存在。而且您不需要使用 php.
解析 CSV 文件
始终在交易中进行批量导入
$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%。我还看到了一个导入过程,它为 each 行执行了 600 次查询(select、插入、更新和显示 table 结构的混合)。这种技术使过程加快了 30%。
我开发了一个用户批量上传模块。有两种情况,当我批量上传 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'
如果用户不存在,插入新记录。
Table 引擎类型是 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
解释查询的输出
如果我理解,对于 SELECT * FROM AdvanceBulkInsert
的所有结果...您 运行 一个请求 SELECT cf.*
,对于所有 SELECT cf.*
,您 运行 SELECT * FROM User
我认为问题是您向基地发送了太多请求。
我认为您应该将所有 select 请求合并为一个大请求。
为此:
替换
SELECT * FROM AdvanceBulkInsert
由EXISTS IN (SELECT * FROM AdvanceBulkInsert where ...)
或JOIN
将
SELECT * FROM User
替换为NOT EXISTS IN(SELECT * from User WHERE )
然后你调用所有合并结果的更新select。
你也应该一个一个地计算你的请求,找出哪个请求花费的时间最多,并且 你也应该 use ANALYSE 找到请求的哪一部分需要时间。
编辑:
现在我看到了你的代码:
一些线索:
您是否为 cf.customTypeId、cfv.customFieldId、cfsa.customFieldId 用户编制了索引。出生日期,用户。名字,user.lastName ?
如果您有使用 CustomFieldSubArea 的 WHERE,则不需要执行 LEFT JOIN CustomFieldSubArea,一个简单的 JOIN CustomFieldSubArea 就足够了。
您将使用 relatedId = 0 启动查询 2 很多时间,也许您可以将结果保存在 var 中?
如果您不需要排序数据,请删除 "ORDER BY cf.sortOrder, cf.label" 。 否则,在 cf.sortOrder、cf.label
上添加索引
尝试这些方法来提高查询性能:
- 在您的数据库结构中定义索引,并只获取您需要的列。
- 不要在 select 查询中使用 *。
- 并且不要将 ID 放在
User.id='51394'
之类的引号中,而是User.id= 51394
. - 如果您在引号中给出 ID,那么您的索引将不起作用。这种方法可以将查询性能提高 20%。
- 如果您正在使用
ENGINE=MyISAM
,则您无法在数据库 table 之间定义索引,请将数据库引擎更改为ENGINE=InnoDB
。并创建一些索引,如外键、全文索引。
索引是你的朋友。
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
添加到 table。 Read my Cookbook on building indexes.
您似乎有可能(概率?)对每条记录进行 3 次查询。这 3 个查询将需要 3 次访问数据库(如果您使用 yii 将记录存储在 yii 对象中,那么这可能会进一步降低速度)。
你能在名字/姓氏/出生日期和电子邮件地址上添加一个唯一键吗?
如果是这样,您只需执行插入....在重复密钥更新时即可。这会将其减少为针对每条记录的单个查询,从而大大加快速度。
但这种语法的最大优点是您可以一次插入/更新多条记录(我通常坚持大约 250 条),因此访问数据库的次数更少。
您可以打开一个 class,您只需将记录传递给它,当记录数达到您的选择时,它就会插入。还添加调用以在析构函数中插入记录以插入任何最终记录。
另一种选择是将所有内容读入临时文件 table,然后将其用作加入您的用户 table 的源以进行更新/插入。这需要对索引做一些努力,但是批量加载到临时 table 很快,并且使用有用的索引进行更新会很快。使用它作为插入的来源也应该很快(如果您排除已经更新的记录)。
另一个问题似乎是您的以下查询,但不确定您在哪里执行此查询。它似乎只需要执行一次,在这种情况下可能无关紧要。您没有给出 CustomType table 的结构,但它已连接到 Customfield 并且字段 customTypeId 没有索引。因此,加入会很慢。类似地,在基于 customFieldId 的 CustomValue 和 CustomFieldSubArea 连接上,两者都没有该字段的索引(希望是唯一索引,就好像这些字段不是唯一的一样,您将返回大量记录 - 每个可能的组合 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
看到它你可以尝试减少查询并检查sql在线编译器检查时间段然后包含在项目下。
当您需要找出查询耗时较长的原因时,您需要检查各个部分。正如您在问题 Explain statement 中所显示的那样,可以为您提供很大帮助。通常最重要的列是:
- select_type - 这应该总是简单的 query/subquery。相关的子查询给了很多麻烦。幸运的是你没有使用任何
- 可能的键 - 这是什么键 select 将通过 搜索
- rows - 有多少候选行由 keys/cache 和其他技术确定。数字越小越好
- Extra - "using" 告诉你究竟是如何找到这些行的,这是最有用的信息
查询分析
我会为第一个和第三个查询发布分析,但它们都是非常简单的查询。以下是给您带来麻烦的查询的细分:
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 CustomType ctyp on ctyp.id = cf.customTypeId
- cf.id = cfv.customFieldId 上的左外连接自定义值 cfv 和 relatedId = 0
- LEFT JOIN CustomFieldSubArea cfsa on cfsa.customFieldId = cf.id
- WHERE ((relatedTable = 'people' and enabled = '1') AND (onCreate = '1')) AND (cfsa.subarea='peoplebulkinsert')
- 按 cf.sortOrder、cf.label 排序
解决方案
让我解释一下上面的列表。 Bold 列完全必须有索引。加入 tables 是一个昂贵的操作,否则需要遍历两个 tables 的所有行。如果您在可连接的列上建立索引,数据库引擎会找到更快更好的方法。这应该是任何数据库的常见做法
italic 列不是必须有索引,但是如果你有大量的行(20 000 是大量的)你也应该在你想要的列上有索引用于搜索,它可能不会对处理速度产生如此大的影响,但值得多花点时间。
因此您需要为这些列添加索引
- 自定义类型 - id
- CustomField - customTypeId、id、relatedTable、enabled、onCreate、sortOrder、label
- CustomValue - customFieldId
- CustomFieldSubArea - customFieldId,子区域
要验证结果,请在添加索引后再次尝试 运行 explain 语句(可能还有一些其他 select/insert/update 查询)。额外的列应该像 "Using Index" 和 possible_keys 列应该列出使用的键(每个连接查询甚至两个或更多)。
旁注:您的代码中有一些拼写错误,您应该修复它们以防其他人也需要处理您的代码:"reqruiteCount" 作为 table 列和 "fileUplaod"作为您引用代码中的数组索引。
对于我的工作,我必须每天添加一个包含 524 列和 10k 记录的 CSV。当我尝试解析它并使用 php 添加记录时,这太可怕了。
因此,我建议您查看有关 LOAD DATA LOCAL INFILE
的文档我copy/past以我自己的代码为例,但根据您的需要调整他
$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 命令非常快(对于 add/update 所有 CSV 只需 1 或 2 秒)
编辑:阅读文档,但当然你需要在你的用户 table 上有一个 uniq 索引才能 "replace" 工作。因此,您无需检查用户是否存在。而且您不需要使用 php.
解析 CSV 文件始终在交易中进行批量导入
$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%。我还看到了一个导入过程,它为 each 行执行了 600 次查询(select、插入、更新和显示 table 结构的混合)。这种技术使过程加快了 30%。