Как стать автором
Обновить

Мой адрес не дом и не улица, мой адрес – Советский Союз?

Время на прочтение 13 мин
Количество просмотров 4K
microBIGDATA или ФИАС в кармане


Питер Брейгель Младший, Уплата налога, 1640 год

Прошлый заход на бреющем по объектам зашел. Продолжим разведку боем. Сегодня поговорим о тяжелом. Пусть ещё не о BIG DATA, но работать уже неудобно – достаточно большие объёмы данных. Не каждому влезет в оперативную память целиком, а некоторым не влезет даже на диск (не места мало, а хламу много). Имя нашему подопечному БД ФИАС — база данных федеральной адресной информационной системы. Архив в 5,5 ГБ. И это сжатый в архив XML. После распаковки будут полные 53 ГБ (для распаковки запасайте 110 ГБ). И как начнёшь его парсить да конвертить, то и 110 ГБ будет мало. О потребном размере ОЗУ тоже будет.

Всё бы ничего, но можно дальше копнуть. Есть такой международный открытый проект по сбору и систематизации адресных данных – OpenAddresses. Так там базы данных побольше будут. Текущее покрытие планеты имеет много белых пятен, например, Россия почти отсутствует. Размер архива – 10 ГБ.


Или база данных достаточно известного проекта OpenStreetMaps. Он строится силами добровольцев по принципу википедии. Довольно подробный и многоязычный. Сейчас полный архив сжатого XML размером 74 ГБ.
Коли про адреса заговорили, неожиданная новость подоспела от DuckDuckGo, лучшего на сегодня из безопасных поисковиков, про его переход на карты Apple. Точнее на Apple MapKit JS. Самая интересная фишка в нашем контексте – «улучшенный поиск адресов». Apple лучше всех кропотливо собирает и бережёт наши данные? Надо будет проследить…
Итак, задача. Как всё это адресное богатство уложить в приятное для использования хранилище, дать возможность помечтать о вольготном API (на Питоне, конечно) и не дать родимому железу поперхнуться под не считанной нагрузкой. Назовём это МикроБольшиеДанные – мкБД или µBG по-англицки :-)

В хозяйстве каждого второго (а то и первого) разработчика эта штука – адресный справочник, он же – справочник топонимов, вещь очень нужная. А когда ещё и нормативная, правильным органом подготовленная, очищенная и хорошо документированная – просто сказка. Надо отдать должное, русская налоговая служба своё цифровое производство делает хорошо. Насколько это возможно. Есть, наверное, внутри какие-то изъяны и очистка данных продолжается. Как этот вопрос разрешить, пусть государственные головы думу думают. Для себя решают и нам всем на пользу. Кстати, одна опечатка из ФИАС ниже в примере нашлась. На результат не влияет. Не стал исправлять. Найдёте?

Не знаю насколько именно в ваших проектах актуальны адресные данные – эти все регионы, города, улицы. Но кажется, без них не обойтись ни в одном проекте для людей. То адрес, где искать человека или куда отправлять ему посылки. То реквизиты паспорта или какого другого документа надо сохранить. А может это адрес рабочего офиса или достопримечательности, что рекомендуют посетить. И что делать? Где взять?

Самое простое решение, без оглядки на ошибки и дубликаты – это примитивные объекты содержащие простые строковые литералы (они же строковые константы, они же string). Пусть пользователи в них вносят очередные поступившие записи. А объекты умеют себя сохранять — это мы уже проходили.

Такие объекты, например, как описано в классе ниже. Прямо из учебника, пусть американского, но с поправкой на нашу российскую действительность – вместо их ZIP, будет наш postalCode. Я б ещё почтовый индекс заменил на число, но ради однообразия оставлю строкой. Кто с ходу узнал язык, а это ObjectScript, тому полагается поощрительный лайк.

Class Soviet.Address Extends %Persistent {
    Property streetName As %String;
    Property cityName As %String; 
    Property areaName As %String; 
    Property postalCode As %String;
}

Безусловно, многие возмутятся, мол из карманов объекта всё наружу (литералами) торчит. Где это видано, чтоб объект свои поля публично светил?! Оставим пока так, больно уж пример красноречивый и понятный любому школьнику.

На самом деле это всё, что нужно. Заполнили поля. Положили на хранение. Передали для работы другим объектам. Наследовали дальше кому-нибудь. Всё работает. И хранится!
А вот пару слов почему так делать не стоит, необходимо сказать. Что есть наш объект Адрес? Почему он не может быть просто группой текстовых строк? Самые очевидные возражения, что приходят в голову, идут от контекста – кто использует этот Адрес, в какой форме и для каких целей? Попробуйте отложить в сторону своё программистское мышление и представить как думает «иностранный турист», «историк», «налоговый инспектор», «юрист» и так далее.

Полагаю, возникает сразу масса дополнительных вопросов-уточнений: какой язык использовать, в какой кодировке хранить и отдавать, к какой эпохе причислить, каким документов введено в действие, юридический или почтовый? А город — это именованный населённый пункт или что? Даже улица может оказаться бульваром, переулком, проспектом или чем-то другим. Как со всеми этими важными деталями реализации поступить?

Возьмём живой пример. Компанией Гугл сейчас руководит Сундар Пичаи. Сам он из Индии. Родился в городе Ченнаи (он же Ченнай). Или же в Мадрасе? В 1996 году индийцы решили, что название города какое-то очень португальское и переименовали столицу штата Тамилнад из Мадраса в Ченнаи. И что должен записать в своих электронных документах Сундар и 72 млн. его земляков?

В общем, целая наука этим занимается – прикладная топонимика.
Так и напрашиваются вопросы в догонку. Как управляться с временем и датой? Так ли очевидны деньги? Так ли просты географические координаты? А как это реализовано в вашем коде? А сможете без понижения уровня абстракции передать в выбранную СУБД? Как не скатываться в атомарные типы машинных данных и постоянно думать об их реконструкции? Здесь же стоит искать источник примитивного или, наоборот, добротного API. Подумайте над этим на досуге.

Одним словом, контекст — самое важное. И объектная модель даёт нам возможность использовать это напрямую посредством инкапсуляции «машинных данных» и реализации зависимого от контекста «живого» поведения. Совсем не то, что низкоуровневые кортежи уложенные в таблицы ;-)

А пока вернёмся к «примитивной» реализации и усложним себе жизнь. Для начала устраним ошибки и дубликаты. То есть будем искать способ писать адреса сразу правильно. Заодно, поможем разработчикам пользовательского интерфейса организовать подсказки пользователям при заполнении полей ввода данных.
Когда в одном месте собираются двое – тексты и платформа данных InterSystems IRIS, у разработчика появляется реальная возможность развернуться на полную не отходя от станка. Например, используя встроенные объектные компоненты iKnow и iFind. Это компоненты для работы с неструктурированными данными и полнотекстового поиска, соответственно. Русский язык поддерживается «из коробки».
Перво-наперво, научим Адрес читать нужные данные из первоисточника. Благо в наборе данных федеральной налоговой службы есть готовые описания структуры документов XML. Согласно прилагаемому к данным описанию с сайта ФИАС, нам понадобится набор данных ADDROBJ, которому, в моём случае, соответствует файл AS_ADDROBJ_2_250_01_04_01_01.xsd

Далее, воспользуемся, любезно подготовленным для нас разработчиками IRIS, системным преобразователем XSD шаблона в соответствующую структуру полей класса %XML.Adaptor. Знак процента вначале как раз обозначает, что это класс из системной библиотеки. Подробности использования есть в документации. Мы операции будем производить в терминале.

set xmlScheme = ##class(%XML.Utils.SchemaReader).%New()
do xmlScheme.Process("http://localhost/AS_ADDROBJ_2_250_01_04_01_01.xsd")

Тоже самое можно получить в IDE Ательер (в меню Tools > Add-Ins > XML Schema Wizard) или аналогичными запросами к объектам напрямую из кода программы.



Так как мы использовали конструктор без указания параметров, а именно название пакета размещения получаемых классов, то они оказались в пакете Test. Как видно из второй команды, файл схемы я отдал через свой локальный веб-сервер на Питоне:

python3 -m http.server 80

Можете использовать любой другой http-сервер, который вам по душе. Или загрузить файл на ваш сервер IRIS и указать к нему прямой путь.

В результате у нас есть два класса полностью отражающие структуру нашего адресного XML:

Test.AddressObjects
/// Состав и структура файла с информацией классификатора адресообразующих элементов БД ФИАС
Class Test.AddressObjects Extends (%Persistent, %XML.Adaptor) [ ProcedureBlock ] {
    Parameter XMLNAME = "AddressObjects";
    Parameter XMLSEQUENCE = 1;

    /// Классификатор адресообразующих элементов
    Relationship Object As Test.Object(XMLNAME = "Object", XMLPROJECTION = "ELEMENT") [ Cardinality = many, Inverse = AddressObjects ];
}

Test.Object
/// Создано из: http://localhost:28869/AS_ADDROBJ_2_250_01_04_01_01.xsd
Class Test.Object Extends (%Persistent, %XML.Adaptor) [ ProcedureBlock ] {
    Parameter XMLNAME = "Object";
    Parameter XMLSEQUENCE = 1;

    /// Глобальный уникальный идентификатор адресного объекта
    Property AOGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOGUID", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Формализованное наименование
    Property FORMALNAME As %String(MAXLEN = 120, MINLEN = 1, XMLNAME = "FORMALNAME", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код региона
    Property REGIONCODE As %String(MAXLEN = 2, MINLEN = 2, XMLNAME = "REGIONCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код автономии
    Property AUTOCODE As %String(MAXLEN = 1, MINLEN = 1, XMLNAME = "AUTOCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код района
    Property AREACODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "AREACODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код города
    Property CITYCODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "CITYCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код внутригородского района
    Property CTARCODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "CTARCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код населенного пункта
    Property PLACECODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "PLACECODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код элемента планировочной структуры
    Property PLANCODE As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "PLANCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код улицы
    Property STREETCODE As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "STREETCODE", XMLPROJECTION = "ATTRIBUTE");

    /// Код дополнительного адресообразующего элемента
    Property EXTRCODE As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "EXTRCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Код подчиненного дополнительного адресообразующего элемента
    Property SEXTCODE As %String(MAXLEN = 3, MINLEN = 3, XMLNAME = "SEXTCODE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Официальное наименование
    Property OFFNAME As %String(MAXLEN = 120, MINLEN = 1, XMLNAME = "OFFNAME", XMLPROJECTION = "ATTRIBUTE");

    /// Почтовый индекс
    Property POSTALCODE As %String(MAXLEN = 6, MINLEN = 6, XMLNAME = "POSTALCODE", XMLPROJECTION = "ATTRIBUTE");

    /// Код ИФНС ФЛ
    Property IFNSFL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "IFNSFL", XMLPROJECTION = "ATTRIBUTE");

    /// Код территориального участка ИФНС ФЛ
    Property TERRIFNSFL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "TERRIFNSFL", XMLPROJECTION = "ATTRIBUTE");

    /// Код ИФНС ЮЛ
    Property IFNSUL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "IFNSUL", XMLPROJECTION = "ATTRIBUTE");

    /// Код территориального участка ИФНС ЮЛ
    Property TERRIFNSUL As %String(MAXLEN = 4, MINLEN = 4, XMLNAME = "TERRIFNSUL", XMLPROJECTION = "ATTRIBUTE");

    /// OKATO
    Property OKATO As %String(MAXLEN = 11, MINLEN = 11, XMLNAME = "OKATO", XMLPROJECTION = "ATTRIBUTE");

    /// OKTMO
    Property OKTMO As %String(MAXLEN = 11, MINLEN = 8, XMLNAME = "OKTMO", XMLPROJECTION = "ATTRIBUTE");

    /// Дата  внесения записи
    Property UPDATEDATE As %Date(XMLNAME = "UPDATEDATE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Краткое наименование типа объекта
    Property SHORTNAME As %String(MAXLEN = 10, MINLEN = 1, XMLNAME = "SHORTNAME", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Уровень адресного объекта
Property AOLEVEL As %Integer(XMLNAME = "AOLEVEL", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ];

    /// Идентификатор объекта родительского объекта
    Property PARENTGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "PARENTGUID", XMLPROJECTION = "ATTRIBUTE");

    /// Уникальный идентификатор записи. Ключевое поле.
Property AOID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOID", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Идентификатор записи связывания с предыдушей исторической записью
Property PREVID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "PREVID", XMLPROJECTION = "ATTRIBUTE");

    /// Идентификатор записи  связывания с последующей исторической записью
Property NEXTID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "NEXTID", XMLPROJECTION = "ATTRIBUTE");

    /// Код адресного объекта одной строкой с признаком актуальности из КЛАДР 4.0.
Property CODE As %String(MAXLEN = 17, MINLEN = 0, XMLNAME = "CODE", XMLPROJECTION = "ATTRIBUTE");

    /// Код адресного объекта из КЛАДР 4.0 одной строкой без признака актуальности (последних двух цифр)
Property PLAINCODE As %String(MAXLEN = 15, MINLEN = 0, XMLNAME = "PLAINCODE", XMLPROJECTION = "ATTRIBUTE");

    /// Статус актуальности адресного объекта ФИАС. Актуальный адрес на текущую дату. Обычно последняя запись об адресном объекте.
    /// 0 – Не актуальный
    /// 1 - Актуальный
    Property ACTSTATUS As %Integer(XMLNAME = "ACTSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ];

    /// Статус центра
    Property CENTSTATUS As %Integer(XMLNAME = "CENTSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ];

    /// Статус действия над записью – причина появления записи (см. описание таблицы OperationStatus):
    /// 01 – Инициация;
    /// 10 – Добавление;
    /// 20 – Изменение;
    /// 21 – Групповое изменение;
    /// 30 – Удаление;
    /// 31 - Удаление вследствие удаления вышестоящего объекта;
    /// 40 – Присоединение адресного объекта (слияние);
    /// 41 – Переподчинение вследствие слияния вышестоящего объекта;
    /// 42 - Прекращение существования вследствие присоединения к другому адресному объекту;
    /// 43 - Создание нового адресного объекта в результате слияния адресных объектов;
    /// 50 – Переподчинение;
    /// 51 – Переподчинение вследствие переподчинения вышестоящего объекта;
    /// 60 – Прекращение существования вследствие дробления;
    /// 61 – Создание нового адресного объекта в результате дробления
    Property OPERSTATUS As %Integer(XMLNAME = "OPERSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ];

    /// Статус актуальности КЛАДР 4 (последние две цифры в коде)
    Property CURRSTATUS As %Integer(XMLNAME = "CURRSTATUS", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ];

    /// Начало действия записи
    Property STARTDATE As %Date(XMLNAME = "STARTDATE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Окончание действия записи
    Property ENDDATE As %Date(XMLNAME = "ENDDATE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Внешний ключ на нормативный документ
    Property NORMDOC As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "NORMDOC", XMLPROJECTION = "ATTRIBUTE");

    /// Признак действующего адресного объекта
    Property LIVESTATUS As %xsd.byte(VALUELIST = ",0,1", XMLNAME = "LIVESTATUS", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Тип адресации:
    /// 0 - не определено
    /// 1 - муниципальный;
    /// 2 - административно-территориальный
    Property DIVTYPE As %xsd.int(VALUELIST = ",0,1,2", XMLNAME = "DIVTYPE", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    Relationship AddressObjects As Test.AddressObjects(XMLPROJECTION = "NONE") [ Cardinality = one, Inverse = Object ];
}


Из всего перечня xml-файлов в ФИАС мы будем использовать только файл с наименованиями регионов, городов и улиц. На момент подготовки публикации у меня был этот:
AS_ADDROBJ_20190106_90809714-fe22-45b2-929c-52bd950963e0.XML

Размер файла ни много, ни мало, а почти 3 ГБ. Обычными инструментами работы с текстами не откроешь – не переваривают они такой размер.
Кстати, максимальная длина строкового литерала (тип String) в InterSystems IRIS не более 3,641,144 символов. То есть загружать напрямую файл или URL в него не получится. Другие ограничения можно подсматривать в документации. Для работы с большими объемами данных можно использовать потоки данных (streams) которые не имеют такого ограничения по длине.
Посмотрим, что у нас получится?

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

Class FIAS.AddressObject Extends (%Persistent, %XML.Adaptor) [ ProcedureBlock ] {
 
    Parameter XMLNAME = "Object";

    Parameter XMLSEQUENCE = 1;

    /// Глобальный уникальный идентификатор адресного объекта
    Property AOGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOGUID", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Официальное наименование
    Property OFFNAME As %String(MAXLEN = 120, MINLEN = 1, XMLNAME = "OFFNAME", XMLPROJECTION = "ATTRIBUTE");

    /// Почтовый индекс
    Property POSTALCODE As %String(MAXLEN = 6, MINLEN = 6, XMLNAME = "POSTALCODE", XMLPROJECTION = "ATTRIBUTE");

    /// Краткое наименование типа объекта
    Property SHORTNAME As %String(MAXLEN = 10, MINLEN = 1, XMLNAME = "SHORTNAME", XMLPROJECTION = "ATTRIBUTE") [ Required ];

    /// Уровень адресного объекта
    Property AOLEVEL As %Integer(XMLNAME = "AOLEVEL", XMLPROJECTION = "ATTRIBUTE", XMLTotalDigits = 10) [ Required ];

    /// Идентификатор объекта родительского объекта
    Property PARENTGUID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "PARENTGUID", XMLPROJECTION = "ATTRIBUTE");

    /// Уникальный идентификатор записи. Ключевое поле.
    Property AOID As %String(MAXLEN = 36, MINLEN = 36, XMLNAME = "AOID", XMLPROJECTION = "ATTRIBUTE") [ Required ];

Далее делаем по писаному. Создаём объект, который понимает XML как родной – используем класс из системной библиотеки %XML.Reader:

set reader = ##class(%XML.Reader).%New()

И даём ему наставление, кого брать, а остальное игнорировать. Принимать будем по одной порции:

do reader.Correlate("Object","FIAS.AddressObject")

Дальше есть вариации как получить исходный мкБД файл. Если удобно, можно положить рядом с хранилищем – локально в файловой системе сервера IRIS. Или, как в моём примере, попросить прислать по HTTP. Есть ещё более универсальный вариант, о котором будет пару слов ниже.

set url="http://localhost/AS_ADDROBJ_20190106_90809714-fe22-45b2-929c-52bd950963e0.XML"   
write reader.OpenUrl(url)

Важно! В этот момент, у большинства, кто будет проходить этот пример на себе, возникнет страшное. Система вернёт вместо радостной «1» (всё в порядке), что-то начинающееся с «0 ¸ STORE…». И это не обрадует. То есть файл с вроде бы мкБД, оказался не совсем микро и не влезает в наш объект. Выделенной на него памяти не хватило. Решаемо? Конечно. Платформа данных IRIS позволяет в оперативной памяти создавать объекты размером до 4 ТБ. Тогда что пошло не так? По умолчанию в настройках системы установлен размер в 256 МБ на объект. А нам надо бы сильно больше. И помните, это требования к ОЗУ. На вашем компьютере/сервере найдётся достаточно запаса?
Какой размер памяти на размещение этого гиганта нам понадобится установил опытным путем – почти 10 ГБ. Что требуется указать в настройках (Меню > Настроить память > Максимальный объем памяти в расчете на процесс (КБ)) или через системную переменную $ZSTORAGE (в килобайтах):

set $ZSTORAGE=10000000

Запустили новый процесс с нужными настройками памяти? Тогда дальше всё просто – читаем и сохраняем.

Есть и альтернативный (и, наверное, предпочтительный) вариант — использовать свойство UsePPGHandler класса %XML.Reader которое позволяет не хранить в памяти XML и работает со стандартными настройками памяти.

set reader = ##class(%XML.Reader).%New()
set reader.UsePPGHandler = 1

далее … Correlate / Read и т.д. …

do reader.Next(.object)
do object.%Save()

И так 3 722 548 раз на каждую операцию :-)

Это утомительно. Поэтому дополним наш класс FIAS.AddressObject методом для импорта, на основе только что показанных команд:

ClassMethod Import() {
        // Создать объект для чтения XML
        Set reader = ##class(%XML.Reader).%New()

        // Получить исходный XML для разбора
        Set status = reader.OpenURL("http://localhost/AS_ADDROBJ_20190106_90809714-fe22-45b2-929c-52bd950963e0.XML")
        If $$$ISERR(status) {Do $System.Status.DisplayError(status)}

        // Связать объект с нужной структурой выборки
        Do reader.Correlate("Object","FIAS.AddressObject")
    
        // Читать и сохранять объект в хранилище
        While (reader.Next(.object,.status)) {
            Set  status = object.%Save()
                                 If $$$ISERR(status) {do $System.Status.DisplayError(status)}
        }
    
       // В случае возникновения ошибки при разборе, показать сообщение
        If $$$ISERR(status) {Do $System.Status.DisplayError(status)}
    }

Воспользуемся мощью нашего компьютерного экзокортекса – всего одна команда в терминале:

do ##class(FIAS.AddressObject).Import()



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



И наконец пара слов о том, когда 4TБ не достаточно. В таком случае идём за потоками (или стримами если угодно). В документации всё разложено по полочкам. Можно бинарные, можно символьные. Хранить в глобале тоже не возбраняется. Рецепт такой: берём поток, режем по частям и отдаём в нужные нам объекты на потребление.

Дальнейшее, про красивые адресные ObjectScript объекты и API на Python не поместилось. Будет отдельная история.
Приятно: Гартнер только что завершил ежегодный сбор реальных пользовательских оценок и отзывов в категории СУБД и на этой основе опубликовал свой рейтинг лучших СУБД 2019 года. Продукты InterSystems Caché и InterSystems IRIS Data Platform получили высшую оценку «Выбор потребителей». Из кого выбирали и как оценили можете заглянуть сами.
Best Operational Database Management Systems Software of 2019 as Reviewed by Customers

Теги:
Хабы:
+8
Комментарии 7
Комментарии Комментарии 7

Публикации

Истории

Работа

Data Scientist
66 вакансий

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн
Геймтон «DatsEdenSpace» от DatsTeam
Дата 5 – 6 апреля
Время 17:00 – 20:00
Место
Онлайн