Возникла задача еженедельного обновления справочника в БД из внешнего присылаемого файла. Справочник оказался не маленьким, примерно 1150000 записей, размер csv файла составляет 26МБ.
На первых шагах, для исполнения задания, было принято самое простое решение, а именно, чтение строки из файла, её преобразование и загрузка в БД.
Проблема возникла в том, что после первой загрузки, удалять данные нельзя поскольку на них могут ссылаться из других таблиц, а значит, что при повторной загрузке, старые данные должны быть обновлены, а новые добавлены.
Для решения данной проблемы была разработана логика по определению того, что необходимо сделать, обновить запись или вставить новую. Как результат всех стараний время работы процедуры составило около 6 часов работы, это примерно по 3500 записей в минуту. Данный результат абсолютно не устраивал и пришлось искать решение.
Пшеничный Николай Юрьевич посоветовал использовать BulkInsert, было обещано многократное ускорение.
Опишу свой опыт тестирования технологии.
Поскольку ранее я ни разу не использовал BulkInsert, было принято решение попробовать всё на локально поднятой тестовой базе.
1. Развёртывание тестовой БД
Прежде всего стоит сказать, что я всё выполнял под windowsXP и у меня уже были установлены FireBird 1,5 и IBExpert.
В каталоге с установленным сервером БД необходимо запустить утилиту isql.exe
..\firebird_1_5\bin\isql.exe
Появиться окно с приглашением к действию.
SQL>
Для создания новой базы данных необходимо выполнить следующую команду:
SQL> CREATE DATABASE 'C:\testDB\TestDB.fdb' user 'sysdba' password 'masterkey';Данная команда создаст файл базы данных TestDB.fdb в каталоге C:\testDB\.
Теперь в БД необходимо создать таблицу где будет храниться новый справочник. Первым делом необходимо подключиться к новой БД, выполнив команду.
SQL> CONNECT 'C:\testDB\TestDB.fdb' user 'sysdba' password 'masterkey';После подключения к БД нужно выполнить команду:
SQL> CREATE TABLE testTable (TestTable_ID BigInt, ordinal varchar(40), opendate date, closedate date);Теперь в базе TestDB существует таблица testTable на которой будут происходить все эксперименты.
Все остальные настройки я проводил через IBExpert.
Необходимо зарегистрировать БД в IBExpert. Вот с такими настройками:
Для создания ключей на поле TestTable_ID в таблице testTable необходимо:
- открыть таблицу;
- выбрать закладку "ограничения";
- выбрать вкладку "первичный ключ";
- нажать правую кнопку мыши и выбрать пункт меню "Новый первичный ключ" кнопка "insert".
В появившейся строке в графе Поле нужно выбрать поле по которому будет создан ключ.
Таблица готова к началу тестов.
2. Работа с внешними таблицами
Для того чтобы включить возможность подключения внешних таблиц нужно изменить файл настроек:..\firebird\firebird.conf
В файле необходимо найти строку с текстом ExternalFileAccess убрать символ комментария и присвоить значение ExternalFileAccess = Full.
Далее для создание внешней таблицы необходимо выполнить следующую команду:
create table ext1 external 'C:\testDB\test_right.csv' -- C:\testDB\test_right.csv - это путь до файла внешней таблицы. Если его нет система создаст файл, если есть попытается загрузить из него данные ( ord varchar(11), -- 1ое поле внешней таблицы, 11 символов текст ope varchar(10), -- дата открытия записи, поскольку FireBird хранит дату с 01.01.0001 то во временную таблицу проще записать дату в текст. clo varchar(10) -- 3е поле дата закрытия записи id bigint -- id записи );Созданную внешнюю таблицу можно просматривать через IBExpert, как обычную таблицу.
Для того чтобы вставить данные в таблицу нужно использовать следующую команду:
insert into ext1 (id, ord, ope, clo) values (1234567890, '01234567891', '01.01.1900', '01.01.2100')Теперь в файле C:\testDB\test_right.csv будет записана 1 запись.
Чтобы освободить файл необходимо удалить внешнюю таблицу:
drop table ext1;
Содержимое файл будет выглядить примерно вот так:
Что будет означать:
2 байта на длину, 11 байт текста, 1 байт дополнения, 2 байта на длину, 10 байт текста, 2 байта на длину, 10 байт текста, 8 байт на длинное число.
Для текстовых полей сначала идёт длина строки в 2 байта, затем текст, длина текста будет дополнена до чётного числа байт.
3. Формирование файла внешней таблицы
Добрые люди уже позаботились о создании класса, который позволит записать простые данные в файл в требуемый формат. Посмотреть можно здесь.
Вставка в БД выполняется по следующему алгоритму.
- Удаляем все ключи;
- Удаляем все записи из Таблицы;
- Подключаем внешнюю Таблицу;
- Выполняем перекачку данных из внешней Таблицы в обычную;
- Восстанавливаем ключи;
- Удаляем внешнюю таблицу.
Далее приведу пример скрипта который использовался для тестирования.
SET NAMES WIN1251; CONNECT 'C:\testDB\TestDB.fdb' USER user 'sysdba' PASSWORD 'masterkey'; -- выполняется подключение к БД alter table payrequest drop constraint FK_PRE_LGA; -- сбрасываются ключи на таблицу (так будет на основной бд) delete from testTable where 1=1; -- удаление из таблицы всех старых данных create table ext1 -- подключение внешней таблицы external '/d/varware/1-23.txt' ( id bigint ,ord varchar(11) ,ope varchar(10) ,clo varchar(10) ); -- перенос данных в основную таблицу insert into lukoilgarantagreement (lukoilgarantagreement_id, ordinal, opendate, closedate) select case id when id = 0 then (select * from GeneratorID) else id end ord, ope, clo from ext1; -- восстановление ключей ALTER TABLE table2 ADD CONSTRAINT FK_PRE_LGA FOREIGN KEY (testTable) REFERENCES TestTable_ID; -- удаление внешней таблицы drop table ext1;
Вот собственно простой способ использования массового добавления записей в БД, за короткий промежуток времени.
4. Итого
Время выполнения всего переноса данных составило около 9 минут. (дольше всего происходит удаление данных из таблицы)Время формирования файла для внешней таблицы составляет примерно 5 минут.
копирование файла на сервер примерно 1 минута.
Вставка данных в БД из внешней таблицы выполняется за 1 минуту 30 секунд.
В среднем процедура вствки данных ускорилась в 45 раз.
Я думаю, что это не плохой результат. И судя по всему это не предел.
Ну это как бы не полноценный BulkInsert, как например в MS SQL (http://msdn.microsoft.com/en-us/library/ms188365.aspx),
ОтветитьУдалитьно сути не меняет - Вставка большого количества данных.
Ты, кстати, забыл написать как делаешь вставку и обновление.
А еще непонятно зачем ключи удалять и записи, если на них могут существовать ссылки...
А еще :) ExternalFileAccess=Full - не хорошо, видимо, т.к. лучше сделать Restricted и указать пути откуда цеплять внешние таблицы.
Ну и хотелось бы подробнее, прикрутил ли ты это к production'у и как оно функционирует там?
Значит по шагам
ОтветитьУдалить1. Тестовый скрипт добавил в тело
2. Из за не решенных проблем с Bulk Update пришлось изощрятся с удалением данных. А поскольку удалять с ключами не получается, то их тоже дропнуть пришлось.
3. Не хорошо позже напишу, как решили.
4. Опять же пока на этапе внедрения. Поэтому рассказать как функционирует не могу.
если Firebird >= 2.1 можно использовать update or insert
ОтветитьУдалитьДа было бы гораздо удобнее. Но тестирование проводил на 1.5
ОтветитьУдалитьПусть и давний пост, но тем не менее мне например тема актуальна :-)
ОтветитьУдалитьСобственно задача та же - вставка больших объемов данных.
Сразу скажу - update or insert достаточно трудоемкая фиговина. Она хороша если вставка ну сотня-две тысяч записей. Когда количество приближается к милиону - тормоза появляются конкретные.
Иначально задача стояла следующая - сделать вставку большого количества записей. Время заличвки не сильно критично но не более часа. Кроме того работа в это время останавливаться не должна, а записи должны быть актуальны. Собственно решено было сделать дополнительное поле в таблице с данными - типа акуальность. Т.е. процесс немного другой становится:
1. Создается внешняя таблица с данными.
2. Подключается к базе
3. Выполняется копирование данных из внешней во внутреннюю таблицу. При записи данных флаг актуальности НЕ ставится. После завершения операции у нас в таблице есть данные и старые и новые. Но в силу того что есть флаг актуальности - по нему производится выбрка только актуальных данных.
4. Теперь мы можем сделать с данными все что хотим - можно просто удалить старые данные, а в новых флаг актуальности проставить. причем сделать это можно в одной транзакции. Можно одним запросом определить то что данные обновились и обновить их. Ну или что-то еще. Смысл в том что данные уже залиты во внутренние таблицы и с ними можно работать не особо заботясь о внешнем файле.
P.S. У меня вообще реализовано немного по другому - я копирую данные из внешней таблицы во внутреннюю временную. А на временную таблицу подключены триггеры которые производят обновление основных данных. Сам процесс занимает немного больше времени чем при обновлении чохом, но обладает большей универсальностью. Кроме того, такой подход позволяет запустить запрос и заниматься другими делами.