【问题标题】:How can I set up an index in Postgres using PostGIS that calculates proximity efficiently?如何使用 PostGIS 在 Postgres 中设置索引以有效计算邻近度?
【发布时间】:2021-01-10 01:04:29
【问题描述】:

我有一个用户表,每个用户都有纬度和经度。我有一个类似“距离容差”的东西,每个用户都可以定义。考虑这一点的一种简单方法是将其构建为约会应用程序:

Alice will only date users within 30km

使用 PostGIS,使用用户位置的 GIST 索引来设置查找附近用户的查询并不难。这里概述了这种方法:https://ngauthier.com/2013/08/postgis-and-rails-a-simple-approach.html,但这里是基础知识:

=> CREATE EXTENSION postgis;

=> CREATE TABLE users (
    id uuid DEFAULT public.gen_random_uuid() NOT NULL,
    latitude numeric(9,6),
    longitude numeric(9,6),
    distance int DEFAULT 20000
);

=> INSERT INTO users (latitude, longitude, distance) (
  SELECT
    40.7128 + x.lat AS latitude,
    -74.006 + x.lon AS longitude,
    10000 * (1 + floor(random() * 3)) AS distance
  FROM (
    SELECT random() * 2 as lat, random() * 2 as lon
    FROM generate_series(1,1000000)
  ) AS x);

=> CREATE INDEX index_on_users_location ON users
  USING gist (st_geographyfromtext((((('SRID=4326;POINT('::text
  || longitude) || ' '::text) || latitude) || ')'::text)));

此时,根据 Alice 的偏好查询附近的用户就很容易了:

=> SELECT COUNT(*) FROM users WHERE
   ST_DWithin(ST_GeographyFromText('SRID=4326;POINT(' ||
      users.longitude || ' ' || users.latitude || ')'),
    ST_GeographyFromText('SRID=4326;POINT(' ||
      '-74.006 40.7128)'),
    30000);

Postgres 使用索引并查找行。问题是,如果您还想考虑 Bob 的偏好,您需要使用两者的最小距离来评估两个用户是否足够接近。

Alice will only date users within 30km
Bob will only date users within 60km

我的(天真的)查询现在是:

=> SELECT COUNT(*) FROM users WHERE
   ST_DWithin(ST_GeographyFromText('SRID=4326;POINT(' ||
      users.longitude || ' ' || users.latitude || ')'),
    ST_GeographyFromText('SRID=4326;POINT(' ||
      '-74.006 40.7128)'),
    LEAST(users.distance, 30000));

此时,Postgres 决定索引不再有用,并切换到顺序扫描表。这大概是因为我需要比较每一行并使用距离容差较小的人。有没有办法可以存储点旁边的距离?也许使用btree_gist?我相对确定我不能从文档中“包括”旁边的距离......

...非键列不能用于索引扫描搜索限定...

https://www.postgresql.org/docs/11/sql-createindex.html

【问题讨论】:

    标签: postgresql postgis


    【解决方案1】:

    也许最好的解决方案是同时应用两个条件。

    SELECT COUNT(*) FROM users WHERE
       ST_DWithin(ST_GeographyFromText('SRID=4326;POINT(' || users.longitude || ' ' || users.latitude || ')'),
                  ST_GeographyFromText('SRID=4326;POINT('|| '-74.006 40.7128)'),
                   30000) 
       AND 
       ST_DWithin(ST_GeographyFromText('SRID=4326;POINT(' || users.longitude || ' ' || users.latitude || ')'),
               ST_GeographyFromText('SRID=4326;POINT('|| '-74.006 40.7128)'),
               users.distance);
    

    (但实际上您可能应该使用 ST_makepoint,而不是一直转换为文本)。

    这将使用具有硬编码距离的条件上的索引来获取候选列表,然后将遍历它们以单独过滤掉不满足其他条件的那些。

    有没有办法可以存储点旁边的距离?

    您可以索引一个圆,该圆的半径为每个用户的点位置周围的用户定义量,但不清楚您如何使用该索引。它对某些查询很有用,但可能不是您要运行的查询。

    CREATE INDEX index_on_users_datable_circle ON users
      USING gist (st_buffer(ST_GeographyFromText('SRID=4326;POINT(' || users.longitude || ' ' || users.latitude || ')'),users.distance));
    

    【讨论】:

      【解决方案2】:

      它不会使用索引,因为每行的距离值(至少可能)不同。

      您可以使用 Alice 的距离进行查询 - 并利用索引,然后使用其他用户距离进行优化。

      类似:

      SELECT COUNT(*) 
      FROM users 
      WHERE ST_DWithin(user.geog, alice.geog, 3000)
       AND  ST_DWithin(user.geog, alice.geog, user.distance)
      

      如果它不起作用,您可能必须将查询一分为二,首先使用物化 CTE 作为 Alice 的距离,然后是用户距离

      PS:帮自己一个忙,将地理位置存储在自己的列中...

      【讨论】:

        猜你喜欢
        • 2013-10-01
        • 1970-01-01
        • 2018-10-10
        • 2021-11-17
        • 2019-02-25
        • 2014-08-08
        • 1970-01-01
        • 1970-01-01
        • 2016-04-19
        相关资源
        最近更新 更多