Я разработал модуль массовой загрузки пользователей. Есть 2 ситуации, когда я делаю массовую загрузку 20 000 записей, когда база данных имеет ноль записей. Это занимает около 5 часов. Но когда в базе уже около 30 000 записей, загрузка происходит очень-очень медленно. Загрузка около 20 000 записей занимает около 11 часов. Я просто читаю файл CSV через fgetcsv
метод.
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 Framework)
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
Объясните вывод запроса
Индексы твой друг.
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
к столу. Прочитайте мою Поваренную книгу по созданию индексов.
Попробуйте эти вещи, чтобы увеличить производительность вашего запроса:
User.id='51394'
вместо User.id= 51394
,ENGINE=MyISAM
тогда вы не сможете определить индексирование между таблицей базы данных, измените ядро базы данных на ENGINE=InnoDB
, И создайте некоторую индексацию, например внешние ключи, полнотекстовую индексацию.Если я понимаю, для всех результат SELECT * FROM AdvanceBulkInsert
… вы запускаете запрос SELECT cf.*
и для всех SELECT cf.*
запускаешь SELECT * FROM User
Я думаю, проблема в том, что вы посылаете слишком много запросов на базу.
Я думаю, что вы должны объединить все ваши запросы на выборку только в одном большом запросе.
Для этого:
заменить
SELECT * FROM AdvanceBulkInsert
по EXISTS IN (SELECT * FROM AdvanceBulkInsert where ...)
или JOIN
заменить SELECT * FROM User
по NOT EXISTS IN(SELECT * from User WHERE )
Затем вы вызываете обновление для всех результатов объединенного выбора.
Вы также должны один за другим запросить, чтобы выяснить, какой из этих запросов занимает больше всего времени, и
ты тоже должен использовать анализ
чтобы найти, какая часть запроса займет время.
Редактировать:
Теперь я вижу ваш код:
Некоторые приводят:
у вас есть индекс для cf.customTypeId, cfv.customFieldId, cfsa.customFieldId, пользователь. dateOfBirth, пользователь. firstName, user.lastName?
вам не нужно делать LEFT JOIN CustomFieldSubArea, если у вас есть WHERE, который использует CustomFieldSubArea, достаточно простого JOIN CustomFieldSubArea.
Вы будете часто запускать запрос 2 с relatedId = 0, может быть, вы можете сохранить результат в var?
если вам не нужны отсортированные данные, удалите «ORDER BY cf.sortOrder, cf.label». Остальное, добавить индекс на cf.sortOrder, cf.label
Когда вам нужно выяснить, почему запрос занимает много времени, вам нужно проверить отдельные детали. Как вы показали в вопросе Объясните заявление может вам очень помочь Обычно наиболее важные столбцы:
Я бы опубликовал аналитику для 1-го и 3-го запросов, но они оба довольно простые. Вот разбивка для запроса, который доставляет вам неприятности:
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
Позвольте мне объяснить выше список. Смелый столбцы полностью должны иметь индекс. Присоединение таблиц — это дорогостоящая операция, которая в противном случае должна проходить через все строки обеих таблиц. Если вы создадите индекс для соединяемых столбцов, механизм БД найдет гораздо более быстрый и лучший способ сделать это. Это должно быть обычной практикой для любой базы данных
курсивный столбцы не обязательно должны иметь индекс, но если у вас большое количество строк (20 000 — большое количество), у вас также должен быть индекс для столбцов, которые вы используете для поиска, это может не иметь такого влияния на скорость обработки, но стоит лишнее время.
Таким образом, вам нужно добавить указатели в эти столбцы
Чтобы проверить результаты, попробуйте снова выполнить оператор объяснения после добавления признаков (и, возможно, нескольких других запросов выбора / вставки / обновления). В дополнительном столбце должно быть написано что-то вроде «Using Index», а в столбце возможных_ключах должны быть указаны используемые ключи (даже два или более на запрос объединения).
Примечание: у вас есть некоторые опечатки в вашем коде, вы должны исправить их на тот случай, если кому-то тоже нужно будет работать с вашим кодом: reqruiteCount в качестве столбца таблицы и fileUplaod в качестве индекса массива в ссылочном коде.
Для моей работы я должен ежедневно добавлять один CSV с 524 столбцами и 10 тыс. Записей. Когда я попытался разобрать его и добавить запись с php, это было ужасно.
Итак, предлагаю вам ознакомиться с документацией о НАГРУЗКА ДАННЫХ ЛОКАЛЬНЫЙ ИНФИЛЬ
Например, я копирую / пропускаю свой собственный код, но адаптирую его к вашим потребностям
$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 очень быстрая (всего 1 или 2 секунды для добавления / обновления всех CSV)
РЕДАКТИРОВАТЬ: прочитайте документацию, но, конечно, вам нужно иметь индекс uniq в вашей пользовательской таблице для «замены» работ. Таким образом, вам не нужно проверять, существует ли пользователь или нет. И вам не нужно анализировать файл CSV с php.
Похоже, у вас есть возможность (вероятность?) 3 запроса для каждой записи. Эти три запроса потребуют трех поездок в базу данных (и если вы используете yii для хранения записей в объектах yii, это может еще больше замедлить работу).
Можете ли вы добавить уникальный ключ на имя / фамилию / DOB и один на адрес электронной почты?
Если это так, вы можете просто сделать INSERT …. ON DUPLICATE KEY UPDATE. Это уменьшит его до одного запроса для каждой записи, что значительно ускорит процесс.
Но большим преимуществом этого синтаксиса является то, что вы можете вставлять / обновлять много записей одновременно (я обычно придерживаюсь около 250), так что даже меньше посещений базы данных.
Вы можете создать класс, которому вы просто передаете записи и который выполняет вставку, когда число записей достигает вашего выбора. Также добавьте в вызов, чтобы вставить записи в деструктор, чтобы вставить любые окончательные записи.
Другой вариант — прочитать все во временную таблицу, а затем использовать ее в качестве источника для присоединения к вашей пользовательской таблице для выполнения обновлений / вставок. Это потребует немного усилий с индексами, но массовая загрузка во временную таблицу будет быстрой, а обновление с использованием полезных индексов будет быстрым. Использование его в качестве источника для вставок также должно быть быстрым (если исключить уже обновленные записи).
Другая проблема, кажется, ваш следующий запрос, но не уверен, где вы выполняете это. По-видимому, его нужно выполнить только один раз, и в этом случае это может не иметь большого значения. Вы не указали структуру таблицы CustomType, но она присоединена к полю 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
проследите за тем, чтобы вы могли попытаться уменьшить запрос и проверить с помощью онлайн-компилятора sql, проверить период времени, а затем включить его в проект.
Всегда делайте массовый импорт внутри перевода
$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 запросов (смесь выбора, вставки, обновления и отображения структуры таблицы) для каждый строка. Этот метод ускорил процесс на 30%.