Ganos实时热力聚合查询能力解析与最佳实践

本文介绍基于PolarDB PostgreSQL版Ganos实时热力聚合查询并动态输出热力瓦片能力,该功能可用于将查询处理结果即时返回给客户端的数据交换结构。

关于热力瓦片

什么是热力瓦片

热力瓦片(HeatMap Tile,简称HMT)底层基于Ganos首创的大规模矢量/轨迹数据实时热力聚合查询技术,用于将查询处理结果即时返回客户端的数据交换结构。该技术改变了热力统计分析中“聚合需要预打码、展示需要预切片”的传统方式,可针对百万级、千万级、亿级规模数据秒级聚合并渲染。HMT支持多种常用聚合函数与代数表达式,支持选择统计与业务相关的指标,并随地图的放大缩小进行不同层级的动态计算和实时绘制,极大程度提升业务效率,为客户产品带来的更多可能。在2022年度的云栖大会上,Ganos发布了这一能力,并现场展示了基于HMT构建的大规模运输轨迹实时查询聚合的案例,将以往需要线下预处理才能发布的数据产品实现完全在线化。该功能获得了行业内的广泛反响与认可。

使用场景

热力瓦片的特点在于能够实现空间数据的实时聚合与渲染,主要应用于处理海量矢量数据并需要实时统计分析的业务场景,比如:

  • 交通运输类:根据运输工具(如车辆、船舶等)的历史轨迹,聚合全域范围内的实时热力数据,并能够根据时间(如冬季、夏季)、起点/终点及类型(如货车、客车)等附加条件进行过滤,从而实时生成相应的热力图。

  • 城市管理类:根据房屋建筑底面数据,聚合全域范围内城市和农村的建筑密度、建筑平均高度、建筑总面积等单项指标,并结合地块信息,聚合容积率等复合指标。

  • 共享出行类:根据共享出行设备的轨迹点,聚合全域范围内设备的停靠区域热力图,并可基于设备事件(如开锁/关锁、上车/下车、事故、损坏)、流向等条件分析共享出行设备的调度与运维策略。

技术优势

相较于基于H3或S2网格预打码聚合的方式,HMT热力瓦片拥有如下优势:

  1. 效率极高,无需预打码,不增加存储成本。HMT的聚合技术与H3/S2等网格聚合方式在技术特性及应用场景上存在显著差异。网格聚合方式通常针对需要以网格编码作为检索条件的应用场景,这要求在预先设定某一精度层级后,对矢量数据进行编码,并基于该编码进行相关统计。然而,HMT技术不需要事先对数据进行编码,能够根据当前视口范围进行动态聚合。随着视口的放大或缩小,聚合过程将实时进行,且在处理各种几何对象时效率一致,均可实现亿级规模的秒级聚合渲染。

  2. 方便宜用,聚合结果直接可视化。HMT提供了聚合结果快速瓦片化的能力,能够将聚合结果直接与前端渲染引擎进行可视化对接,确保所见即所得。此外,HMT还提供了一系列统计函数,以帮助用户快速自动化生成最佳的渲染色表,从而确保前端的最佳表现。

经过多个真实场景中的测试,HMT的聚合技术展现出极高的效率,基本能够在秒级时间内完成亿级规模的全图聚合:

应用场景

数据量

瓦片范围

聚合效率

轨迹聚合

  • 轨迹线:45万

  • 轨迹点:3100万

  • 全球级别

  • 512*512瓦片

372 ms

建筑底面聚合

建筑底面:3.08亿

  • 全球级别

  • 512*512瓦片

17 s

说明

上表数据均为在全球展示尺度下的全量数据聚合效率,随着地图的持续放大,聚合效率将不断提升。

功能介绍

热力瓦片包含一系列的SQL函数,用于解决热力瓦片的生成与统计问题,具体包括:

  • ST_AsHMT:将一组几何对象或轨迹对象按照指定范围和指定分辨率转为热力矩阵瓦片。

  • ST_HMTAsArray:将热力图瓦片转换为基于数组矩阵的表示方法,以便于进行查看。

  • ST_HMTStats:计算热力图瓦片的统计值信息,以便在渲染过程中使用。

  • ST_HMTAsRaster:将热力瓦片转换为Raster对象,以便于进行查看和计算操作。

使用技巧

采用并行提升性能

当数据量较大时,可以采用并行处理以提升性能。在实际应用中,可根据视口范围进行相应设置,例如在较高层级下使用16个并行进程,而在较低层级下则不使用并行处理等。在CPU资源充足的情况下,为确保每个查询均可利用并行处理,需要将max_worker_processesmax_parallel_workers的值设置为并行度与并发数的乘积,相关介绍可参考社区文档。以下示例将为您展示如何设置并行度为16:

SET max_worker_processes = 300;
SET max_parallel_workers = 260;
SET max_parallel_workers_per_gather = 16;
ALTER TABLE table_name SET (parallel_workers=16);
SET force_parallel_mode = on;

瓦片大小

通常情况下,采用512*512的瓦片重采样至256*256的瓦片以避免锯齿现象。然而,在特定情况下,如数据量极大时,每个瓦片的计算过程会非常耗时,此时可以考虑使用大瓦片(1024*1024)以减少瓦片获取的数量,从而提升性能。

使用&&操作符进行空间过滤

从计算性能上看,ST_AsHMT的计算速度明显快于ST_Intersects。在进行索引过滤时,还可以使用&&操作符进行过滤。

SELECT ST_AsHMT(column_name, --geometry类型列
    ST_MakeEnvelope(0, 0, 10, 10, 4326), -- 定义空间范围
    512,
    512,
    value       -- 用于生成输出的参考值列
)
FROM table_name
WHERE column_name && ST_MakeEnvelope(0, 0, 10, 10, 4326);

查询范围的空间参考转换

当查询范围与几何对象的空间范围不一致时,应对查询范围进行空间参考转换后再进行查询。否则,自动转换可能会导致性能下降。在获取瓦片后,将图片转换为指定的空间参考后进行展示。

SELECT ST_AsHMT(column_name, -- 空间参考系统为WGS 84坐标系
    ST_Transform(ST_TileEnvelope(6, 48, 32), 4326), -- 定义瓦片的范围
    512,
    512,
    value       -- 指定用于渲染瓦片图像的值
)
FROM table_name
WHERE column_name && ST_Transform(ST_TileEnvelope(6, 48, 32), 4326));

对空间表进行VACCUM FULLCLUSTER操作

  • VACCUM FULL操作可以回收空闲空间,降低磁盘文件大小,从而在查询时降低IO操作的数量。

    VACUUM full table_name;
    CLUSTER table_name using index_name;
  • CLUSTER操作可以确保数据组织与索引保持一致,相邻的空间数据将被存储在相邻的数据页面中,从而在访问时降低数据库的磁盘访问负担。详细用法请参考社区文档

    CLUSTER table_name USING index_name;

最佳实践

  1. 数据库端配置。

    1. 将几何或轨迹数据导入数据库中,建议采用FDW机制进行操作,详细操作可以参考矢量格数据快速入库。请确保所有对象均具备相同的空间参考系统,您可以通过ST_Srid函数进行确认。

    2. 在几何列或轨迹列创建空间索引。

      CREATE INDEX index_name ON table_name USING GIST(column_name);
    3. 根据空间范围进行热力瓦片的查询。

      说明

      以下SQL语句中的ST_MakeEnvelope可以使用ST_TileEnvelope函数获取瓦片范围。

      • 针对网格内对象数量的热力聚合进行分析。

        SELECT ST_AsHMT(column_name, --geometry type
            ST_MakeEnvelope(0, 0, 10, 10, 4326), -- Extent
            512,        -- Width
            512        -- height
        )
        FROM table_name
        WHERE column_name && ST_MakeEnvelope(0, 0, 10, 10, 4326);
      • 对特定网格内的数值进行聚合时,可采用value字段中的数值进行求和操作。

        SELECT ST_AsHMT(column_name, --geometry type
            ST_MakeEnvelope(0, 0, 10, 10, 4326), -- Extent
            512,        -- Width
            512,        -- height
            value       -- value column
        )
        FROM table_name
        WHERE column_name && ST_MakeEnvelope(0, 0, 10, 10, 4326);
      • 对特定网格内的数值进行聚合并采用value字段中的数值进行求和操作时,还可以增加其他过滤条件。

        SELECT ST_AsHMT(column_name, --geometry type
            ST_MakeEnvelope(0, 0, 10, 10, 4326), -- Extent
            512,        -- Width
            512,        -- height
            value       -- value column
        )
        FROM table_name
        WHERE column_name && ST_MakeEnvelope(0, 0, 10, 10, 4326);
        AND name like 'xxxx%' AND value > 100;
  2. 本案例使用Node.js编写一个简易应用,以演示热力瓦片的实际应用场景。

    文件结构如下:

    └── hmt_server
        ├── app.js
        ├── hmt.proto
        ├── index.html
        └── package.json

    其中,hmt.protoST_AsHMT中介绍的proto文件,其他文件内容将在下文给出。

    后端代码package.json和app.js文件如下:

    • package.json:

      {
        "name": "hmt_server",
        "version": "1.0.0",
        "main": "app.js",
        "license": "ISC",
        "dependencies": {
          "chroma-js": "^2.4.2",
          "express": "^4.18.2",
          "lru-cache": "^10.1.0",
          "pg": "^8.11.3",
          "protobufjs": "^7.2.5",
          "sharp": "^0.32.6"
        }
      }
    • app.js:

      const express = require('express');
      const { Pool } = require('pg');
      const chroma = require('chroma-js');
      const sharp = require("sharp");
      const protobuf = require('protobufjs');
      const { LRUCache } = require('lru-cache');
      
      // 设定数据库连接
      const CONNECTION = {
        user: 'YOUR_USER',
        password: 'YOUR_PWD',
        host: 'YOUR_HOST',
        database: 'YOUR_DB',
        port: YOUR_PORT
      };
      
      // 目标表名
      const TABLE_NAME = 'YOUR_TABLE';
      
      // 目标几何字段名
      const GEOMETRY_COLUMN = 'YOUR_GEOM_COLUMN';
      
      // 设定无数据值
      const NO_DATA_VALUE = 0;
      
      // 目标几何字段空间参考
      const SRID = 4326
      
      // 设定色带
      const COLOR_MAP = [
        ['#536edb', 1],
        ['#5d96a5', 3],
        ['#68be70', 5],
        ['#91d54d', 7],
        ['#cddf37', 9],
        ['#fede28', 11],
        ['#fda938', 13],
        ['#fb7447', 15],
        ['#f75a40', 17],
        ['#f24734', 19],
        ['#e9352a', 21],
        ['#da2723', 23],
        ['#cb181d', 25]
      ];
      
      // 创建数据库连接池,默认为10个连接
      const pool = new Pool(CONNECTION);
      
      // 配置颜色转换
      const [colors, domains] = COLOR_MAP.reduce(([c, d], [colors, domains]) =>
        [[...c, colors], [...d, domains]], [[], []]);
      const colorMap = chroma.scale(colors).domain(domains).mode('rgb')
      
      // 加载protobuf
      const hmtDecoder = protobuf.loadSync('./hmt.proto').lookupType('HMT');
      
      // 创建一个1x1的透明png,作为空瓦片返回
      const emptyPng = Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACXBIWXMAAA7EAAAOxAGVKw4bAAAADUlEQVQImWP4//8/AwAI/AL+hc2rNAAAAABJRU5ErkJggg==', 'base64');
      
      // 对于小比例尺瓦片(z<5),因更新相对不明显,设定一个24小时过期的缓存
      const globalCache = new LRUCache({ max: 1000, ttl: 1000 * 3600 * 24 });
      
      // 对于更大比例尺瓦片(z>=5),设定一个12小时过期的缓存,也可根据实际情况自行修改
      const localCache = new LRUCache({ max: 2000, ttl: 1000 * 3600 * 12 });
      
      // 注册Express路由
      express()
        // 响应HTML页面
        .get("/", (_, res) => res.sendFile('index.html', { root: __dirname }))
        // 响应热力瓦片服务
        .get('/hmt/:z/:x/:y', async ({ params: { z, x, y } }, res) => {
          const cache = z < 5 ? globalCache : localCache;
          const key = `${z},${x},${y}`
          if (!cache.has(key)) {
            // 设定并行度,并调用ST_AsHMT函数,请求该区域256x256的热力瓦片
            const parallel = z <= 5 ? 10 : 5;
            const sql = `
        set max_parallel_workers = ${parallel};
        set max_parallel_workers_per_gather = ${parallel};
        WITH _PARAMS(_BORDER) as (VALUES(ST_Transform(ST_TileEnvelope(${key}),${SRID})))
        SELECT ST_AsHMT(${GEOMETRY_COLUMN},_BORDER,256,256) tile
        FROM ${TABLE_NAME},_PARAMS
        WHERE _BORDER && ${GEOMETRY_COLUMN};`
            // 跳过set语句,获取ST_AsHMT函数的结果
            const { rows: [{ tile }] } = (await pool.query(sql))[2];
      
            // 若该区域无数据则直接返回空瓦片
            if (!tile) cache.set(key, emptyPng);
            else {
              // 解析protobuf结果
              const { type, doubleValues, intValues } = hmtDecoder.decode(tile);
              const { values } = type == 1 ? doubleValues : intValues;
      
              // 将数值转换为对应的颜色,并剔除无数据值
              const pixels = values.reduce((_pixels, value) => {
                _pixels.push(...colorMap(value).rgb());
                _pixels.push(value <= NO_DATA_VALUE ? 0 : 255);
                return _pixels;
              }, [])
      
              // 渲染为png瓦片
              const rawConfig = { raw: { width: 256, height: 256, channels: 4 } };
              const renderedPng = await sharp(Uint8Array.from(pixels), rawConfig)
                .png().toBuffer();
              cache.set(key, renderedPng);
            }
          }
          const tile = cache.get(key)
          res.set("Content-Type", "image/png").send(tile);
        })
        // 监听5500端口
        .listen(5500, () => console.log('HMT server started.'));

      其中:

      • 色带为连续色带,支持十六进制字符串颜色、CSS3名称颜色等多种表达方式,详细介绍可参见chroma.js文档

      • 若希望瓦片渲染效果更加平滑,可以请求512x512的原始数据,然后降采样至256x256分辨率,但这将增加响应时间。

      • 并行度的设定应根据数据量的大小、数据库集群的配置以及对响应速度的需求进行调整。

        说明

        本案例在Z<=5时适当降低并行度。

    • 前端代码index.html采用Mapbox作为前端地图SDK,其token可在此处申请查看。

      由于热力瓦片最终以PNG格式渲染,因此与绝大多数地图SDK兼容。

      <!DOCTYPE html>
      <html>
      <head>
        <meta charset="utf-8">
        <title>HMT Viewer</title>
        <meta name="viewport" content="initial-scale=1,maximum-scale=1,user-scalable=no">
        <link href="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.css" rel="stylesheet">
        <script src="https://api.mapbox.com/mapbox-gl-js/v2.14.1/mapbox-gl.js"></script>
      </head>
      <body>
        <div id="map" style="position: absolute;left:0; top: 0; bottom: 0; width: 100%;"></div>
        <script>
          let CENTER = [YOUR_LONGITUDE, YOUR_LATITUDE]
          mapboxgl.accessToken = YOUR_MAPBOX_TOKEN;
          const map = new mapboxgl.Map({
            container: 'map',
            style: "mapbox://styles/mapbox/navigation-night-v1",
            center: CENTER,
            zoom: 5
          })
          map.on("load", () => {
            map.addSource('hmt_source', {
              type: 'raster',
              minzoom: 3,
              tiles: [`${window.location.href}hmt/{z}/{x}/{y}`],
              tileSize: 256,
            });
            map.addLayer({
              id: 'hmt',
              type: 'raster',
              source: 'hmt_source',
            });
          });
        </script>
      </body>
      </html>
  3. 安装与发布。

    ## 定位到hmt_server目录
    cd ./hmt_server
    ## 安装依赖库
    npm i
    ## 运行热力瓦片服务
    node .
    ## 此时可以打开浏览器,登录地址 http://localhost:5500/ 查看效果
  4. 效果预览

    • 船舶轨迹线实时聚合

      3100万轨迹点,45万轨迹线实时聚合。

      船舶轨迹线-1

      传播轨迹线-2

    • 建筑底面实时聚合

      3.08亿建筑底面实时聚合。

      建筑底面

总结

目前,Ganos已成功支撑数十个行业领域的数千个应用场景。稳定性、成本效益、性能与易用性始终是Ganos长期追求的目标。HMT热力瓦片是Ganos在大规模空间数据高效聚合与可视化领域的内核级核心竞争力,为大规模数据分析挖掘提供了真正高效、易用的方案,欢迎您体验。

试用体验

您可以访问PolarDB免费试用页面,选择试用“云原生数据库PolarDB PostgreSQL版”,体验Ganos的HMT实时热力统计查询能力。