在 Bigtable 中使用地理空间数据

本页面介绍了如何使用 GoogleSQL 地理函数在 Bigtable 中存储和查询地理空间数据。地理空间数据包括地球表面的几何表示形式,以点、线和多边形的形式呈现。您可以将这些实体分组到集合中,以跟踪位置、路线或其他感兴趣的区域。

基本操作

借助专用的 GoogleSQL 地理函数,Bigtable 可以针对地理空间计算和查询时间优化数据库性能。在基本使用示例中,我们使用了一个名为 tourist_points_of_interest 的表,其中包含一个名为 poi_data 的列族。

poi_data
行键 name location
p1 埃菲尔铁塔 POINT(2.2945 48.8584)

写入地理位置数据

如需将地理空间数据写入 Bigtable 列,您需要以 GeoJSONWell-Known Text (WKT) 格式提供地理位置实体的字符串表示形式。Bigtable 会将底层数据存储为原始字节,因此您可以提供 任何实体组合(例如 Points, LineStrings, Polygons, MultiPoints, MultiLineStrings, 或 MultiPolygons)。 您提供的地理空间实体的类型在查询时非常重要,因为您需要 使用适当的地理函数来解读数据。

例如,如需向 tourist_points_of_interest 表中添加一些景点, 您可以使用 cbt CLI 工具,如下所示:

  • 使用 WKT:
    cbt set tourist_points_of_interest p2 poi_data:name='Louvre Museum'
    cbt set tourist_points_of_interest p2 poi_data:location='POINT(2.3376 48.8611)'
    
    cbt set tourist_points_of_interest p3 poi_data:name='Place Charles de Gaulle'
    cbt set tourist_points_of_interest p3 poi_data:location='POLYGON(2.2941 48.8733, 2.2941 48.8742, 2.2957 48.8742, 2.2958 48.8732, 2.2941 48.8733)'
  • 使用 GeoJSON:
    cbt set tourist_points_of_interest p2 poi_data:name='Louvre Museum'
    cbt set tourist_points_of_interest p2 poi_data:location='{"type": "Point", "coordinates": [2.3376, 48.8611]}'
    
    cbt set tourist_points_of_interest p3 poi_data:name='Place Charles de Gaulle'
    cbt set tourist_points_of_interest p3 poi_data:location='{"type": "Polygon", "coordinates": [[ [2.2941, 48.8733], [2.2941, 48.8742], [2.2957, 48.8742], [2.2958, 48.8732], [2.2941, 48.8733] ]] }'

查询地理位置数据

您可以在 Bigtable Studio 中使用 GoogleSQL 查询 tourist_points_of_interest 表,并使用地理函数过滤结果。例如,如需查找距离特罗卡德罗广场 1500 米内的所有旅游景点, 您可以使用 ST_DWITHIN 函数:

SELECT _key, poi_data['name'], poi_data['location']
FROM points_of_interest
WHERE ST_DWITHIN(CAST(poi_data['location'] AS STRING), ST_GEOGPOINT(2.2874, 48.86322), 1500);

结果将类似于以下内容:

屏幕截图:显示了 Bigtable Studio 中的 tourist_points_of_interest 表,其中查询结果已使用 ST_DWITHIN 地理位置函数进行过滤。

location 列显示了写入操作中提供的原始文本数据表示形式,但 Bigtable 会将其存储为字节。在此示例中,即使 Place Charles de Gaulle 被定义为超出 1500 米限制的多边形,它也会包含在查询结果中。 这是因为,如果地理空间实体中包含的任何点位于预期距离内,ST_DWITHIN 函数都会返回 true。

高级用法

以下部分介绍了如何在地理围栏分析等复杂场景中使用地理函数。查询优化 部分介绍了如何使用连续的物化视图来提高性能。

为了说明 Bigtable 中地理函数的高级用法, 我们假设您是一位研究员,正在监测 居住在 雪山岛南侧的帝企鹅的行为。

您的设备为每只企鹅提供了数百个位置 ping,因此您创建了 penguin_movements 表来存储这些 ping:

penguin_details
行键 penguin_id 时间戳 location
ping#123 pen_01 2025-12-06 08:15:22+00 POINT(-57.51 -64.42)
ping#124 pen_01 2025-12-06 10:22:05+00 POINT(-57.55 -64.43)
ping#125 pen_01 2025-12-07 12:35:45+00 POINT(-57.58 -64.41)
ping#126 pen_02 2025-12-12 06:05:11+00 POINT(-57.49 -64.39)

示例:查找越过地理围栏的人

在研究过程中,您观察到企鹅在特定觅食地周围的行为。使用虚拟边界定义的区域通常称为地理围栏。 觅食地可以被视为地理围栏区域。 您想了解是否有企鹅在 2025 年 12 月 3 日访问过觅食地。如需回答此问题,您可以使用 ST_CONTAINS 函数,如下所示:

SELECT
  penguin_details['penguin_id'],
  penguin_details['location'],
  penguin_details['timestamp']
FROM
  penguin_movements
WHERE
  penguin_details['timestamp'] >= '2025-12-03 00:00:00 UTC'
  AND penguin_details['timestamp'] < '2025-12-04 00:00:00 UTC'
  -- The feeding ground boundary is defined with a WKT POLYGON entity
  -- Polygon's last point must be equal to its first point to close the loop
  AND ST_CONTAINS(
        ST_GEOGFROMTEXT('POLYGON((-57.21 -64.51, -57.23 -64.55, -57.08 -64.56, -57.06 -64.51, -57.21 -64.51))'),
        ST_GEOGFROMTEXT(CAST(penguin_details['location'] AS STRING))
      )

您会获得类似于以下内容的结果:

+------------+----------------------+------------------------+
| penguin_id |       location       |       timestamp        |
+------------+----------------------+------------------------+
| pen_01     | POINT(-57.15 -64.53) | 2025-12-03 08:15:22+00 |
| pen_02     | POINT(-57.18 -64.54) | 2025-12-03 14:30:05+00 |
| pen_10     | POINT(-57.10 -64.52) | 2025-12-03 11:45:10+00 |
+------------+----------------------+------------------------+

示例:将点转换为路线

为了更好地直观呈现企鹅的行进方式,您决定使用各个位置 ping 整理为 LineStrings,如下所示ST_MAKELINE

SELECT
  penguin_id,
  DATE(timestamp, 'UTC') AS route_date,
  ST_MAKELINE(ARRAY_AGG(location_cast_to_geog)) AS route
FROM (
  -- Sub-expression to sort all location pings by timestamp
  -- This way you make sure individual points can form a realistic route
  SELECT
    penguin_details['penguin_id'] AS penguin_id,
    -- Extract and cast timestamp once
    CAST(CAST(penguin_details['timestamp'] AS STRING) AS TIMESTAMP) AS timestamp,
    -- Extract and cast location once
    ST_GEOGFROMTEXT(CAST(penguin_details['location'] AS STRING)) AS location_cast_to_geog
  FROM
    penguin_movements
  ORDER BY
    -- Pre-sorts location pings for the ARRAY_AGG function
    penguin_id, timestamp ASC
)
GROUP BY
  penguin_id,
  route_date

借助此查询,您可以获得更高级的研究分析洞见。展开即可下部分,查看示例查询和结果。

每只企鹅的每日行进距离是多少?

如需计算每只企鹅每天行进的距离,您可以使用 ST_LENGTH 函数。

SELECT
  penguin_id,
  route_date,
  ST_LENGTH(route) AS daily_distance_meters
FROM (
  -- Subquery to aggregate points into daily routes
  SELECT
    penguin_id,
    DATE(timestamp, 'UTC') AS route_date,
    ST_MAKELINE(ARRAY_AGG(location_cast_to_geog)) AS route
  FROM (
    -- Sub-expression to sort all location pings by timestamp
    -- This way you make sure individual points can form a realistic route
    SELECT
      penguin_details['penguin_id'] AS penguin_id,
      -- Extract and cast timestamp once
      CAST(CAST(penguin_details['timestamp'] AS STRING) AS TIMESTAMP) AS timestamp,
      -- Extract and cast location once
      ST_GEOGFROMTEXT(CAST(penguin_details['location'] AS STRING)) AS location_cast_to_geog
    FROM
      penguin_movements
    ORDER BY
      -- Pre-sorts location pings for the ARRAY_AGG function
      penguin_id, timestamp ASC
  )
  GROUP BY
    penguin_id,
    route_date
) AS DailyRoutes;

此查询会返回类似于以下内容的结果:

+------------+------------+-----------------------+
| penguin_id | route_date | daily_distance_meters |
+------------+------------+-----------------------+
| pen_01     | 2025-12-02 | 15420.7               |
| pen_03     | 2025-12-03 | 22105.1               |
| pen_32     | 2025-12-03 | 9850.3                |
+------------+------------+-----------------------+

每日路线中有多少位于地理围栏区域内?

如需回答此问题,您可以使用 ST_INTERSECTION 函数过滤结果,以检查企鹅路线的哪些部分与地理围栏区域相交。 然后,ST_LENGTH函数可以帮助计算觅食地内的移动距离与当天行进的总距离的比率。

SELECT
  penguin_id,
  route_date,
  ST_LENGTH(ST_INTERSECTION(route, ST_GEOGFROMTEXT('POLYGON((-57.21 -64.51, -57.23 -64.55, -57.08 -64.56, -57.06 -64.51, -57.21 -64.51))'))) / ST_LENGTH(route) AS proportion_in_ground
FROM (
  -- Subquery to aggregate points into daily routes
  SELECT
    penguin_id,
    DATE(timestamp, 'UTC') AS route_date,
    ST_MAKELINE(ARRAY_AGG(location_cast_to_geog)) AS route
  FROM (
    -- Sub-expression to sort all location pings by timestamp
    -- This way you make sure individual points can form a realistic route
    SELECT
      penguin_details['penguin_id'] AS penguin_id,
      -- Extract and cast timestamp once
      CAST(CAST(penguin_details['timestamp'] AS STRING) AS TIMESTAMP) AS timestamp,
      -- Extract and cast location once
      ST_GEOGFROMTEXT(CAST(penguin_details['location'] AS STRING)) AS location_cast_to_geog
    FROM
      penguin_movements
    ORDER BY
      -- Pre-sorts location pings for the ARRAY_AGG function
      penguin_id, timestamp ASC
  )
  GROUP BY
    penguin_id,
    route_date
) AS DailyRoutes
WHERE
  ST_INTERSECTS(route, ST_GEOGFROMTEXT('POLYGON((-57.21 -64.51, -57.23 -64.55, -57.08 -64.56, -57.06 -64.51, -57.21 -64.51))'))

此查询会返回类似于以下内容的结果:

+------------+------------+----------------------+
| penguin_id | route_date | proportion_in_ground |
+------------+------------+----------------------+
| pen_01     | 2025-12-03 | 0.652                |
| pen_03     | 2025-12-03 | 0.918                |
| pen_32     | 2025-12-04 | 0.231                |
+------------+------------+----------------------+

使用连续的物化视图优化查询

对大型数据集执行地理空间查询可能会很慢,因为它们可能需要扫描许多行。Bigtable 不支持专用的地理空间 索引,但您可以使用 连续的物化视图来提高查询性能。

如需为此类视图创建唯一的行键,您可以将 GEOGRAPHY 类型转换为一个 S2 单元格。 S2 是一个库,可让您对球体几何图形执行复杂的三角函数运算。 根据地理坐标计算距离的计算成本可能很高。S2 单元格可以将地球表面上的特定区域表示为 64 位整数,因此非常适合使用连续的物化视图进行地理空间索引。

如需定义 S2 单元格,您可以调用 S2_CELLIDFROMPOINT(location, level) 函数并提供粒度 level 实参。此实参是介于 030 之间的 数字,用于定义每个单元格的大小: 数字越大,单元格越小。

在企鹅研究示例中,您可以 创建一个连续的物化视图 ,该视图按每只企鹅的位置和时间戳编制索引:

-- Query used to create the continuous materialized view
SELECT
  -- Create S2 cells for each penguin's location.
  -- Note that the `level` value is the same for each location so that
  -- every cell is the same size. This ensures consistency for your data.
  S2_CELLIDFROMPOINT(ST_GEOGFROMTEXT(CAST(penguin_details['location'] AS STRING)), level => 16) AS s2_cell_id,
  penguin_details['timestamp'] AS observation_time,
  penguin_details['penguin_id'] AS penguin_id,
  penguin_details['location'] AS location
FROM
  penguin_movements

使用两阶段方法查询此视图以回答 “谁越过地理围栏?”这个问题 会更加高效。 您首先使用连续的具体化视图中的 cell_id 索引快速查找大致范围内的候选行,然后在缩减的数据集上应用准确的 ST_CONTAINS 函数:

SELECT
  idx.penguin_id,
  idx.location,
  idx.observation_time
FROM
  penguin_s2_time_index AS idx
WHERE
  -- Part one: use an approximate spatial filter and timestamp filter
  -- for fast scans on continuous materialized view keys.
  -- Use the S2_COVERINGCELLIDS function to create an array of S2 cells
  -- that cover the feeding ground polygon. The `level` argument must be the
  -- same value as the one you used to create the continuous materialized view.
  idx.s2_cell_id IN UNNEST(S2_COVERINGCELLIDS(
    ST_GEOGFROMTEXT('POLYGON((-57.21 -64.51, -57.23 -64.55, -57.08 -64.56, -57.06 -64.51, -57.21 -64.51))'),
    min_level => 16,
    max_level => 16,
    max_cells => 500
  ))
  AND idx.observation_time >= '2025-12-03 00:00:00 UTC'
  AND idx.observation_time < '2025-12-04 00:00:00 UTC'
  -- Part two: use ST_CONTAINS() on the returned set to ensure precision.
  -- S2 cells are squares, so they don't equal arbitrary geofence polygons.
  -- You still need to check if a specific point is contained within the area,
  -- but the filter applies to a smaller data set and is much faster.
  AND ST_CONTAINS(ST_GEOGFROMTEXT('POLYGON((-57.21 -64.51, -57.23 -64.55, -57.08 -64.56, -57.06 -64.51, -57.21 -64.51))'), idx.location);

限制

在使用地理位置数据时,Bigtable 不支持以下功能:

  • 三维几何图形。这包括 WKT 格式中的“Z”后缀,以及 GeoJSON 格式中的海拔坐标。
  • 线性参考系统。这包括 WKT 格式中的“M”后缀。
  • 除几何图形基元或多部分几何图形以外的 WKT 几何图形对象。 具体而言,Bigtable 仅支持 Point、MultiPoint、LineString、MultiLineString、Polygon、MultiPolygon 和 GeometryCollection。
  • 不支持 ST_CLUSTERDBSCAN 函数。

如需了解特定于 GeoJson 和 WKT 输入格式的限制条件,请参阅 ST_GEOGFROMGEOJSONST_GEOGFROMTEXT

接下来怎么做?