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

Как поместить весь мир в обычный ноутбук: PostgreSQL и OpenStreetMap

Время на прочтение29 мин
Количество просмотров26K

Когда человек раньше говорил что он контролирует весь мир, то его обычно помещали в соседнюю палату с Бонапартом Наполеоном. Надеюсь, что эти времена остались в прошлом и каждый желающий может анализировать геоданные всей Земли и получать ответы на свои глобальные вопросы за минуты и секунды. Я опубликовал Openstreetmap_h3 — свой проект, который позволяет производить геоаналитику над данными из OpenStreetMap в PostGIS или в движке запросов, способном работать с Apache Arrow/Parquet.

Первым делом передаю привет хейтерам и скептикам. То что я разработал — действительно уникально и решает проблему преобразования и анализа геоданных используя обычные и привычные инструменты доступные каждому аналитику и датасаенс специалисту без бигдат, GPGPU, FPGA. То что выглядит сейчас простым в использовании и в коде — это мой личный проект в который я инвестировал свои отпуска, выходные, бессонные ночи и уйму личного времени за последние 3 года. Может быть я поделюсь и предысторией проекта и граблями по которым ходил, но сначала я все же опишу конечный результат.

Первый пост не претендует на монографию, начну с краткого обзора. Планирую поделиться своим опытом на конференциях и в публикациях, а тема хоть и нишевая, но будет полезна многим проектам, работающих с геоданными из OSM. Для программистов и пользователей, не сталкивавшихся с OSM инструментарием и моделью данных многие вещи могут показаться немного странными. Пока я не буду углубляться в эти основы. Отмечу лишь, что несмотря на его критику сам OpenStreetMap и сервисы развиваются и объем данных увеличивается год от года. В некоторых регионах планеты полнота и качество данных OSM лучше чем у других проектов. Поэтому просто примем модель данных такой какая она есть — гибкая и не ограничивающая пользователя в разметке, усложняющая решения для аналитики и визуализации данных за счёт особенностей дизайна структур данных.

Есть специализированные колоночные, распределенные opensource БД с которыми я также экспериментировал, но все же PostgreSQL+PostGIS на данный момент оказались самыми надёжными и протестированными доступными решениями. Ну и вишенкой на торте: эта же схема данных в Postgresql всего лишь одним параметром может загружаться в колоночное хранилище CitusDB внутри того же PostgreSQL.

С анализом геоданных вы сталкиваетесь каждый день

Когда вы пользуетесь такси в небольших городах, вызывая машину по телефону, то с высокой вероятностью вашу поездку тарифицирует программа на основе данных OSM. Для тарификации используется какой-либо из пакетов прокладки маршрута. Благодаря этому сценарию использования, сотрудники таксопарка указывают номер дома и улицу на зданиях и делают вклад не только в свой бизнес, но и в OpenStreetMap.

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

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

Ну и можно ответить на любой вопрос по географии который вам придет в голову: посчитать площади городов и построек, протяженности дорог и извлечь названия городов, областей и островов. Можете, например, стать чемпионом по игре в "Города" или основать новый сервис прокатов электро самокатов. Все ограничивается лишь вашей фантазией.

Зачем этот проект и чем не устраивали существующие решения

Попытки загрузить дамп планеты в PostgreSQL базу были долгие и мучительные... Утилита Osmosis написана на java, имеет богатый функционал по работе с данными OSM и в том числе позволяет загружать эти данные в PostgreSQL базу данных со структурой pgSnapshot для аналитики. Этот проект больше не развивается по функционалу, только на поддержке. Позволяет преобразовывать данные из PBF в tsv файлы для копирования в базу данных и есть скрипты для создания схемы БД. Схема pgSnapshot lossless, что позволяет анализировать всю доступную информацию без потерь, строить маршруты итп. Но главный минус — огромные таблицы nodes, ways на сотни гигабайт, что не позволяет производить параллельную загрузку данных в таблицу сохраняя физический порядок записей, долгое построение гигантских по объему индексов, длительный full scan по гигантской таблице. Osm2pgsql, Imposm позволяет загружать данные в PostGIS, импорт данных происходит по мэппингу с потерей части данных, не подходит для роутинга и больше ориентирована для визуализации тайлов или работы с подмножеством трансформированных данных в таблицы определенные пользователем. Osmium export делает скрипты быстрее чем Osmosis, но функционал PostGIS экспорта ограничен и модель данных примитивнее по структуре чем в Osmosis.

Когда рабочая лошадка не утянет, пора запрягать слона (PostgreSQL+PostGIS)

Хотелось бы иметь данные в PostgreSQL в формате максимально близком к pgSnapshot, с быстрым механизмом загрузки данных из PBF и возможности параллельной загрузки в БД. Было бы идеально чтобы данные были секционированы так чтобы внутри одной секции были объекты расположенные близко друг к другу и сетка разбиения была приблизительно одинаковой по радиусу, как у экватора, так и у полюсов планеты. Из систем геопартиционирования рассматривал варианты — деления по административным границам/странам, разбиение прямоугольной сеткой, иерархические системы S2 и H3. И выбор был сделан в пользу последней, так как эта иерархическая система индексирования H3 имеет преимущества в сохранении расстояний (приблизительно одинаковый радиус сегмента в любой точке земного шара), отлично подходит для задач маршрутизации, библиотеки для работы с H3 хорошо поддерживаются и постоянно развиваются и доступны для PostgreSQL/JVM.

В итоге я разработал программу на Java и спроектировал схему БД совместимую с pgSnapshot, которая использует декларативное партиционирование данных PostgreSQL на основе диапазонов H3 индекса координат объектов. Утилиту, которая может автоматически создавать скрипты для схемы БД, подготавливать данные для загрузки из PBF в таблицы и загружать их в БД многопоточно.

Это был путь многочисленных экспериментов, начинал с модификации кода Osmosis и osm‑parquetizer. Собирал координаты точек линии используя memory mapped файл на сотню гигабайт в Java коде, чтобы затем выбросить этот свой код и переиспользовать функционал add‑locations‑to‑ways из osmium — наиболее оптимизированного доступного решения которое делает это же гораздо быстрее. К тому же osmium умеет собирать мультиполигоны, что гораздо быстрее и надежнее чем делать это на уровне БД, здесь тоже решил положиться на протестированное сообществом решение.

Если вас все ещё тянет в бигдату призываем на помощь Apache Arrow

У нас теперь есть секционированные по H3 индексам геоданные в PostgreSQL и вся надежность и качество этой экосистемы. Но если нам нужно передать эти же секционированные данные в Apache Spark или Hadoop экосистему, то хотелось бы сразу получить данные из PBF в формате Arrow/Parquet без промежуточного PostgreSQL. Учитывая свой предыдущий опыт оптимизаций osm‑parquetizer, я добавил в свою утилиту возможность экспорта геоданных через Apache Arrow формат сериализации. Для получения Parquet файлов, партиционированных по H3 индексу нужен дополнительный шаг постобработки результирующего набора файлов, из-за недоработки в Arrow Java API (пока что делаю это дополнительным python скриптом постпроцессинга). Из преимуществ моей утилиты перед osm‑parquetizer — больший параллелизм в трансформации PBF данных, сборка координат для линий и наличие геоиндекса H3 для nodes, ways. Одна проблема выбора для геоаналитики в parquet — это как сохранять линии. Некоторые из альтернативных решений сохраняют их как массив точек, я же пока решил кодировать линии в WKB формате.

Openstreetmap_h3 написана на Java

Как я упомянул выше, этот проект возник эволюционно из Osmosis и osm‑parquetizer и был переписан «с нуля» учитывая все грабли, которые собрал в этих Java проектах. Скрипт сборки проекта на maven, а само приложение написано как утилита командной строки:

Основная логика проекта
package com.github.isuhorukov.osm.pgsnapshot;

import com.beust.jcommander.JCommander;
import com.github.isuhorukov.osm.pgsnapshot.model.*;
import com.github.isuhorukov.osm.pgsnapshot.model.statistics.BlockStat;
import com.github.isuhorukov.osm.pgsnapshot.model.statistics.MultipolygonTime;
import com.github.isuhorukov.osm.pgsnapshot.model.statistics.PbfStatistics;
import com.github.isuhorukov.osm.pgsnapshot.model.statistics.Stat;
import com.github.isuhorukov.osm.pgsnapshot.model.table.StatType;
import com.github.isuhorukov.osm.pgsnapshot.model.table.TableStat;
import com.github.isuhorukov.osm.pgsnapshot.util.CompactH3;
import com.github.isuhorukov.osm.pgsnapshot.util.PartitionSplitter;
import com.google.common.util.concurrent.MoreExecutors;
import com.uber.h3core.H3Core;
import com.uber.h3core.util.LatLng;
import net.postgis.jdbc.geometry.LineString;
import net.postgis.jdbc.geometry.LinearRing;
import net.postgis.jdbc.geometry.Point;
import net.postgis.jdbc.geometry.Polygon;
import net.postgis.jdbc.geometry.binary.BinaryWriter;
import org.apache.arrow.memory.ArrowBuf;
import org.apache.arrow.memory.BufferAllocator;
import org.apache.arrow.memory.RootAllocator;
import org.apache.arrow.vector.*;
import org.apache.arrow.vector.complex.ListVector;
import org.apache.arrow.vector.complex.MapVector;
import org.apache.arrow.vector.complex.impl.UnionListWriter;
import org.apache.arrow.vector.complex.impl.UnionMapWriter;
import org.apache.arrow.vector.ipc.ArrowFileWriter;
import org.apache.arrow.vector.types.FloatingPointPrecision;
import org.apache.arrow.vector.types.pojo.ArrowType;
import org.apache.arrow.vector.types.pojo.Field;
import org.apache.arrow.vector.types.pojo.FieldType;
import org.apache.arrow.vector.types.pojo.Schema;
import org.apache.commons.io.IOUtils;
import org.geotools.geometry.jts.JTS;
import org.geotools.referencing.CRS;
import org.h2gis.functions.spatial.properties.ST_IsClosed;
import org.locationtech.jts.algorithm.MinimumBoundingCircle;
import org.locationtech.jts.geom.*;
import org.locationtech.jts.io.WKBWriter;
import org.opengis.referencing.FactoryException;
import org.opengis.referencing.crs.CoordinateReferenceSystem;
import org.opengis.referencing.operation.TransformException;
import org.openstreetmap.osmosis.core.container.v0_6.EntityContainer;
import org.openstreetmap.osmosis.core.container.v0_6.NodeContainer;
import org.openstreetmap.osmosis.core.container.v0_6.RelationContainer;
import org.openstreetmap.osmosis.core.container.v0_6.WayContainer;
import org.openstreetmap.osmosis.core.domain.v0_6.Entity;
import org.openstreetmap.osmosis.core.domain.v0_6.RelationMember;
import org.openstreetmap.osmosis.core.domain.v0_6.Tag;
import org.openstreetmap.osmosis.core.domain.v0_6.WayNode;
import org.openstreetmap.osmosis.pbf2.v0_6.impl.PbfBlobDecoder;
import org.openstreetmap.osmosis.pbf2.v0_6.impl.PbfBlobDecoderListener;
import org.openstreetmap.osmosis.pbf2.v0_6.impl.RawBlob;
import org.openstreetmap.osmosis.pgsnapshot.v0_6.impl.MemberTypeValueMapper;

import java.io.*;
import java.nio.charset.StandardCharsets;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;

import static java.util.stream.Collectors.toList;

public class OsmPbfTransformation {

    public static final String NODES_DIR = "nodes";
    public static final String RELATIONS_DIR = "relations";
    public static final String WAYS_DIR = "ways";
    public static final String MULTIPOLYGON_DIR = "multipolygon";
    public static final String SQL_DIR = "sql";
    public static final String ARROW_DIR = "arrow";
    public static final String IMPORT_RELATED_METADATA_DIR = "import_related_metadata";
    public static final String STATIC_DIR = "static";

    public static void main(String[] args) throws Exception{

        CliParameters parameters = parseCliArguments(args);
        if (parameters == null){
            return;
        }
        long commandStartTime = System.currentTimeMillis();

        String sourceFilePath = parameters.sourceFilePath;
        File sourcePbfFile = new File(sourceFilePath);
        if(!sourcePbfFile.exists() || sourcePbfFile.length()==0){
            throw new IllegalArgumentException("Input pbf should exists and should be non empty");
        }

        Splitter.Blocks blocks =ExternalProcessing.enrichSourcePbfAndSplitIt(sourcePbfFile);

        File inputDirectory = new File(blocks.getDirectory());
        File[] files = inputDirectory.listFiles();
        Arrays.sort(Objects.requireNonNull(files));
        File resultDirectory = prepareResultDirectories(new File(inputDirectory.getParent(),
                                                            resultDirectoryNameFromSource(inputDirectory)),
                parameters.savePostgresqlTsv, parameters.saveArrow);

        copyOsmiumSettings(resultDirectory);
        if(parameters.savePostgresqlTsv) {
            copyResources(resultDirectory, parameters.columnarStorage);
        }

        long processingStartTime = System.currentTimeMillis();

        final H3Core h3Core = H3Core.newInstance();


        ExecutorService saveExecutorService = getExecutorService(parameters.workers);
        ExecutorService executorService = getExecutorService(parameters.workers);

        Map<Long, BlockStat> blockStat= new ConcurrentHashMap<>();
        AtomicInteger currentBlockToSave= new AtomicInteger(0);
        for(File blockFile: files){

            executorService.submit(() -> {
                long threadStart = System.currentTimeMillis();
                Long blockNumber = Long.parseLong(blockFile.getName());
                if(blockNumber%1000==0){
                    System.out.println(blockNumber);
                }
                RawBlob rawBlob;
                try {
                    FileInputStream blobInputStream = new FileInputStream(blockFile);
                    rawBlob = new RawBlob("OSMData", IOUtils.toByteArray(blobInputStream));
                } catch (IOException e) {
                    throw new IllegalArgumentException(e);
                }

                PbfBlobDecoder blobDecoder = new PbfBlobDecoder(rawBlob, new PbfBlobDecoderListener() {
                    @Override
                    public void complete(List<EntityContainer> decodedEntities) {
                        long blockStartTime = System.currentTimeMillis();
                        GeometryFactory geometryFactory = new GeometryFactory();
                        MemberTypeValueMapper memberTypeValueMapper = new MemberTypeValueMapper();
                        Map<Short, StringBuilder> csvResultPerH33 =new HashMap<>();
                        BinaryWriter binaryWriter = new BinaryWriter();
                        ArrayList<ArrowNodeOrWay> arrowNodeOrWays = new ArrayList<>();
                        ArrayList<ArrowRelation> arrowRelations = new ArrayList<>();
                        WKBWriter wkbWriter = new WKBWriter();
                        final CoordinateReferenceSystem coordinateReferenceSystem;
                        try {
                            coordinateReferenceSystem = CRS.decode("EPSG:" + Serializer.SRID);
                        } catch (FactoryException e) {
                            throw new RuntimeException(e);
                        }

                        Map<Short, Stat> nodeStat =new HashMap<>();
                        long nodeRecords = decodedEntities.stream().
                                filter(entityContainer -> entityContainer instanceof NodeContainer).
                            map(entityContainer -> ((NodeContainer) entityContainer).getEntity()).map(entity -> {
                                prepareNodeData(csvResultPerH33, binaryWriter, arrowNodeOrWays,
                                        nodeStat, entity, h3Core, parameters.collectOnlyStat, parameters.saveArrow, parameters.savePostgresqlTsv);
                                return null;
                            }).filter(Objects::isNull).count();

                        Map<Short, Stat> wayStat =new HashMap<>();
                        long wayRecords = decodedEntities.stream().
                                filter(entityContainer -> entityContainer instanceof WayContainer).
                            map(entityContainer -> ((WayContainer) entityContainer).getEntity()).map(entity -> {
                            prepareWayData(geometryFactory, csvResultPerH33, binaryWriter,  wkbWriter,
                                    arrowNodeOrWays, wayStat, entity, h3Core,
                                    parameters.scaleApproximation, parameters.collectOnlyStat, parameters.skipBuildings,
                                    coordinateReferenceSystem, parameters.saveArrow, parameters.savePostgresqlTsv);
                                return null;
                            }).filter(Objects::isNull).count();
                        BlockStat blockStatistic = new BlockStat(blockNumber);
                        blockStatistic.setThreadStart(threadStart);
                        if(!nodeStat.isEmpty()) {
                            blockStatistic.setNodeStat(nodeStat);
                        }
                        if(!wayStat.isEmpty()) {
                            blockStatistic.setWayStat(wayStat);
                        }
                        long relationCount = decodedEntities.stream().
                            filter(entityContainer -> entityContainer instanceof RelationContainer).
                            map(entityContainer -> ((RelationContainer) entityContainer).getEntity()).
                            map(entity -> {
                                if(!parameters.collectOnlyStat) {
                                    long relationId = entity.getId();
                                    if(parameters.savePostgresqlTsv){
                                        StringBuilder relationCsv = csvResultPerH33.computeIfAbsent((short)0, h33Key -> new StringBuilder());
                                        Serializer.serializeRelation(relationCsv, relationId, entity.getTags());
                                    }

                                    ArrowRelation arrowRelation = null;
                                    if(parameters.saveArrow){
                                        arrowRelation = new ArrowRelation(relationId, TagsUtil.tagsToMap(entity.getTags()));
                                        arrowRelations.add(arrowRelation);
                                    }

                                    List<RelationMember> relationMembers = entity.getMembers();
                                    for(int sequenceId=0; sequenceId<relationMembers.size();sequenceId++){
                                        RelationMember relationMember = relationMembers.get(sequenceId);
                                        long memberId = relationMember.getMemberId();
                                        String memberType = memberTypeValueMapper.getMemberType(relationMember.getMemberType());
                                        String memberRole = relationMember.getMemberRole();
                                        if(parameters.saveArrow){
                                            arrowRelation.getRelationMembers().add(
                                                    new ArrowRelationMember(memberId, memberType.charAt(0), memberRole));
                                        }
                                        if(parameters.savePostgresqlTsv){
                                            StringBuilder relationMembersCsv = csvResultPerH33.computeIfAbsent((short)1, h33Key -> new StringBuilder());
                                            Serializer.serializeRelationMembers(relationMembersCsv, relationId,
                                                    memberId, memberType, memberRole, sequenceId);
                                        }
                                    }
                                }
                                return null;
                            }).count();
                        long multipolygonCount = relationCount==0 ? 0 : decodedEntities.stream().
                            filter(entityContainer -> entityContainer instanceof RelationContainer).
                            map(entity -> {
                                for(Tag tag: entity.getEntity().getTags()){
                                    if("type".equals(tag.getKey()) && "multipolygon".equals(tag.getValue())){
                                        return 1;
                                    }
                                }
                                return 0;
                            }).mapToLong(Integer::longValue).sum();
                        long relationMemberCount = decodedEntities.stream().
                                filter(entityContainer -> entityContainer instanceof RelationContainer).
                                map(entityContainer -> ((RelationContainer) entityContainer).getEntity()).
                                mapToLong(value -> value.getMembers().size()).sum();
                        blockStatistic.setNodeCount(nodeRecords);
                        blockStatistic.setWayCount(wayRecords);
                        blockStatistic.setRelationCount(relationCount);
                        blockStatistic.setRelationMembersCount(relationMemberCount);
                        blockStatistic.setMultipolygonCount(multipolygonCount);
                        blockStat.put(blockNumber, blockStatistic);
                        blockStatistic.setProcessingTime(System.currentTimeMillis()-blockStartTime);

                        if(!parameters.collectOnlyStat) {
                            if(parameters.saveArrow){
                                long startSaveTime = System.currentTimeMillis();
                                if(!arrowNodeOrWays.isEmpty()){
                                    saveArrowNodesOrWays(arrowNodeOrWays, blockNumber, new File(resultDirectory,ARROW_DIR));
                                }
                                if(!arrowRelations.isEmpty()){
                                    saveArrowRelations(arrowRelations, blockNumber, new File(resultDirectory, ARROW_DIR));
                                }
                                blockStatistic.setSaveTime(System.currentTimeMillis()-startSaveTime);
                            }
                            if(parameters.savePostgresqlTsv){
                                saveDataOnlyInOneThread(csvResultPerH33, nodeRecords, wayRecords,
                                    blockStatistic, relationCount,
                                    currentBlockToSave, blockNumber, resultDirectory, saveExecutorService);
                            }
                        }
                    }

                    @Override
                    public void error() {
                        System.out.println("ERROR in block "+blockNumber);
                    }
                });
                blobDecoder.run();
                long threadTime = System.currentTimeMillis() - threadStart;
                blockStat.get(blockNumber).setThreadTime(threadTime);
            });
        }
        executorService.shutdown();
        executorService.awaitTermination(2,TimeUnit.DAYS);
        saveExecutorService.shutdown();//stop executor only when all tasks in processing executor is finished

        List<BlockStat> blockStatistics = new ArrayList<BlockStat>(blockStat.values());
        long multipolygonCount = blockStatistics.stream().map(BlockStat::getMultipolygonCount).mapToLong(Long::longValue).sum();
        long dataProcessingTime = System.currentTimeMillis() - processingStartTime;
        System.out.println(files.length+" "+" time "+dataProcessingTime);
        System.out.println("diff between total and processing " + blockStatistics.stream().map(blockStat1 -> blockStat1.getThreadTime()-blockStat1.getProcessingTime()).mapToLong(Long::longValue).sum());
        System.out.println("total thread time "+ blockStatistics.stream().map(BlockStat::getThreadTime).mapToLong(Long::longValue).sum());
        System.out.println("total processing time "+ blockStatistics.stream().map(BlockStat::getProcessingTime).mapToLong(Long::longValue).sum());
        System.out.println("total save time "+ blockStatistics.stream().map(BlockStat::getSaveTime).mapToLong(Long::longValue).sum());
        System.out.println("total waiting for save time "+ blockStatistics.stream().map(BlockStat::getWaitingForSaveTime).mapToLong(Long::longValue).sum());
        System.out.println("thread max time "+ blockStatistics.stream().map(BlockStat::getThreadTime).mapToLong(Long::longValue).max().orElse(0));
        System.out.println("processing max time "+ blockStatistics.stream().map(BlockStat::getProcessingTime).mapToLong(Long::longValue).max().orElse(0));
        System.out.println("nodes "+ blockStatistics.stream().map(BlockStat::getNodeCount).mapToLong(Long::longValue).sum());
        System.out.println("ways "+ blockStatistics.stream().map(BlockStat::getWayCount).mapToLong(Long::longValue).sum());
        System.out.println("relations "+ blockStatistics.stream().map(BlockStat::getRelationCount).mapToLong(Long::longValue).sum());
        System.out.println("relation members "+ blockStatistics.stream().map(BlockStat::getRelationMembersCount).mapToLong(Long::longValue).sum());
        System.out.println("multipolygon count "+ multipolygonCount);


        if(!parameters.collectOnlyStat) {
            savePartitioningScripts(resultDirectory, parameters.scriptCount,
                    parameters.thresholdPercentFromMaxPartition, blockStatistics, parameters.columnarStorage);
        }

        MultipolygonTime multipolygonTime = new MultipolygonTime(); //multipolygonCount calculation is only one reason why this generator at the end of process
        if(!parameters.collectOnlyStat) {
            multipolygonTime = ExternalProcessing.prepareMultipolygonDataAndScripts(sourcePbfFile,
                    resultDirectory, parameters.scriptCount, multipolygonCount);
        }
        PbfStatistics statistics = new PbfStatistics(blockStatistics);
        statistics.setMultipolygonCount(multipolygonCount);
        statistics.setDataProcessingTime(dataProcessingTime);
        statistics.setAddLocationsToWaysTime(blocks.getAddLocationsToWaysTime());
        statistics.setPbfSplitTime(blocks.getPbfSplitTime());
        statistics.setMultipolygonExportTime(multipolygonTime.getMultipolygonExportTime());
        statistics.setSplitMultipolygonByPartsTime(multipolygonTime.getSplitMultipolygonByPartsTime());
        statistics.setTotalTime(System.currentTimeMillis()-commandStartTime);

        saveStatistics(resultDirectory, statistics);

    }
}

Все это позволит в будущем легко перенести обработку в Apache Spark, что позволит еще больше повысить параллелизм обработки секций PBF дампа в распределенной среде. Но опять же надо доделать прототип и измерить производительность — стоит ли игра свеч. Для генерации parquet файлов такой подход по моему мнению будет иметь смысл, а для PostgreSQL TSV файлов вряд ли.

Первые шаги с Openstreetmap_h3

Итак, все что нам потребуется для начала: база данных PostgreSQL 14, данные проекта Openstreetmap и обычный ноутбук. В моем случае это Dell Latitude 7320: i7–1165G7@2.80GHz, 16Гб ОЗУ с 2Тб NVMe накопителем. Проект работает как с дампом всего мира в формате PBF, так и региональными выгрузками c Geofabrik

Клонируем и собираем проект:

git clone https://github.com/igor-suhorukov/openstreetmap_h3.git
cd openstreetmap_h3
mvn install

Для работы проект требует Docker. При запуске отображается список доступных параметров и их описание:

~/dev/projects/oss_contrib/openstreetmap_h3$ java -jar target/osm-to-pgsnapshot-schema-ng-1.0-SNAPSHOT.jar
The following option is required: [-source_pbf]
Usage: <main class> [options]
  Options:
  * -source_pbf
      Source path for OpenStreetMap data in PBF format
    -collect_only_statistics
      Collect only statistics from data - partition distribution
      Default: false
    -columnar_storage
      Use columnar storage in PostgreSql tables for nodes/ways/multipolygon
      Default: false
    -data_partition_ratio
      Filling ratio from maximum size of partition. This parameter change 
      PostgreSQL partitions count
      Default: 0.48
    -help
      Information about command line parameters
    -pg_script_count
      Script count for PostgreSQL parallel COPY
      Default: 4
    -result_in_arrow
      Save data transforming and enrichment in Apache Arrow format
      Default: false
    -result_in_tsv
      Save result data in TabSeparatedValue format for PostgreSQL COPY
      Default: true
    -scale_approx_calc
      Approximate scale calculation. Value 'false' - distance in meter
      Default: false
    -skip_buildings
      Skip any ways with 'building' tag
      Default: false
    -worker_threads
      Worker threads count for data processing
      Default: 4

После этого загружаем OSM PBD дамп планеты или какую‑либо одну страну с Geofabric

java -jar target/osm-to-pgsnapshot-schema-ng-1.0-SNAPSHOT.jar -source_pbf /home/acc/dev/map/thailand/thailand-latest.osm.pbf
В результате получаем набор скриптов для параллельной загрузки в БД:
~/dev/map/thailand/thailand-latest_loc_ways$ tree
.
├── import_related_metadata
│   ├── osm_file_block_content.tsv
│   ├── osm_file_block.tsv
│   └── osm_file_statistics.tsv
├── multipolygon
│   ├── multipolygon_aa
│   ├── multipolygon_ab
│   ├── multipolygon_ac
│   ├── multipolygon_ad
│   └── multipolygon_ae
├── nodes
│   ├── 16713.tsv
│   ├── 16717.tsv
│   ├── 16744.tsv
│   ├── 16745.tsv
│   ├── 16747.tsv
│   ├── 16748.tsv
│   ├── 16749.tsv
│   ├── 25600.tsv
│   ├── 25601.tsv
│   ├── 25606.tsv
│   ├── 25620.tsv
│   ├── 25622.tsv
│   ├── 25632.tsv
│   ├── 25634.tsv
│   ├── 25636.tsv
│   ├── 25638.tsv
│   ├── 25648.tsv
│   ├── 25649.tsv
│   ├── 25650.tsv
│   ├── 25651.tsv
│   ├── 25652.tsv
│   ├── 25653.tsv
│   ├── 25654.tsv
│   ├── 25728.tsv
│   ├── 25729.tsv
│   ├── 25730.tsv
│   ├── 25731.tsv
│   ├── 25732.tsv
│   ├── 25733.tsv
│   ├── 25734.tsv
│   ├── 25744.tsv
│   ├── 25745.tsv
│   ├── 25748.tsv
│   ├── 25749.tsv
│   ├── 25756.tsv
│   ├── 25760.tsv
│   ├── 25761.tsv
│   ├── 25762.tsv
│   ├── 25763.tsv
│   ├── 25764.tsv
│   ├── 25765.tsv
│   ├── 25766.tsv
│   ├── 25774.tsv
│   ├── 25776.tsv
│   ├── 25777.tsv
│   ├── 25778.tsv
│   ├── 25779.tsv
│   ├── 25780.tsv
│   ├── 25781.tsv
│   ├── 25782.tsv
│   ├── 25873.tsv
│   ├── 25880.tsv
│   ├── 25881.tsv
│   ├── 25882.tsv
│   ├── 25883.tsv
│   ├── 25884.tsv
│   ├── 25885.tsv
│   ├── 25886.tsv
│   ├── 25985.tsv
│   ├── 25989.tsv
│   ├── 25992.tsv
│   ├── 25993.tsv
│   ├── 25994.tsv
│   ├── 25995.tsv
│   ├── 25996.tsv
│   ├── 25998.tsv
│   ├── 26000.tsv
│   ├── 26001.tsv
│   ├── 26003.tsv
│   ├── 26008.tsv
│   ├── 26009.tsv
│   ├── 26010.tsv
│   ├── 26011.tsv
│   ├── 26012.tsv
│   ├── 26013.tsv
│   ├── 26014.tsv
│   ├── 26019.tsv
│   ├── 26026.tsv
│   ├── 26029.tsv
│   └── 26030.tsv
├── relations
│   ├── 00000.tsv
│   └── 00001.tsv
├── sql
│   ├── nodes_import_000.sql
│   ├── nodes_import_001.sql
│   ├── nodes_import_002.sql
│   ├── nodes_import_003.sql
│   ├── ways_import_000.sql
│   ├── ways_import_001.sql
│   ├── ways_import_002.sql
│   ├── ways_import_003.sql
│   ├── y_multipoly_aa.sql
│   ├── y_multipoly_ab.sql
│   ├── y_multipoly_ac.sql
│   ├── y_multipoly_ad.sql
│   └── y_multipoly_ae.sql
├── static
│   ├── database_after_init.sql
│   ├── database_init.sql
│   ├── h3_poly.tsv.gz
│   ├── multipolygon.sql
│   ├── multipolygon_tables.sql
│   └── osmium_export.json
└── ways
    ├── 16717.tsv
    ├── 16744.tsv
    ├── 16745.tsv
    ├── 16747.tsv
    ├── 16748.tsv
    ├── 16749.tsv
    ├── 25620.tsv
    ├── 25622.tsv
    ├── 25634.tsv
    ├── 25638.tsv
    ├── 25648.tsv
    ├── 25649.tsv
    ├── 25650.tsv
    ├── 25651.tsv
    ├── 25652.tsv
    ├── 25653.tsv
    ├── 25654.tsv
    ├── 25728.tsv
    ├── 25729.tsv
    ├── 25730.tsv
    ├── 25731.tsv
    ├── 25732.tsv
    ├── 25733.tsv
    ├── 25734.tsv
    ├── 25744.tsv
    ├── 25745.tsv
    ├── 25748.tsv
    ├── 25749.tsv
    ├── 25756.tsv
    ├── 25760.tsv
    ├── 25761.tsv
    ├── 25762.tsv
    ├── 25763.tsv
    ├── 25764.tsv
    ├── 25765.tsv
    ├── 25766.tsv
    ├── 25774.tsv
    ├── 25776.tsv
    ├── 25777.tsv
    ├── 25778.tsv
    ├── 25779.tsv
    ├── 25780.tsv
    ├── 25781.tsv
    ├── 25782.tsv
    ├── 25873.tsv
    ├── 25880.tsv
    ├── 25881.tsv
    ├── 25882.tsv
    ├── 25883.tsv
    ├── 25884.tsv
    ├── 25886.tsv
    ├── 25984.tsv
    ├── 25985.tsv
    ├── 25989.tsv
    ├── 25992.tsv
    ├── 25993.tsv
    ├── 25994.tsv
    ├── 25995.tsv
    ├── 25996.tsv
    ├── 25998.tsv
    ├── 26000.tsv
    ├── 26001.tsv
    ├── 26003.tsv
    ├── 26008.tsv
    ├── 26009.tsv
    ├── 26010.tsv
    ├── 26011.tsv
    ├── 26012.tsv
    ├── 26013.tsv
    ├── 26014.tsv
    ├── 26017.tsv
    ├── 26019.tsv
    ├── 26026.tsv
    ├── 26030.tsv
    └── 32767.tsv

7 directories, 184 files

И чтобы автоматически создать базу данных я использую свой docker образ PostgreSQL(Dockerfile в репозитории проекта) с предустановленными расширениями и скриптом инициализации БД, поддерживающем многопоточную загрузку данных:

docker run --name postgis14-thailand --memory=12g --memory-swap=12g --memory-swappiness 0 --shm-size=1g -v /home/acc/dev/map/database/thailand:/var/lib/postgresql/data -v /home/acc/dev/map/thailand/thailand-latest_loc_ways:/input -e POSTGRES_PASSWORD=osmworld -e LD_LIBRARY_PATH=/usr/lib/jvm/java-11-openjdk-amd64/lib/server/ -d -p 5432:5432 -p 5005:5005 5d411c3be57f -c checkpoint_timeout='15 min' -c checkpoint_completion_target=0.9 -c shared_buffers='4096 MB' -c wal_buffers=-1 -c bgwriter_delay=200ms -c bgwriter_lru_maxpages=100 -c bgwriter_lru_multiplier=2.0 -c bgwriter_flush_after=0 -c max_wal_size='32768 MB' -c min_wal_size='16384 MB'
docker logs postgis14-thailand | tail -n 40
initdb: warning: enabling "trust" authentication for local connections
You can change this by editing pg_hba.conf or using the option -A, or
--auth-local and --auth-host, the next time you run initdb.
2023-01-29 10:22:09.841 UTC [1] LOG:  starting PostgreSQL 14.1 (Debian 14.1-1.pgdg110+1) on x86_64-pc-linux-gnu, compiled by gcc (Debian 10.2.1-6) 10.2.1 20210110, 64-bit
2023-01-29 10:22:09.841 UTC [1] LOG:  listening on IPv4 address "0.0.0.0", port 5432
2023-01-29 10:22:09.841 UTC [1] LOG:  listening on IPv6 address "::", port 5432
2023-01-29 10:22:09.847 UTC [1] LOG:  listening on Unix socket "/var/run/postgresql/.s.PGSQL.5432"
2023-01-29 10:22:09.855 UTC [163] LOG:  database system was shut down at 2023-01-29 10:22:08 UTC
2023-01-29 10:22:09.863 UTC [1] LOG:  database system is ready to accept connections
Time: 16.011 ms
ANALYZE
Time: 49.590 ms
CREATE TABLE
Time: 1.509 ms
COPY 1
Time: 1.110 ms
ANALYZE
Time: 1.167 ms
CREATE TABLE
Time: 1.284 ms
COPY 566
Time: 2.092 ms
ANALYZE
Time: 1.880 ms
CREATE TABLE
Time: 1.210 ms
COPY 17482
Time: 19.023 ms
ANALYZE
Time: 27.044 ms
SELECT 80
Time: 8.333 ms
ANALYZE
Time: 1.533 ms
SELECT 75
Time: 9.650 ms
ANALYZE
Time: 1.213 ms

2023-01-29 10:22:07.219 UTC [49] LOG:  received fast shutdown request
waiting for server to shut down....2023-01-29 10:22:07.222 UTC [49] LOG:  aborting any active transactions
2023-01-29 10:22:07.223 UTC [49] LOG:  background worker "logical replication launcher" (PID 56) exited with exit code 1
2023-01-29 10:22:07.392 UTC [51] LOG:  shutting down
..2023-01-29 10:22:09.631 UTC [49] LOG:  database system is shut down
 done
server stopped

PostgreSQL init process complete; ready for start up.

docker exec -it postgis14-thailand psql -U postgres -d osmworld
psql (14.1 (Debian 14.1-1.pgdg110+1))
Type "help" for help.

osmworld=# \d
                       List of relations
 Schema |          Name          |       Type        |  Owner   
--------+------------------------+-------------------+----------
 public | geography_columns      | view              | postgres
 public | geometry_columns       | view              | postgres
 public | h3_3_bounds_complex    | table             | postgres
 public | multipolygon           | partitioned table | postgres
 public | multipolygon_000       | table             | postgres
 public | multipolygon_001       | table             | postgres
 public | multipolygon_002       | table             | postgres
 public | multipolygon_003       | table             | postgres
 public | multipolygon_004       | table             | postgres
 public | multipolygon_005       | table             | postgres
 public | multipolygon_006       | table             | postgres
 public | multipolygon_007       | table             | postgres
 public | multipolygon_008       | table             | postgres
 public | multipolygon_009       | table             | postgres
 public | multipolygon_010       | table             | postgres
 public | multipolygon_32767     | table             | postgres
 public | nodes                  | partitioned table | postgres
 public | nodes_000              | table             | postgres
 public | nodes_001              | table             | postgres
 public | nodes_002              | table             | postgres
 public | nodes_003              | table             | postgres
 public | nodes_004              | table             | postgres
 public | nodes_005              | table             | postgres
 public | nodes_006              | table             | postgres
 public | nodes_007              | table             | postgres
 public | nodes_008              | table             | postgres
 public | nodes_009              | table             | postgres
 public | nodes_010              | table             | postgres
 public | osm_file_block         | table             | postgres
 public | osm_file_block_content | table             | postgres
 public | osm_file_statistics    | table             | postgres
 public | osm_stat_nodes_3_3     | table             | postgres
 public | osm_stat_ways_3_3      | table             | postgres
 public | relation_members       | table             | postgres
 public | relations              | table             | postgres
 public | spatial_ref_sys        | table             | postgres
 public | ways                   | partitioned table | postgres
 public | ways_000               | table             | postgres
 public | ways_001               | table             | postgres
 public | ways_002               | table             | postgres
 public | ways_003               | table             | postgres
 public | ways_004               | table             | postgres
 public | ways_005               | table             | postgres
 public | ways_006               | table             | postgres
 public | ways_007               | table             | postgres
 public | ways_008               | table             | postgres
 public | ways_009               | table             | postgres
 public | ways_010               | table             | postgres
 public | ways_32767             | table             | postgres
(49 rows)

osmworld=# explain select h3_3, count(*) from ways group by 1;
                                        QUERY PLAN                                         
-------------------------------------------------------------------------------------------
 Gather  (cost=34456.99..97465.64 rows=76 width=10)
   Workers Planned: 4
   ->  Parallel Append  (cost=33456.99..96458.04 rows=19 width=10)
         ->  HashAggregate  (cost=96457.88..96457.95 rows=7 width=10)
               Group Key: ways_4.h3_3
               ->  Seq Scan on ways_004 ways_4  (cost=0.00..90269.92 rows=1237592 width=2)
         ->  HashAggregate  (cost=60359.46..60359.53 rows=7 width=10)
               Group Key: ways_9.h3_3
               ->  Seq Scan on ways_009 ways_9  (cost=0.00..56666.64 rows=738564 width=2)
         ->  HashAggregate  (cost=39018.48..39018.54 rows=6 width=10)
               Group Key: ways_7.h3_3
               ->  Seq Scan on ways_007 ways_7  (cost=0.00..36567.32 rows=490232 width=2)
         ->  HashAggregate  (cost=33456.99..33457.06 rows=7 width=10)
               Group Key: ways_2.h3_3
               ->  Seq Scan on ways_002 ways_2  (cost=0.00..31580.66 rows=375266 width=2)
         ->  HashAggregate  (cost=30029.44..30029.53 rows=9 width=10)
               Group Key: ways_5.h3_3
               ->  Seq Scan on ways_005 ways_5  (cost=0.00..28346.96 rows=336496 width=2)
         ->  HashAggregate  (cost=26775.62..26775.72 rows=10 width=10)
               Group Key: ways.h3_3
               ->  Seq Scan on ways_000 ways  (cost=0.00..25251.75 rows=304775 width=2)
         ->  HashAggregate  (cost=19023.96..19024.03 rows=7 width=10)
               Group Key: ways_1.h3_3
               ->  Seq Scan on ways_001 ways_1  (cost=0.00..17901.97 rows=224397 width=2)
         ->  HashAggregate  (cost=15682.35..15682.43 rows=8 width=10)
               Group Key: ways_6.h3_3
               ->  Seq Scan on ways_006 ways_6  (cost=0.00..14803.23 rows=175823 width=2)
         ->  HashAggregate  (cost=13881.85..13881.90 rows=5 width=10)
               Group Key: ways_3.h3_3
               ->  Seq Scan on ways_003 ways_3  (cost=0.00..13173.90 rows=141590 width=2)
         ->  HashAggregate  (cost=6970.89..6970.92 rows=3 width=10)
               Group Key: ways_8.h3_3
               ->  Seq Scan on ways_008 ways_8  (cost=0.00..6567.26 rows=80726 width=2)
         ->  HashAggregate  (cost=2385.09..2385.10 rows=1 width=10)
               Group Key: ways_11.h3_3
               ->  Seq Scan on ways_32767 ways_11  (cost=0.00..2327.06 rows=11606 width=2)
         ->  HashAggregate  (cost=37.55..37.59 rows=4 width=10)
               Group Key: ways_10.h3_3
               ->  Seq Scan on ways_010 ways_10  (cost=0.00..36.70 rows=170 width=2)
(39 rows)

osmworld=# select h3_3, count(*) from ways group by 1 order by 2 desc limit 20;
 h3_3  | count  
-------+--------
 25764 | 890643
 26010 | 329973
 25994 | 203645
 25730 | 188810
 25995 | 139373
 26011 | 136930
 25780 | 118165
 25765 | 105996
 26009 | 100196
 25762 |  91177
 26014 |  75555
 26003 |  71479
 26008 |  70353
 25883 |  60808
 25634 |  59705
 25777 |  58151
 16749 |  57887
 25782 |  57509
 25638 |  57296
 25880 |  56838
(20 rows)

Запускаем контейнер базы с OSM данными всего мира и подключаемся к базе данных любым удобным пользователю клиентом:

docker start postgis14-planet-220704
psql -h 127.0.0.1 -p 5432 -U postgres -d osmworld
И любуемся статистикой базы всего мира (587 GB):
         Table          | Rows  |     Total Size     |     Table Size     |  Index(es) Size  |     TOAST Size     
------------------------+-------+--------------------+--------------------+------------------+--------------------
 *** TOTAL ***          | ~1B   | 587 GB (100.00%)   | 501 GB (100.00%)   | 54 GB (100.00%)  | 32 GB (100.00%)
                        |       |                    |                    |                  | 
 ways_051               | ~21M  | 12 GB (2.02%)      | 11 GB (2.14%)      | 1070 MB (1.94%)  | 73 MB (0.22%)
 relation_members       | ~113M | 9523 MB (1.58%)    | 6136 MB (1.20%)    | 3386 MB (6.15%)  | 8192 bytes (0.00%)
 ways_043               | ~14M  | 8102 MB (1.35%)    | 7334 MB (1.43%)    | 737 MB (1.34%)   | 31 MB (0.09%)
 ways_052               | ~14M  | 8013 MB (1.33%)    | 7200 MB (1.40%)    | 729 MB (1.32%)   | 84 MB (0.26%)
 ways_002               | ~17M  | 7923 MB (1.32%)    | 7013 MB (1.37%)    | 870 MB (1.58%)   | 40 MB (0.12%)
 ways_001               | ~15M  | 7360 MB (1.22%)    | 6485 MB (1.26%)    | 788 MB (1.43%)   | 87 MB (0.26%)
 ways_081               | ~14M  | 6963 MB (1.16%)    | 6127 MB (1.19%)    | 716 MB (1.30%)   | 120 MB (0.37%)
 ways_004               | ~13M  | 6886 MB (1.15%)    | 6108 MB (1.19%)    | 691 MB (1.25%)   | 87 MB (0.27%)
 ways_029               | ~11M  | 6824 MB (1.14%)    | 6197 MB (1.21%)    | 567 MB (1.03%)   | 60 MB (0.18%)
 ways_047               | ~11M  | 6750 MB (1.12%)    | 6101 MB (1.19%)    | 577 MB (1.05%)   | 71 MB (0.22%)
 ways_093               | ~13M  | 6522 MB (1.09%)    | 5749 MB (1.12%)    | 644 MB (1.17%)   | 128 MB (0.39%)
 ways_095               | ~13M  | 6451 MB (1.07%)    | 5689 MB (1.11%)    | 671 MB (1.22%)   | 91 MB (0.28%)
 ways_090               | ~12M  | 6405 MB (1.07%)    | 5708 MB (1.11%)    | 620 MB (1.13%)   | 77 MB (0.23%)
 ways_003               | ~12M  | 6401 MB (1.07%)    | 5553 MB (1.08%)    | 641 MB (1.16%)   | 206 MB (0.63%)
 multipolygon_32767     | ~187k | 6366 MB (1.06%)    | 338 MB (0.07%)     | 27 MB (0.05%)    | 6002 MB (18.32%)
 ways_005               | ~12M  | 6317 MB (1.05%)    | 5590 MB (1.09%)    | 601 MB (1.09%)   | 126 MB (0.38%)
 ways_092               | ~12M  | 6227 MB (1.04%)    | 5511 MB (1.07%)    | 610 MB (1.11%)   | 107 MB (0.33%)
 ways_074               | ~11M  | 6208 MB (1.03%)    | 5507 MB (1.07%)    | 587 MB (1.07%)   | 114 MB (0.35%)
 ways_091               | ~11M  | 6158 MB (1.02%)    | 5425 MB (1.06%)    | 578 MB (1.05%)   | 156 MB (0.47%)
 ways_098               | ~12M  | 6154 MB (1.02%)    | 5421 MB (1.06%)    | 595 MB (1.08%)   | 138 MB (0.42%)
 ways_089               | ~11M  | 6128 MB (1.02%)    | 5418 MB (1.06%)    | 576 MB (1.05%)   | 134 MB (0.41%)
 ways_080               | ~11M  | 5973 MB (0.99%)    | 5206 MB (1.01%)    | 549 MB (1.00%)   | 217 MB (0.66%)
 ways_097               | ~11M  | 5940 MB (0.99%)    | 5298 MB (1.03%)    | 547 MB (0.99%)   | 95 MB (0.29%)
 ways_045               | ~11M  | 5877 MB (0.98%)    | 5261 MB (1.03%)    | 546 MB (0.99%)   | 70 MB (0.21%)
 ways_053               | ~9M   | 5810 MB (0.97%)    | 5289 MB (1.03%)    | 487 MB (0.88%)   | 34 MB (0.10%)
 ways_019               | ~10M  | 5794 MB (0.96%)    | 5124 MB (1.00%)    | 517 MB (0.94%)   | 153 MB (0.47%)
 ways_006               | ~10M  | 5774 MB (0.96%)    | 5033 MB (0.98%)    | 510 MB (0.93%)   | 231 MB (0.70%)
 ways_046               | ~10M  | 5702 MB (0.95%)    | 5094 MB (0.99%)    | 505 MB (0.92%)   | 103 MB (0.31%)
 ways_042               | ~10M  | 5683 MB (0.95%)    | 5075 MB (0.99%)    | 496 MB (0.90%)   | 112 MB (0.34%)
 ways_049               | ~9M   | 5663 MB (0.94%)    | 5118 MB (1.00%)    | 456 MB (0.83%)   | 89 MB (0.27%)
 ways_037               | ~9M   | 5634 MB (0.94%)    | 5040 MB (0.98%)    | 447 MB (0.81%)   | 147 MB (0.45%)
 ways_054               | ~9M   | 5627 MB (0.94%)    | 4856 MB (0.95%)    | 485 MB (0.88%)   | 286 MB (0.87%)
 ways_017               | ~10M  | 5605 MB (0.93%)    | 4950 MB (0.97%)    | 507 MB (0.92%)   | 148 MB (0.45%)
 ways_075               | ~9M   | 5587 MB (0.93%)    | 4940 MB (0.96%)    | 488 MB (0.89%)   | 159 MB (0.49%)
 ways_026               | ~9M   | 5585 MB (0.93%)    | 5064 MB (0.99%)    | 482 MB (0.88%)   | 39 MB (0.12%)
 ways_048               | ~9M   | 5584 MB (0.93%)    | 4985 MB (0.97%)    | 484 MB (0.88%)   | 114 MB (0.35%)
 ways_082               | ~10M  | 5548 MB (0.92%)    | 4890 MB (0.95%)    | 500 MB (0.91%)   | 158 MB (0.48%)
 ways_000               | ~9M   | 5460 MB (0.91%)    | 4750 MB (0.93%)    | 461 MB (0.84%)   | 248 MB (0.76%)
 ways_094               | ~10M  | 5457 MB (0.91%)    | 4737 MB (0.92%)    | 493 MB (0.89%)   | 227 MB (0.69%)
 ways_033               | ~8M   | 5454 MB (0.91%)    | 4808 MB (0.94%)    | 403 MB (0.73%)   | 243 MB (0.74%)
 ways_087               | ~9M   | 5445 MB (0.91%)    | 4808 MB (0.94%)    | 463 MB (0.84%)   | 173 MB (0.53%)
 ways_010               | ~9M   | 5357 MB (0.89%)    | 4774 MB (0.93%)    | 440 MB (0.80%)   | 143 MB (0.44%)
 ways_040               | ~9M   | 5348 MB (0.89%)    | 4722 MB (0.92%)    | 467 MB (0.85%)   | 159 MB (0.48%)
 ways_088               | ~9M   | 5262 MB (0.88%)    | 4662 MB (0.91%)    | 455 MB (0.83%)   | 145 MB (0.44%)
 ways_060               | ~8M   | 5243 MB (0.87%)    | 4673 MB (0.91%)    | 420 MB (0.76%)   | 150 MB (0.46%)
 ways_096               | ~10M  | 5207 MB (0.87%)    | 4613 MB (0.90%)    | 495 MB (0.90%)   | 99 MB (0.30%)
 ways_024               | ~8M   | 5205 MB (0.87%)    | 4722 MB (0.92%)    | 431 MB (0.78%)   | 52 MB (0.16%)
 ways_071               | ~9M   | 5198 MB (0.87%)    | 4625 MB (0.90%)    | 469 MB (0.85%)   | 104 MB (0.32%)
:

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

docker start postgis14-planet-220704

psql -h 127.0.0.1 -p 5432 -U postgres -d osmworld

Password for user postgres: 
? ? postgres_dba 6.0 installed. Use ":dba" to see menu
Timing is on.
psql (14.6 (Ubuntu 14.6-1.pgdg20.04+1), server 14.1 (Debian 14.1-1.pgdg110+1))
Type "help" for help.

osmworld=# \timing on
Timing is on.
osmworld=# create table school 
    as select h3_3, count(*) as "count" from ways 
        where closed and (tags->'building' = 'school' 
         or (tags->'building' is not null and tags->'amenity'='school')) 
       group by h3_3 order by 2 desc;
SELECT 6783
Time: 215154,128 ms (03:35,154)

Посмотрим план запроса, что же заняло эти 03:35 минуты у базы данных, как наш запрос выполнялся параллельно на всех партициях:

explain create table school 
    as select h3_3, count(*) as "count" from ways 
        where closed and (tags->'building' = 'school' 
         or (tags->'building' is not null and tags->'amenity'='school')) 
       group by h3_3 order by 2 desc

QUERY PLAN                                                                                              
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Gather Merge  (cost=13217952.21..13219300.04 rows=11052 width=10)
   Workers Planned: 6
   ->  Sort  (cost=13216952.12..13216956.72 rows=1842 width=10)
         Sort Key: (count(*)) DESC
         ->  Parallel Append  (cost=1107174.96..13216852.21 rows=1842 width=10)
               ->  GroupAggregate  (cost=1889369.47..1890603.83 rows=7 width=10)
                     Group Key: ways_51.h3_3
                     ->  Sort  (cost=1889369.47..1889780.90 rows=164572 width=2)
                           Sort Key: ways_51.h3_3
                           ->  Seq Scan on ways_051 ways_51  (cost=0.00..1873744.28 rows=164572 width=2)
                                 Filter: (closed AND (((tags -> 'building'::text) = 'school'::text) OR (((tags -> 'building'::text) IS NOT NULL) AND ((tags -> 'amenity'::text) = 'school'::text))))
               ->  HashAggregate  (cost=1278539.18..1278539.58 rows=40 width=10)
                     Group Key: ways_2.h3_3
                     ->  Seq Scan on ways_002 ways_2  (cost=0.00..1277795.15 rows=148806 width=2)
                           Filter: (closed AND (((tags -> 'building'::text) = 'school'::text) OR (((tags -> 'building'::text) IS NOT NULL) AND ((tags -> 'amenity'::text) = 'school'::text))))
               ->  GroupAggregate  (cost=1269524.73..1270332.33 rows=7 width=10)
                     Group Key: ways_43.h3_3
                     ->  Sort  (cost=1269524.73..1269793.90 rows=107671 width=2)
                           Sort Key: ways_43.h3_3
                           ->  Seq Scan on ways_043 ways_43  (cost=0.00..1260525.44 rows=107671 width=2)
                                 Filter: (closed AND (((tags -> 'building'::text) = 'school'::text) OR (((tags -> 'building'::text) IS NOT NULL) AND ((tags -> 'amenity'::text) = 'school'::text))))
               ->  GroupAggregate  (cost=1248285.94..1249040.42 rows=7 width=10)
                     Group Key: ways_52.h3_3
                     ->  Sort  (cost=1248285.94..1248537.41 rows=100588 width=2)
                           Sort Key: ways_52.h3_3
                           ->  Seq Scan on ways_052 ways_52  (cost=0.00..1239928.03 rows=100588 width=2)
                                 Filter: (closed AND (((tags -> 'building'::text) = 'school'::text) OR (((tags -> 'building'::text) IS NOT NULL) AND ((tags -> 'amenity'::text) = 'school'::text))))
 ...

 JIT:
   Functions: 806
   Options: Inlining true, Optimization true, Expressions true, Deforming true
(612 rows)

И визуализируем результат в QGIS:

select 
   h3_to_geo_boundary_geometry(h3_3::h3index), 
   count 
  from school 
      where count>100

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

Продолжение истории

  1. «Разделяй и властвуй» для OpenStreetMap мира в PostgreSQL

  2. Две беды: дороги и полнота данных. Считаем протяженность дорог родины по данным OpenStreetMap

  3. Колоночная база данных в PostgreSQL 15 и факты о территории России по данным OpenStreetMap

  4. Как в Северной Америке с дорогами и плотностью застройки? Работаем с 100Гб OSM данных в PostgreSQL

Добро пожаловать в мир аналитики гео данных!

Уникальность проекта OpenStreetMap в возможности каждого на планете использовать данные проекта, но с учетом объема данных, это было не так просто. Теперь для запуска базы не нужен мощный специализированный сервер и, надеюсь, что мой подход к секционированию данных OSM и возможность использовать для аналитики геопространственных данных PostgreSQL позволит большему количеству людей делать запросы к этому уникальному проекту и разрабатывать новые сервисы, которые позволят учитывать расстояния, объекты реального мира и улучшать мир вокруг нас.

Теги:
Хабы:
Всего голосов 79: ↑79 и ↓0+79
Комментарии22

Публикации

Истории

Работа

Java разработчик
363 вакансии

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

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань