Введение

Год назад ФНС прекратила выкладывать базу ФИАС в формате DBF, теперь база доступна только в формате XML. Данные из DBF можно было вставлять в базу как есть, с XML так не получается, перед вставкой в базу, XML надо парсить, и это занимает время. Полная база ФИАС занимает 290 GB, парсинг такого объёма занимает значительное время.

Это время можно сократить, если парсинг распараллелить. Для этого надо разделить файлы на группы и каждую группу обрабатывать отдельно, но одновременно (процессоры давно многоядерные, надо не забывать этим пользоваться). Кроме того, конечно, надо использовать потоковый парсер, чтобы не ждать полной загрузки файла в оперативную память.

Я разработал несколько скриптов для создания и импорта базы. Эта статья будет о том как с помощью этих скриптов развернуть базу ФИАС за 9 часов на 6-ти ядерном процессоре.

Итоги

Странно начинать с итогов, но это интересней чем вникать в подробности, поэтому сначала кратко, а потом всё более и более подробно.

Алгоритм

  1. Скачать с сервера ФНС полную версию БД (https://fias.nalog.ru/Updates) 36 GB

  2. Развернуть архив, файлы с данными займут 300 GB

  3. Установить пакет для создания таблиц БД

  4. Установить пакет для импорта файлов

  5. Скопировать скрипты из исходников пакетов в отдельную директорию

  6. Разбить директории с данными регионов на равные по весу группы (каждая группа регионов будет импортироваться отдельным процессом PHP)

  7. Для каждой группы сделать копию скрипта импорта, в скрипте отредактировать список регионов в соответствии с группой

  8. Выполнить скрипт создания таблиц

  9. Запустить копии скриптов импорта в отдельных окнах терминала (параллельное выполнение)

  10. Выполнить скрипт создания индексов

  11. Have fun

Затраченное время

  1. 01-22 регионы, 54 GB - Rows was read is 301 929 373 duration is 08:20:06

  2. 23-36, 59 GB - Rows was read is 326 217 126 duration is 08:51:45

  3. 37-52, 63 GB - Rows was read is 342 339 705 duration is 09:15:50

  4. 53-67, 57 GB - Rows was read is 315 815 574 duration is 08:36:36

  5. 68-99, 55 GB - Rows was read is 285 073 083 duration is 07:53:59

Дополнительно к этому, создание индексов, ещё час - Creation index duration is 01:05:17. Запускать создание индексов параллельно особого смысла не имеет, потому что загрузка диска была 100% пополам с промежутками загрузки на 5%, чтобы совсем не было простоя диска, можно запустить два обработчика на создание индексов. Это сэкономит минут 20.

Использованное аппаратное обеспечение

Жёсткий диск

WDC WD40PURX-64AKYY0

Capacity:	3.6 TB
Type:	HDD

Не самый медленный диск, во время импорта его скорости вполне хватало, загрузка не превышала 50%. Когда я развернул базу на "быстром" NVMe диске, импорт занял 40 часов, на этот "медленный" диск база развернулась за 42:58:16, + 1 час на индексы, вместе 44 часа, это на четыре часа дольше, да, разница есть, но не драматичная.

База без индексов занимает 190 GB, с индексами весит столько же сколько исходные файлы: 231 GB (248,504,378,147 bytes), надо сказать, что у меня сделан всего 1 индекс в каждой таблице, для работы с базой, мне кажется, надо минимум пять индексов.

Процессор

Intel(R) Core(TM) i5-9400 CPU @ 2.90GHz

Base speed:	2.90 GHz
Sockets:	1
Cores:	6
Logical processors:	6
Virtualization:	Enabled
L1 cache:	384 KB
L2 cache:	1.5 MB
L3 cache:	9.0 MB

Загрузка процессора для пяти обработчиков упёрлась в потолок 100%, для четырёх - так же 100%, 3 - 85%, 2 - 55%, 1 - 35%. Памяти потребовалось: 8 Mb на процесс PHP и 4 Mb на процесс PostreSql, чтение с диска PHP - 2 Mb/s, PostreSql - 1.5 Mb/s.

В следующий раз я разобью файлы (регионы) на 4 группы, и импорт буду делать в 4 потока, пять это перебор для моей машины.

Про оперативную память нет смысла писать, потому что как видите, её объём не значителен.

How To

Настало время рассказать в подробностях.

Ставим пакеты

composer require sbwerewolf/fias-gar-schema-deploy-tool
composer require sbwerewolf/fias-gar-data-import-tool

Устанавливаем пакет для журналирования, на свой вкус.

composer require monolog/monolog

Логи можно вообще не писать, тогда в скрипте в качестве логера надо использовать \Psr\Log\NullLogger .

Копируем скрипты и SQL шаблоны создания таблиц

cp ./vendor/sbwerewolf/fias-gar-data-import-tool/test/* ./
cp ./vendor/sbwerewolf/fias-gar-schema-deploy-tool/test/* ./
cp ./config.env.example ./config.env

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

Файл "config.env.example" нужен из исходников "fias-gar-data-import-tool".

Проверьте, что директории скопировались вместе с файлами, в директории templates должно быть 28 директорий, и в каждой директории 2-4 файла.

Правим конфиг

Открываем config.env, правим настройки.

LOGIN PASSWORD DSN меняем под СУБД, в выбранной БД создаём схему для ФИАС, название схемы пишем в SCHEMA. У меня СУБД - Постгрес 14.

СУБД должна поддерживать секционирование. Может быть MySQL тоже умеет, но я не уверен за правильность синтаксиса. Синтаксис придётся править в шаблонах, для каждой из 28-ми таблиц.

В моих скриптах не используется ORM, поэтому извините, под вашу СУБД придётся поработать руками.

DO_IMPORT_WITH_CHECK присваиваем FALSE - чтобы перед вставкой не проверять существование записи, это будет лишний SELECT. При импорте значение FALSE , при обновлении базы - TRUE .

XML_FILES_PATH записываем путь к развёрнутому архиву ФИАС.

BATCH_SIZE это количество записей до выполненияCOMMIT, устанавливаем на свой вкус, по умолчанию 100 000. Я ставил 1 000 000, в этом случае, пауза между камитами просто увеличилась в 10 раз, ускорения не заметил.

Настраиваем скрипты

install-storage.php

Скрипт создания таблиц.

Настраиваем журналирование

Ниже создаётся логгер, запись будет и в файл и в консоль.

$logger = new Logger('common');

$pathComposer = new Path(__DIR__);
$logsPath = $pathComposer->make(
    [
        'logs',
        pathinfo(__FILE__, PATHINFO_FILENAME) . '-' . time() . '.log',
    ]
);

$writeHandler = new StreamHandler($logsPath);
$logger->pushHandler($writeHandler);

$logger->pushProcessor(function ($record) {
    /** @var LogRecord $record */
    echo "{$record->datetime} {$record->message}" . PHP_EOL;

    return $record;
});

Логи можно вообще не писать, тогда в скрипте в качестве логера надо использовать \Psr\Log\NullLogger .

Настройку логгера надо повторить в каждом скрипте (три скрипта - три раза).

Задаём путь к шаблонам SQL скриптов создания таблиц, директории перечисляем как элементы массива.

$templatesPath = $pathComposer->make(['template']);

Корневая директория задаётся при создании экземпляра $pathComposer = new Path(__DIR__);

Задаём список таблиц (имена папок с шаблонами скриптов для конкретной таблицы)

$templatesKitList = [
    'ADDHOUSETYPES',
    'ADDR_OBJ_DIVISION',
    'ADDR_OBJ_PARAMS',
    'ADDRESSOBJECTS',
    'ADDRESSOBJECTTYPES',
    'ADM_HIERARCHY',
    'APARTMENTS',
    'APARTMENTS_PARAMS',
    'APARTMENTTYPES',
    'CARPLACES',
    'CARPLACES_PARAMS',
    'CHANGE_HISTORY',
    'HOUSES',
    'HOUSES_PARAMS',
    'HOUSETYPES',
    'MUN_HIERARCHY',
    'NDOCKINDS',
    'NDOCTYPES',
    'NORMDOCS',
    'OBJECTLEVELS',
    'OPERATIONTYPES',
    'PARAMTYPES',
    'REESTR_OBJECTS',
    'ROOMS',
    'ROOMS_PARAMS',
    'ROOMTYPES',
    'STEADS',
    'STEADS_PARAMS',
];
$command->run($templatesKitList, 'create-table.php')

В зависимости от того как вы собираетесь использовать базу, можно удалить создание каких то таблиц, например "*_PARAMS", "NORMDOCS", "CARPLACES", "ROOMS", "STEADS".

Задаём таблицы для которых нужно сделать секционирование по региону (таблицы с колонкой REGION), файлы этих таблиц лежат в директории региона.

$templatesKitList = [
    'ADDR_OBJ_DIVISION',
    'ADDR_OBJ_PARAMS',
    'ADDRESSOBJECTS',
    'ADM_HIERARCHY',
    'APARTMENTS',
    'APARTMENTS_PARAMS',
    'CARPLACES',
    'CARPLACES_PARAMS',
    'CHANGE_HISTORY',
    'HOUSES',
    'HOUSES_PARAMS',
    'MUN_HIERARCHY',
    'NORMDOCS',
    'REESTR_OBJECTS',
    'ROOMS',
    'ROOMS_PARAMS',
    'STEADS',
    'STEADS_PARAMS',
];
$command->run(
    $templatesKitList,
    'create-partition.php',
    false,
    99,
    1

);

Соответственно, удаляем таблицы которые были удалены из первого списка.

data-import.php

Скрипт для импорта XML файлов в БД.

Делаем пять копий (для параллельной обработки в пять потоков).

cp ./data-import.php ./data-import-1.php
cp ./data-import.php ./data-import-2.php
cp ./data-import.php ./data-import-3.php
cp ./data-import.php ./data-import-4.php
cp ./data-import.php ./data-import-5.php

В каждой копии скрипта импорта, настраиваем параметры выполнения команды импорта (класс $options = new ImportOptions()).

$options = new ImportOptions(
    $doAddNewWithCheck,
    [
        AddHouseTypes::class =>
            'AS_ADDHOUSE_TYPES_20*.{x,X}{m,M}{l,L}',
        AddressObjectTypes::class =>
            'AS_ADDR_OBJ_TYPES_20*.{x,X}{m,M}{l,L}',
        ApartmentTypes::class =>
            'AS_APARTMENT_TYPES_20*.{x,X}{m,M}{l,L}',
        HouseTypes::class =>
            'AS_HOUSE_TYPES_20*.{x,X}{m,M}{l,L}',
        NormativeDocumentsKinds::class =>
            'AS_NORMATIVE_DOCS_KINDS_20*.{x,X}{m,M}{l,L}',
        NormativeDocumentsTypes::class =>
            'AS_NORMATIVE_DOCS_TYPES_20*.{x,X}{m,M}{l,L}',
        ObjectLevels::class =>
            'AS_OBJECT_LEVELS_20*.{x,X}{m,M}{l,L}',
        OperationTypes::class =>
            'AS_OPERATION_TYPES_20*.{x,X}{m,M}{l,L}',
        ParamTypes::class =>
            'AS_PARAM_TYPES_20*.{x,X}{m,M}{l,L}',
        RoomTypes::class =>
            'AS_ROOM_TYPES_20*.{x,X}{m,M}{l,L}',
    ],
    '{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}',
    [
        AddressObjects::class =>
            'AS_ADDR_OBJ_20*.{x,X}{m,M}{l,L}',
        AddressObjectDivision::class =>
            'AS_ADDR_OBJ_DIVISION_20*.{x,X}{m,M}{l,L}',
        AddressObjectParams::class =>
            'AS_ADDR_OBJ_PARAMS_20*.{x,X}{m,M}{l,L}',
        AdministrativeHierarchy::class =>
            'AS_ADM_HIERARCHY_20*.{x,X}{m,M}{l,L}',
        Apartments::class =>
            'AS_APARTMENTS_20*.{x,X}{m,M}{l,L}',
        ApartmentsParams::class =>
            'AS_APARTMENTS_PARAMS_20*.{x,X}{m,M}{l,L}',
        CarPlaces::class =>
            'AS_CARPLACES_20*.{x,X}{m,M}{l,L}',
        CarPlacesParams::class =>
            'AS_CARPLACES_PARAMS_20*.{x,X}{m,M}{l,L}',
        ChangeHistory::class =>
            'AS_CHANGE_HISTORY_20*.{x,X}{m,M}{l,L}',
        Houses::class =>
            'AS_HOUSES_20*.{x,X}{m,M}{l,L}',
        HousesParams::class =>
            'AS_HOUSES_PARAMS_20*.{x,X}{m,M}{l,L}',
        MunicipalHierarchy::class =>
            'AS_MUN_HIERARCHY_20*.{x,X}{m,M}{l,L}',
        NormativeDocuments::class =>
            'AS_NORMATIVE_DOCS_20*.{x,X}{m,M}{l,L}',
        ReestrObjects::class =>
            'AS_REESTR_OBJECTS_20*.{x,X}{m,M}{l,L}',
        Rooms::class =>
            'AS_ROOMS_20*.{x,X}{m,M}{l,L}',
        RoomsParams::class =>
            'AS_ROOMS_PARAMS_20*.{x,X}{m,M}{l,L}',
        Steads::class =>
            'AS_STEADS_20*.{x,X}{m,M}{l,L}',
        SteadsParams::class =>
            'AS_STEADS_PARAMS_20*.{x,X}{m,M}{l,L}',
    ],
);

Первый аргумент (логическая переменная) отвечает за выполнение проверки существования записи перед её добавлением. В скрипте значение этого аргумента задаётся переменной окружения DO_IMPORT_WITH_CHECK.

$doAddNewWithCheck,

Соответственно при разворачивании базы нам эта проверка не нужна, передаём значение false. При обновлении базы, значение должно быть true.

Второй аргумент (массив) это справочники. Справочники общие на все регионы, для них не надо создавать партиции, они импортируются один раз.Импорт справочников много времени не занимает, но при желании можно убрать из списка справочники которые вам не нужны.

    [
        AddHouseTypes::class =>
            'AS_ADDHOUSE_TYPES_20*.{x,X}{m,M}{l,L}',
        AddressObjectTypes::class =>
            'AS_ADDR_OBJ_TYPES_20*.{x,X}{m,M}{l,L}',
        ApartmentTypes::class =>
            'AS_APARTMENT_TYPES_20*.{x,X}{m,M}{l,L}',
        HouseTypes::class =>
            'AS_HOUSE_TYPES_20*.{x,X}{m,M}{l,L}',
        NormativeDocumentsKinds::class =>
            'AS_NORMATIVE_DOCS_KINDS_20*.{x,X}{m,M}{l,L}',
        NormativeDocumentsTypes::class =>
            'AS_NORMATIVE_DOCS_TYPES_20*.{x,X}{m,M}{l,L}',
        ObjectLevels::class =>
            'AS_OBJECT_LEVELS_20*.{x,X}{m,M}{l,L}',
        OperationTypes::class =>
            'AS_OPERATION_TYPES_20*.{x,X}{m,M}{l,L}',
        ParamTypes::class =>
            'AS_PARAM_TYPES_20*.{x,X}{m,M}{l,L}',
        RoomTypes::class =>
            'AS_ROOM_TYPES_20*.{x,X}{m,M}{l,L}',
    ],

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

Третий аргумент (строка) определяет маску для выбора директорий с регионами. Соответственно делаем копии этого скрипта по количеству потоков обработки.

'{1,2,3,4,5,6,7,8,9,0}{1,2,3,4,5,6,7,8,9,0}',

У меня было пять копий, с такими значениями:

'{01,02,03,04,05,06,07,08,09,10,11,12,13,14,15,16,17,18,19,20,21,22}',
'{23,24,25,26,27,28,29,30,31,32,33,34,35,36}',
'{37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52}',
'{53,54,55,56,57,58,59,60,61,62,63,64,65,66,67}',
'{99,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95}',

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

    [
        AddressObjects::class =>
            'AS_ADDR_OBJ_20*.{x,X}{m,M}{l,L}',
        AddressObjectDivision::class =>
            'AS_ADDR_OBJ_DIVISION_20*.{x,X}{m,M}{l,L}',
        AddressObjectParams::class =>
            'AS_ADDR_OBJ_PARAMS_20*.{x,X}{m,M}{l,L}',
        AdministrativeHierarchy::class =>
            'AS_ADM_HIERARCHY_20*.{x,X}{m,M}{l,L}',
        Apartments::class =>
            'AS_APARTMENTS_20*.{x,X}{m,M}{l,L}',
        ApartmentsParams::class =>
            'AS_APARTMENTS_PARAMS_20*.{x,X}{m,M}{l,L}',
        CarPlaces::class =>
            'AS_CARPLACES_20*.{x,X}{m,M}{l,L}',
        CarPlacesParams::class =>
            'AS_CARPLACES_PARAMS_20*.{x,X}{m,M}{l,L}',
        ChangeHistory::class =>
            'AS_CHANGE_HISTORY_20*.{x,X}{m,M}{l,L}',
        Houses::class =>
            'AS_HOUSES_20*.{x,X}{m,M}{l,L}',
        HousesParams::class =>
            'AS_HOUSES_PARAMS_20*.{x,X}{m,M}{l,L}',
        MunicipalHierarchy::class =>
            'AS_MUN_HIERARCHY_20*.{x,X}{m,M}{l,L}',
        NormativeDocuments::class =>
            'AS_NORMATIVE_DOCS_20*.{x,X}{m,M}{l,L}',
        ReestrObjects::class =>
            'AS_REESTR_OBJECTS_20*.{x,X}{m,M}{l,L}',
        Rooms::class =>
            'AS_ROOMS_20*.{x,X}{m,M}{l,L}',
        RoomsParams::class =>
            'AS_ROOMS_PARAMS_20*.{x,X}{m,M}{l,L}',
        Steads::class =>
            'AS_STEADS_20*.{x,X}{m,M}{l,L}',
        SteadsParams::class =>
            'AS_STEADS_PARAMS_20*.{x,X}{m,M}{l,L}',
    ],

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

create-indexes.php

Скрипт создания индексов.

Снова редактируем список таблиц для которых будем создавать индексы.

$templatesKitList = [
    'ADDHOUSETYPES',
    'ADDRESSOBJECTTYPES',
    'APARTMENTTYPES',
    'HOUSETYPES',
    'NDOCKINDS',
    'NDOCTYPES',
    'OBJECTLEVELS',
    'OPERATIONTYPES',
    'PARAMTYPES',
    'ROOMTYPES',
    'ADDR_OBJ_DIVISION',
    'ADDR_OBJ_PARAMS',
    'ADDRESSOBJECTS',
    'ADM_HIERARCHY',
    'APARTMENTS',
    'APARTMENTS_PARAMS',
    'CARPLACES',
    'CARPLACES_PARAMS',
    'CHANGE_HISTORY',
    'HOUSES',
    'HOUSES_PARAMS',
    'MUN_HIERARCHY',
    'NORMDOCS',
    'REESTR_OBJECTS',
    'ROOMS',
    'ROOMS_PARAMS',
    'STEADS',
    'STEADS_PARAMS',
];
$command->run($templatesKitList, 'create-index.php');

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

Запускаем скрипты

Сначала создаём таблицы. Перед этим, конечно, включаем OPCache и отключаем дебагер (у меня xDebug), всё это делается в php.ini.

[opcache]
; Determines if Zend OPCache is enabled
opcache.enable=1
[xDebug]
;zend_extension = xdebug

Создаём таблицы

Запускам скрипт

php ./install-storage.php

Через минуту схема БД будет готова

Creation tables duration is 00:01:10

Запускам импорт

Теперь в отдельных окнах терминала запускаем собственно импорт

php ./data-import1.php

Аналогичным образом запускаем остальные копии скрипта импорта базы.

Скрипт начнёт работу, в консоли будет написано:

Script is starting
2023-02-03T05:06:58.107703+05:00 Script is starting
2023-02-03T05:06:58.198173+05:00 Run import FIAS GAR files from `D:\WORK\fias-gar\gar_xml`, with commit each `100 000` operations
2023-02-03T05:06:58.198461+05:00 Import starting
2023-02-03T05:06:58.218593+05:00 Starting common reference import
2023-02-03T05:06:58.218764+05:00 Start import files of pattern AS_ADDHOUSE_TYPES_20*.{x,X}{m,M}{l,L}
2023-02-03T05:06:58.431129+05:00 Starting import file D:\WORK\fias-gar\gar_xml\AS_ADDHOUSE_TYPES_20221222_86d52a97-cf6d-4329-8f55-0723b3feda90.XML
2023-02-03T05:06:58.460446+05:00 Rows read `4`, success Operations `4`, success Inserts `4`, insert Affected `4`, failure Inserts `0`, success Updates `0`, update Affected `0`, failure Updates `0`
2023-02-03T05:06:58.461465+05:00 Finish import files of pattern AS_ADDHOUSE_TYPES_20*.{x,X}{m,M}{l,L}

Каждые 100 000 (BATCH_SIZE) записей будет выводиться статистика:

2023-02-03T05:08:04.095876+05:00 Rows read `500 000`, success Operations `500000`, success Inserts `500000`, insert Affected `500000`, failure Inserts `0`, success Updates `0`, update Affected `0`, failure Updates `0`
2023-02-03T05:08:04.123922+05:00 Batch duration is 00:00:12 795 ms 521 mcs 500 ns, rows was read is `500 000`, mem allocated is `2`Mb, import processing with `500000` success Operations

Ждём 8 часов :)) По завершению работы скрипта будет подведена статистика:

2023-02-03T13:27:01.834772+05:00 Rows read `301 900 000`, success Operations `301900000`, success Inserts `301900000`, insert Affected `301900000`, failure Inserts `0`, success Updates `0`, update Affected `0`, failure Updates `0`
2023-02-03T13:27:01.835855+05:00 Batch duration is 00:00:08 723 ms 752 mcs 100 ns, rows was read is `301 900 000`, mem allocated is `2`Mb, import processing with `301900000` success Operations
2023-02-03T13:27:04.426001+05:00 Rows read `301 929 373`, success Operations `301929373`, success Inserts `301929373`, insert Affected `301929373`, failure Inserts `0`, success Updates `0`, update Affected `0`, failure Updates `0`
2023-02-03T13:27:04.426257+05:00 Finish import files of pattern AS_STEADS_PARAMS_20*.{x,X}{m,M}{l,L}
2023-02-03T13:27:04.426419+05:00 Finish region data import
2023-02-03T13:27:04.427513+05:00 Batch duration is 00:00:02 591 ms 510 mcs 900 ns, rows was read is `301 929 373`, mem allocated is `2`Mb, import processing with `301929373` success Operations
2023-02-03T13:27:04.427642+05:00 Rows was read is `301 929 373`, import was processed with `301929373` success operations, max mem allocated is `2`Mb
2023-02-03T13:27:04.427753+05:00 Import duration is 08:20:06 469 ms 147 mcs 400 ns
2023-02-03T13:27:04.427842+05:00 Script is finished

Создаём индексы

Для ускорения можно создать две копии скрипта создания индексов.

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

Вы может попросить у меня логи, я с радостью поделюсь.

Запускаем скрипт.

php ./create-indexes.php

Ждём час.

Creation index duration is 01:05:17

Вот и сказочке конец, а кто слушал, молодец !

Обновление базы

Обновление происходит аналогичным образом. В конфиге (config.env), в XML_FILES_PATH записываем путь к файлам обновления, меняем значение DO_IMPORT_WITH_CHECK на TRUE, запускаем скрипт, ждём, пользуемся.

Заключение

Импорт написан в декларативном стиле, каждая таблица объявлена как набор колонок. Для колонок определёно их поведение, при присвоении значения, при вставке новой записи, при обновлении ранее добавленной записи.

Это позволяет на базе этого кода писать произвольные импорты, в любые таблицы, любых данных. Одно условие - значения для импорта, надо передавать как массив [имя колонки => значение колонки].

Пространство имён SbWereWolf\FiasGarDataImport\Import\Composition\Column содержит примеры классов колонок, SbWereWolf\FiasGarDataImport\Import\Processor - примеры таблиц.

Изучайте, копируйте, распространяйте :)

Бонус

PgAdmin, загрузка по транзакциям, 5 скриптов импорта.

PgAdmin, загрузка по транзакциям, 5 скриптов импорта
PgAdmin, загрузка по транзакциям, 5 скриптов импорта

Загрузка HDD, 5 скриптов импорта.

Загрузка HDD, 5 скриптов импорта.
Загрузка HDD, 5 скриптов импорта.

Метрики процессов, 5 скриптов импорта.

Метрики процессов, 5 скриптов импорта.
Метрики процессов, 5 скриптов импорта.

Загрузка HDD, 4 скрипта импорта.

Загрузка HDD, 4 скрипта импорта.
Загрузка HDD, 4 скрипта импорта.

Метрики процессов, 3 скрипта импорта.

Метрики процессов, 3 скрипта импорта.
Метрики процессов, 3 скрипта импорта.

Загрузка HDD, 3 скрипта импорта.

Загрузка HDD, 3 скрипта импорта.
Загрузка HDD, 3 скрипта импорта.

Загрузка CPU, 3 скрипта импорта.

Загрузка CPU, 3 скрипта импорта.
Загрузка CPU, 3 скрипта импорта.

Метрики процессов, 2 скрипта импорта.

Метрики процессов, 2 скрипта импорта.
Метрики процессов, 2 скрипта импорта.

Метрики процессов, 1 скрипт импорта.

Метрики процессов, 1 скрипт импорта
Метрики процессов, 1 скрипт импорта

Загрузка CPU, 1 скрипт импорта.

Загрузка CPU, 1 скрипт импорта.
Загрузка CPU, 1 скрипт импорта.

Загрузка HDD, работает скрипт создания индексов в один поток.

Загрузка HDD, работает скрипт создания индексов в один поток
Загрузка HDD, работает скрипт создания индексов в один поток

Учитывая белые провалы, можно запустить создание индексов в два потока, в два скрипта. CPU при этом не сильно нагружен. Что интересно, при создании индексов, сначала идёт чтение на скорости 100 MB/s, потом запись на той же скорости 100 MB/s.

Комментарии (12)


  1. OBIEESupport
    04.02.2023 03:29

    В этом точно есть рациональное зерно. Только много ли есть задач, где нужна вся страна разом?


    1. joffer
      04.02.2023 13:42

      ну вы же знаете, какие указания приходят "сверху" - "нужна вся страна. Зачем? На всякий случай, шобы было". А потом, по факту, как всегда, максимум 3 - 4 региона и МСК и используются


  1. screwer
    04.02.2023 04:05
    +4

    Данные из DBF можно было вставлять в базу как есть, с XML так не получается

    У DBF тривиальный формат, создавали бы сразу его, сами.

    Развернуть архив, файлы импорта займут 300 GB

    А не надо этого делать. Надо сразу по мере распаковки и парсить файл, вообще не записывая распакованное на диск. Это изрядно сократит число дисковых операций.

    чтение с диска PHP - 2 Mb/s, PostreSql - 1.5 Mb/s

    (Голосом Швондера) это какой-то... позор!

    ЗЫ: задачка из разряда сортировки за час. Видится мне здесь генерация всех дбф должна занять также не более часа.


    1. SbWereWolf Автор
      06.02.2023 11:55

      Если реализация займёт всего час, то я с удовольствием посмотрю на реализацию, особенно если она будет для PHP8.
      У меня на этот код ушло две недели, и на статью ещё 6 часов, у вас получиться потратить один час своего времени для меня ?
      Я буду вам очень признателен.


  1. shurutov
    04.02.2023 07:59

    Скачать с сервера ФНС полную версию БД (https://fias.nalog.ru/Updates) 36 GB

    И тут же рядом лежит БД в формате КЛАДР 4.0. В dbf...


    1. JavaNoob
      04.02.2023 11:11

      >И тут же рядом лежит БД в формате КЛАДР 4.0. В dbf

      как-то странно сравнивать ж и п

      Вопрос к автору: а как в ФИАСе нынче с номерами домов? Год-два назад их было очень мало.


      1. Xokare228
        05.02.2023 02:35

        Я не автор, но отвечу. Очень даже неплохо. Не всё есть, но никакие OSM и иже с ними даже рядом не стояли.




  1. alex1t
    04.02.2023 10:10
    +3

    Нам тоже нужна вся страна. Грузили через xml2csv: конвертируем xml в csv (там вполне плоская иерархия) и грузим в postges через COPY. Грузится минут за 30


  1. foxyrus
    04.02.2023 11:24
    +2

    1. Volkodlak
      04.02.2023 15:53

      Друзья, внимательнее надо быть (по вашей же ссылке):

      В целях повсеместного использования (ч. 2 ст. 8 Федерального закона № 443 от 28.12.2013) в информационных системах пользователей ФИАС сведений об адресах (унифицированных и структурированных), содержащихся в Государственном адресном реестре, еженедельно на портале ФИАС размещаются файлы выгрузки адресных сведений в формате «ГАР».

      Так вот то что в статье это и есть формат "ГАР"


  1. ikrusenstern
    04.02.2023 19:20

    Во первых необязательно вообще архив распаковывать и занимать почти 300 гб, есть же XMLReader который умеет читать файлы прям с архива. Например так:

    $reader = new XMLReader();
    $reader->open('zip://gar_xml.zip#77/AS_ADDR_OBJ_20230202_e407fd98-b92a-45dd-9e34-ef15f6d3bae5.XML', 'UTF-8', LIBXML_COMPACT | LIBXML_NONET | LIBXML_NOBLANKS);

    Во вторых есть готовое решение которе справляется с задачей ни чуть не хуже:

    https://github.com/liquetsoft/fias-component базовый компонент, на его основе есть библиотеки для работы с базой ФИАС в формате ГАР для рахных фреймворков итд. Например для Laravel, Symfony или для импорта сразу в Elasticsearch.

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

    P.S.

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


  1. aegoroff
    04.02.2023 20:11
    +2

    развернуть базу ФИАС за 9 часов на 6-ти ядерном процессоре.

    Вам определенно надо менять технологию - для примера, я на Rust XML файл весом 25 гигабайт разбираю за меньше чем за минуту, правда это все лежит на NVMe диске.