sql - Postgres : How to find nearest tsrange from timestamp outside of ranges?

标签 sql postgresql postgis nearest-neighbor

我正在为供应商提供的本地服务建模(在 Postgres 9.6.1/postGIS 2.3.1 中):

create table supplier (
    id                serial primary key,
    name              text not null check (char_length(title) < 280),
    type              service_type,
    duration          interval,
    ...
    geo_position      geography(POINT,4326)
    ...
);

每个供应商都会保留一份日历,其中列出了他/她可以预订的时间段:

create table timeslot (
    id                 serial primary key,
    supplier_id        integer not null references supplier(id),
    slot               tstzrange not null,

    constraint supplier_overlapping_timeslot_not_allowed
    exclude using gist (supplier_id with =, slot with &&)
);

当客户想知道附近哪些供应商可以在某个时间预订时,我创建一个 View 和一个函数:

create view supplier_slots as
    select
        supplier.name, supplier.type, supplier.geo_position, supplier.duration, ...
        timeslot.slot
    from
        supplier, timeslot
    where
        supplier.id = timeslot.supplier_id;


create function find_suppliers(wantedType service_type, near_latitude text, near_longitude text, at_time timestamptz)
returns setof supplier_slots as $$
declare
    nearpoint geography;
begin
    nearpoint := ST_GeographyFromText('SRID=4326;POINT(' || near_latitude || ' ' || near_longitude || ')');
    return query
        select * from supplier_slots
        where type = wantedType
            and tstzrange(at_time, at_time + duration) <@ slot
        order by ST_Distance( nearpoint, geo_position )
        limit 100;
end;
$$ language plpgsql;

所有这些都非常有效。

现在,对于在请求的时间没有可预订时段的供应商,我想找到他们在请求的 at_time 之前和之后的最近可用时段>,也按距离排序。

这让我有点旋转,我找不到任何合适的运算符来给我最近的 tsrange。

关于最明智的方法有什么想法吗?

最佳答案

解决方案取决于您想要的准确定义。

架构

我建议这些稍微调整的表定义可以使任务更简单,增强完整性并提高性能:

CREATE TABLE supplier (
   supplier_id  serial PRIMARY KEY,
   supplier     text NOT NULL CHECK (length(title) < 280),
   type         service_type,
   duration     interval,
   geo_position geography(POINT,4326)
);

CREATE TABLE timeslot (
   timeslot_id  serial PRIMARY KEY,
   supplier_id  integer NOT NULL -- references supplier(id),
   slot_a       timestamptz NOT NULL,
   slot_z       timestamptz NOT NULL,
   CONSTRAINT   timeslot_range_valid CHECK (slot_a < slot_z)
   CONSTRAINT   timeslot_no_overlapping
     EXCLUDE USING gist (supplier_id WITH =, tstzrange(slot_a, slot_z) WITH &&)
);

CREATE INDEX timeslot_slot_z ON timeslot (supplier_id, slot_z);
CREATE INDEX supplier_geo_position_gist ON supplier USING gist (geo_position);
  • 保存两个 timestamptzslot_aslot_z,而不是 tstzrangeslot - 并相应地调整约束。现在,这会自动将所有范围视为默认的包含下限和排除上限 - 这可以避免极端情况错误/头痛。

    附带好处:2 个 timestamptz 只需 16 个字节,而不是 tstzrange 的 25 个字节(带填充的 32 个字节)。

  • 您可能对 slot 进行过的所有查询都可以继续使用 tstzrange(slot_a, slot_z) 作为直接替换。

  • (supplier_id, slot_z)上为当前查询添加索引。
    以及 supplier.geo_position 上的空间索引(您可能已经有了)。

    根据类型中的数据分布,查询中常见类型的几个部分索引可能有助于提高性能:

    CREATE INDEX supplier_geo_type_foo_gist ON supplier USING gist (geo_position)
    WHERE supplier = 'foo'::service_type;
    

查询/函数

此查询查找提供正确service_typeX个最接近的供应商(示例中为100个),每个供应商都有一个最接近的匹配时间时隙(由到时隙开始的时间距离定义)。我将其与实际匹配的插槽结合起来,这可能是也可能不是您需要的。

CREATE FUNCTION f_suppliers_nearby(_type service_type, _lat text, _lon text, at_time timestamptz)
  RETURNS TABLE (supplier_id  int
               , name         text
               , duration     interval
               , geo_position geography(POINT,4326)
               , distance     float 
               , timeslot_id  int
               , slot_a       timestamptz
               , slot_z       timestamptz
               , time_dist    interval
   ) AS
$func$
   WITH sup_nearby AS (  -- find matching or later slot
      SELECT s.id, s.name, s.duration, s.geo_position
           , ST_Distance(ST_GeographyFromText('SRID=4326;POINT(' || _lat || ' ' || _lon || ')')
                          , geo_position) AS distance
           , t.timeslot_id, t.slot_a, t.slot_z
           , CASE WHEN t.slot_a IS NOT NULL
                  THEN GREATEST(t.slot_a - at_time, interval '0') END AS time_dist
      FROM   supplier s
      LEFT   JOIN LATERAL (
         SELECT *
         FROM   timeslot
         WHERE  supplier_id = supplier_id
         AND    slot_z > at_time + s.duration  -- excl. upper bound
         ORDER  BY slot_z
         LIMIT  1
         ) t ON true
      WHERE  s.type = _type
      ORDER  BY s.distance
      LIMIT  100
      )
   SELECT *
   FROM  (
      SELECT DISTINCT ON (supplier_id) *  -- 1 slot per supplier
      FROM  (
         TABLE sup_nearby  -- matching or later slot

         UNION ALL         -- earlier slot
         SELECT s.id, s.name, s.duration, s.geo_position
              , s.distance
              , t.timeslot_id, t.slot_a, t.slot_z
              , GREATEST(at_time - t.slot_a, interval '0') AS time_dist
         FROM   sup_nearby s
         CROSS  JOIN LATERAL (  -- this time CROSS JOIN!
            SELECT *
            FROM   timeslot
            WHERE  supplier_id = s.supplier_id
            AND    slot_z <= at_time  -- excl. upper bound
            ORDER  BY slot_z DESC
            LIMIT  1
            ) t
         WHERE  s.time_dist IS DISTINCT FROM interval '0'  -- exact matches are done
         ) sub
      ORDER  BY supplier_id, time_dist  -- pick temporally closest slot per supplier
   ) sub
   ORDER  BY time_dist, distance;  -- matches first, ordered by distance; then misses, ordered by time distance

$func$  LANGUAGE sql;

我没有使用您的 View supplier_slots,而是针对性能进行了优化。风景可能还是很方便的。您可以包含 tstzrange(slot_a, slot_z) AS 插槽以实现向后兼容性。

查找最近 100 个供应商的基本查询是教科书上的“K 最近邻”问题。 GiST 索引对此非常有效。相关:

附加任务(查找时间上最近的槽)可以分为两个任务:查找下一个较高的行和下一个较低的行。该解决方案的核心功能是拥有两个子查询,其中ORDER BY slot_z LIMIT 1ORDER BY slot_z DESC LIMIT 1 code>,这会导致两次非常快的索引扫描。

我将第一个与查找实际匹配项结合起来,这是一种(我认为很聪明)优化,但可能会分散实际解决方案的注意力。

关于sql - Postgres : How to find nearest tsrange from timestamp outside of ranges?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/41208541/

相关文章:

postgresql - 如何从无效的线串中构建区域(或防止错误)

sql - 从多个表中获取数据

postgresql - 将 postgis 备份从 1.5 恢复到 2.1.2

sql - 存储为数字而不是文本对数据库性能有何改进?

MySQL 从两个数据库表中获取最新条目

postgresql - 使用 PostgreSQL JDBC 的连接池

windows - 在 Windows 桌面上调整 postgreSQL 以利用 24GB RAM

mysql - 将项目从一个数据库插入到另一个 SQL 语法

sql - 我们可以使用校验和来检查该行是否已更改(sql server)?

java - 如何配置 Hibernate 以使用大写和小写字母制作表/列名称