экспорт использования памяти из базы данных в csv в php - PullRequest
1 голос
/ 15 марта 2011

Мне нужно экспортировать данные из MySQL в CSV. Я должен выбрать данные из нескольких таблиц, поместив их в массивы, затем обработать их и вернуть в браузер как .csv. Я заметил, что массивы потребляют огромное количество строк. Например, я импортировал файл .csv в базу данных размером 1.8M, затем я пытаюсь экспортировать эти данные из базы данных в файл .csv. Memory_get_peak_usage () показывает более 128 МБ для хранения массивов с данными.

Например, этот небольшой массив занимает более 700 байт:

$startMemory = memory_get_usage();  
        //get constant fields of the subscriber
        $data = array(array('subscriber_id' => 1315444, 'email_address' => 'test0@gmail.com',
                            'first_name' => 'Michael', 'last_name' => 'Allen'));
        echo memory_get_usage() - $startMemory;

Таким образом, для экспорта даже нескольких мегабайт данных требуется сотни мегабайт памяти в сценарии php. Есть ли способ решить эту проблему? Таблицы:

    CREATE TABLE `subscribers` (
     `subscriber_id` int(10) unsigned NOT NULL auto_increment,
     `list_id` int(10) unsigned NOT NULL,
     `account_id` int(10) unsigned NOT NULL,
     `email_address` varchar(100) collate utf8_unicode_ci NOT NULL,
     `first_name` varchar(50) collate utf8_unicode_ci NOT NULL default '',
     `last_name` varchar(50) collate utf8_unicode_ci NOT NULL default '',
     `ip` int(10) unsigned default NULL COMMENT '\nThe ip address of the subscriber that we can get when he opens the \nthe email or subscribe using subsribe form.\nTheoretically it can be used to segment by Location (which is not correct if someone uses proxy).',
     `preferred_format` tinyint(4) NOT NULL default '0' COMMENT 'Preferred format of \n0 - HTML, \n1 -Text,\n2 - Mobile',
     `state` tinyint(4) NOT NULL default '1' COMMENT '1 - subscribed\n2 - unsubscribed\n3 - cleaned\n4 - not confirmed, it means the user subscribed but has not confirmed it yet.\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n\n',
     `cause_of_cleaning` tinyint(4) NOT NULL default '0' COMMENT '\nThis field is the cause of moving the subscriber to the \n0 - not used\n1 - spam complaint\n2 - hard bounce\n3 - several soft bounces',
     `date_added` datetime NOT NULL COMMENT 'The data when the subscriber was added. I suppose this field can be used in the conditions forming the segment',
     `last_changed` datetime NOT NULL,
     PRIMARY KEY  (`subscriber_id`),
     UNIQUE KEY `email_list_id` (`email_address`,`list_id`),
     KEY `FK_list_id` (`list_id`),
     CONSTRAINT `FK_list_id` FOREIGN KEY (`list_id`) REFERENCES `lists` (`list_id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB 
    CREATE TABLE `subscribers_multivalued` (
     `id` int(10) unsigned NOT NULL auto_increment,
     `subscriber_id` int(10) unsigned NOT NULL,
     `field_id` int(10) unsigned NOT NULL,
     `value` varchar(100) collate utf8_unicode_ci NOT NULL,
     `account_id` int(10) unsigned NOT NULL COMMENT '\nThe identifier of the account',
     PRIMARY KEY  (`id`),
     KEY `subscriber_fk` (`subscriber_id`),
     KEY `field_fk` (`field_id`),
     CONSTRAINT `field_fk_string_multivalued` FOREIGN KEY (`field_id`) REFERENCES `custom_fields` (`field_id`) ON DELETE CASCADE ON UPDATE CASCADE,
     CONSTRAINT `subscriber_fk_multivalued` FOREIGN KEY (`subscriber_id`) REFERENCES `subscribers` (`subscriber_id`) ON DELETE CASCADE ON UPDATE CASCADE
    ) ENGINE=InnoDB
CREATE TABLE `subscribers_custom_data_string` (
 `subscriber_id` int(10) unsigned NOT NULL,
 `field_id` int(10) unsigned NOT NULL,
 `value` varchar(255) collate utf8_unicode_ci NOT NULL,
 `account_id` int(10) unsigned NOT NULL COMMENT '\nThe identifier of the account',
 PRIMARY KEY  (`subscriber_id`,`field_id`),
 KEY `subscriber_fk` (`subscriber_id`),
 KEY `field_fk` (`field_id`),
 CONSTRAINT `field_fk_string` FOREIGN KEY (`field_id`) REFERENCES `custom_fields` (`field_id`) ON DELETE CASCADE ON UPDATE CASCADE,
 CONSTRAINT `subscriber_fk_string` FOREIGN KEY (`subscriber_id`) REFERENCES `subscribers` (`subscriber_id`) ON DELETE CASCADE ON UPDATE CASCADE
) 

Существуют другие таблицы для полей, аналогичные таблице со строками для чисел, дат. Для них первичный ключ - subscriber_id, field_id.

При сбое запроса (например, у нас есть несколько настраиваемых полей):

SELECT subscribers. email_address, subscribers. first_name, subscribers. last_name, GROUP_CONCAT (t1.value SEPARATOR '|') AS Цвета, GROUP_CONCAT (t2.value SEPARATOR '|') AS Языки ОТ subscribers LEFT JOIN subscribers_multivalued AS t1 ON подписчики.subscriber_id = t1.subscriber_id И t1.field_id = 112 LEFT JOIN subscribers_multivalued AS t2 ON подписчики.subscriber_id = t2.subscriber_id AND t2.field_id = 111 ГДЕ (list_id = 40) ГРУППА ПО subscribers. email_address, subscribers. first_name, subscribers. last_name

Это вернуло бы это:

test1000@gmail.com Мишель Буш Красный | Красный | Синий | Синий Английский | Испанский | Английский | Испанский вместо test1000@gmail.com Мишель Буш Красный | Синий Английский | испанский

Спасибо за любую информацию.

Ответы [ 5 ]

2 голосов
/ 15 марта 2011

Используя только две таблицы:

Ваш оригинальный запрос:

SELECT subscribers.email_address, 
       subscribers.first_name, 
       subscribers.last_name, 
       t1.value AS Languages 
  FROM subscribers 
  LEFT JOIN (SELECT subscriber_id, 
                    field_id, 
                    GROUP_CONCAT(value SEPARATOR '|') AS value 
               FROM subscribers_multivalued 
              WHERE field_id=37 
              GROUP BY subscriber_id, field_id
            ) AS t1 
         ON subscribers.subscriber_id=t1.subscriber_id 
        AND t1.field_id=37 
 WHERE (list_id=49) 
   AND (state=1)

дает план объяснения:

id  select_type  table                    type  possible_keys  key         key_len  ref    rows  Extra
1   PRIMARY      subscribers              ref   FK_list_id     FK_list_id  4        const  2     Using where
1   PRIMARY      <derived2>               ALL   NULL           NULL        NULL     NULL   5      
2   DERIVED      subscribers_multivalued  ALL   field_fk       field_fk    4               11    Using filesort

Мое предложение о присоединении:

SELECT subscribers.email_address, 
       subscribers.first_name, 
       subscribers.last_name, 
       GROUP_CONCAT(t1.value SEPARATOR '|') AS Languages 
  FROM subscribers 
  LEFT JOIN subscribers_multivalued t1 
         ON subscribers.subscriber_id=t1.subscriber_id 
        AND t1.field_id=37 
 WHERE (list_id=49) 
   AND (state=1)
 GROUP BY subscribers.email_address, 
          subscribers.first_name, 
          subscribers.last_name

дает план объяснения:

id  select_type  table        type  possible_keys           key            key_len  ref                             rows  Extra
1   SIMPLE       subscribers  ref   FK_list_id              FK_list_id     4        const                           2     Using where; Using filesort
1   SIMPLE       t1           ref   subscriber_fk,field_fk  subscriber_fk  4        test.subscribers.subscriber_id  1      

Хотя я заполнил эти две таблицы только очень небольшим объемом данных, это говорит о том, что моя версия запроса будет выполняться более эффективно для базы данных, поскольку она не использует производную таблицу, которую генерирует ваш запрос.

Другие таблицы могут быть связаны в запросе почти таким же образом, и весь результат буферизуется непосредственно в файл CSV, а не анализируется далее с помощью PHP.

Это должно дать вам более быстрый и эффективный запуск.

EDIT

SELECT subscribers.email_address, 
       subscribers.first_name, 
       subscribers.last_name, 
       GROUP_CONCAT(DISTINCT t1.value SEPARATOR '|') AS Colors, 
       GROUP_CONCAT(DISTINCT t2.value SEPARATOR '|') AS Languages 
  FROM subscribers 
  LEFT JOIN subscribers_multivalued AS t1 
         ON subscribers.subscriber_id=t1.subscriber_id 
        AND t1.field_id=112 
  LEFT JOIN subscribers_multivalued AS t2 
    ON subscribers.subscriber_id=t2.subscriber_id 
   AND t2.field_id=37 
 WHERE (list_id=49) 
 GROUP BY subscribers.email_address, 
          subscribers.first_name, 
          subscribers.last_name

Обратите внимание на использование DISTINCT в функции GROUP_CONCAT ()

2 голосов
/ 15 марта 2011

Если это возможно с помощью вашей бизнес-логики, вы можете выполнить преобразования в mysql и сделать

SELECT * from table INTO OUTFILE 'file_name.csv'

с теми же параметрами, что и LOAD DATA INFILE, файл, в который вы пишете, не должен существовать.

1 голос
/ 15 марта 2011

Соберите код в единую функцию, которая считывает данные для строки N th , обрабатывает их, выводит эту строку без буферизации вывода и удаляет все временные данные. Вызовите эту функцию несколько раз. Это должно уменьшить использование вашей памяти только до того, что необходимо в данной строке, вместо того, чтобы обрабатывать все строки вместе.

Как только это будет сделано, вы можете расширить его для одновременного считывания произвольного числа строк, чтобы настроить использование памяти и компромисс производительности, что влечет за собой.

0 голосов
/ 04 октября 2012

Таким образом, для экспорта даже нескольких мегабайт данных требуется сотни мегабайт памяти в сценарии php.Есть ли способ решить эту проблему?

Поскольку другой пост с таким же вопросом помечен как дубликат этого, я дам ответ на эту часть вашеговопрос без учета всех других конкретных деталей, которые вы задали.

Обработка строк в PHP

Чтобы уменьшить объем памяти, необходимый PHP в любое время, вы можете извлекать строки изСервер MySQL, по одному, и передавайте их клиентскому браузеру (или некоторому файлу на сервере) без буферизации.

Чтобы извлекать строки по одной, добавьте MYSQLI_USE_RESULT в качестве параметра resultmode кmysqli::query вызов для перебора результатов по одной строке за раз, без передачи их всех в PHP в одной связке.См. Также документацию для mysqli::use-result.

Убедитесь, что вы не используете Буферизацию вывода PHP , так что вам не понадобится память для всего документа,Если вы записываете содержимое в файл на сервере (например, используя fwrite), вы можете позже передать этот файл клиенту, используя readfile или аналогичный.Вы можете использовать файл в качестве кэша, если вам нужно передать один и тот же результат несколько раз.

MySQL запись в файл

Если у вас есть привилегия FILE , тогда вы можете выполнить запрос SELECT … INTO OUTFILE …, чтобы сервер MySQL записал результат непосредственно в какой-либо (временный) файл на сервере.

Затем можно либо перенаправить клиент для этого файла, чтобы веб-сервер служил им как простой статический файл, или вы можете передать содержимое клиенту самостоятельно, используя readfile.Последнее имеет то преимущество, что вы можете удалить файл сразу после того, как он был передан клиенту, в случаях, когда вы знаете, что он вам больше не понадобится.

0 голосов
/ 17 марта 2011

Похоже, что для предотвращения избыточной группировки можно использовать DISTINCT в статистической функции GROUP_CONCAT:

SELECT `subscribers`.`email_address`, `subscribers`.`first_name`, `subscribers`.`last_name`,
GROUP_CONCAT(DISTINCT t1.value SEPARATOR '|') AS Colors, GROUP_CONCAT(DISTINCT t2.value SEPARATOR '|') AS Languages
FROM `subscribers`
LEFT JOIN `subscribers_multivalued` AS `t1` ON subscribers.subscriber_id=t1.subscriber_id AND t1.field_id=49
LEFT JOIN `subscribers_multivalued` AS `t2` ON subscribers.subscriber_id=t2.subscriber_id AND t2.field_id=48
WHERE (list_id=63)
GROUP BY `subscribers`.`email_address`, `subscribers`.`first_name`, `subscribers`.`last_name`
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...