【问题标题】:Spatial query on large table with multiple self joins performing slow具有多个自连接的大型表的空间查询执行缓慢
【发布时间】:2015-10-06 15:27:57
【问题描述】:

我正在对 Postgres 9.3.9 中的一个大表进行查询。它是一个空间数据集,并且是空间索引的。比如说,我需要找到 3 种类型的对象:A、B 和 C。标准是 B 和 C 都在 A 的一定距离内,比如 500 米。

我的查询是这样的:

select 
  school.osm_id as school_osm_id, 
  school.name as school_name, 
  school.way as school_way, 
  restaurant.osm_id as restaurant_osm_id, 
  restaurant.name as restaurant_name, 
  restaurant.way as restaurant_way, 
  bar.osm_id as bar_osm_id, 
  bar.name as bar_name, 
  bar.way as bar_way 
from (
    select osm_id, name, amenity, way, way_geo 
    from planet_osm_point 
    where amenity = 'school') as school, 
   (select osm_id, name, amenity, way, way_geo 
    from planet_osm_point 
    where amenity = 'restaurant') as restaurant, 
   (select osm_id, name, amenity, way, way_geo 
    from planet_osm_point 
    where amenity = 'bar') as bar 
where ST_DWithin(school.way_geo, restaurant.way_geo, 500, false) 
  and ST_DWithin(school.way_geo, bar.way_geo, 500, false);

这个查询给了我我想要的,但它需要很长时间,比如 13 秒才能执行。我想知道是否有另一种方法来编写查询并提高效率。

查询计划:

Nested Loop  (cost=74.43..28618.65 rows=1 width=177) (actual time=33.513..11235.212 rows=10591 loops=1)
   Buffers: shared hit=530967 read=8733
   ->  Nested Loop  (cost=46.52..28586.46 rows=1 width=174) (actual time=31.998..9595.212 rows=4235 loops=1)
         Buffers: shared hit=389863 read=8707
         ->  Bitmap Heap Scan on planet_osm_point  (cost=18.61..2897.83 rows=798 width=115) (actual time=7.862..150.607 rows=8811 loops=1)
               Recheck Cond: (amenity = 'school'::text)
               Buffers: shared hit=859 read=5204
               ->  Bitmap Index Scan on idx_planet_osm_point_amenity  (cost=0.00..18.41 rows=798 width=0) (actual time=5.416..5.416 rows=8811 loops=1)
                     Index Cond: (amenity = 'school'::text)
                     Buffers: shared hit=3 read=24
         ->  Bitmap Heap Scan on planet_osm_point planet_osm_point_1  (cost=27.91..32.18 rows=1 width=115) (actual time=1.064..1.069 rows=0 loops=8811)
               Recheck Cond: ((way_geo && _st_expand(planet_osm_point.way_geo, 500::double precision)) AND (amenity = 'restaurant'::text))
               Filter: ((planet_osm_point.way_geo && _st_expand(way_geo, 500::double precision)) AND _st_dwithin(planet_osm_point.way_geo, way_geo, 500::double precision, false))
               Rows Removed by Filter: 0
               Buffers: shared hit=389004 read=3503
               ->  BitmapAnd  (cost=27.91..27.91 rows=1 width=0) (actual time=1.058..1.058 rows=0 loops=8811)
                     Buffers: shared hit=384528 read=2841
                     ->  Bitmap Index Scan on idx_planet_osm_point_waygeo  (cost=0.00..9.05 rows=137 width=0) (actual time=0.193..0.193 rows=64 loops=8811)
                           Index Cond: (way_geo && _st_expand(planet_osm_point.way_geo, 500::double precision))
                           Buffers: shared hit=146631 read=2841
                     ->  Bitmap Index Scan on idx_planet_osm_point_amenity  (cost=0.00..18.41 rows=798 width=0) (actual time=0.843..0.843 rows=6291 loops=8811)
                           Index Cond: (amenity = 'restaurant'::text)
                           Buffers: shared hit=237897
   ->  Bitmap Heap Scan on planet_osm_point planet_osm_point_2  (cost=27.91..32.18 rows=1 width=115) (actual time=0.375..0.383 rows=3 loops=4235)
         Recheck Cond: ((way_geo && _st_expand(planet_osm_point.way_geo, 500::double precision)) AND (amenity = 'bar'::text))
         Filter: ((planet_osm_point.way_geo && _st_expand(way_geo, 500::double precision)) AND _st_dwithin(planet_osm_point.way_geo, way_geo, 500::double precision, false))
         Rows Removed by Filter: 1
         Buffers: shared hit=141104 read=26
         ->  BitmapAnd  (cost=27.91..27.91 rows=1 width=0) (actual time=0.368..0.368 rows=0 loops=4235)
               Buffers: shared hit=127019
               ->  Bitmap Index Scan on idx_planet_osm_point_waygeo  (cost=0.00..9.05 rows=137 width=0) (actual time=0.252..0.252 rows=363 loops=4235)
                     Index Cond: (way_geo && _st_expand(planet_osm_point.way_geo, 500::double precision))
                     Buffers: shared hit=101609
               ->  Bitmap Index Scan on idx_planet_osm_point_amenity  (cost=0.00..18.41 rows=798 width=0) (actual time=0.104..0.104 rows=779 loops=4235)
                     Index Cond: (amenity = 'bar'::text)
                     Buffers: shared hit=25410
 Total runtime: 11238.605 ms

目前我只使用一张表,其中有 1,372,711 行。它有 73 列

       Column       |         Type         |       Modifiers
--------------------+----------------------+---------------------------
 osm_id             | bigint               | 
 access             | text                 | 
 addr:housename     | text                 | 
 addr:housenumber   | text                 | 
 addr:interpolation | text                 | 
 admin_level        | text                 | 
 aerialway          | text                 | 
 aeroway            | text                 | 
 amenity            | text                 | 
 area               | text                 | 
 barrier            | text                 | 
 bicycle            | text                 | 
 brand              | text                 | 
 bridge             | text                 | 
 boundary           | text                 | 
 building           | text                 | 
 capital            | text                 | 
 construction       | text                 | 
 covered            | text                 | 
 culvert            | text                 | 
 cutting            | text                 | 
 denomination       | text                 | 
 disused            | text                 | 
 ele                | text                 | 
 embankment         | text                 | 
 foot               | text                 | 
 generator:source   | text                 | 
 harbour            | text                 | 
 highway            | text                 | 
 historic           | text                 | 
 horse              | text                 | 
 intermittent       | text                 | 
 junction           | text                 | 
 landuse            | text                 | 
 layer              | text                 | 
 leisure            | text                 | 
 lock               | text                 | 
 man_made           | text                 | 
 military           | text                 | 
 motorcar           | text                 | 
 name               | text                 | 
 natural            | text                 | 
 office             | text                 | 
 oneway             | text                 | 
 operator           | text                 | 
 place              | text                 | 
 poi                | text                 | 
 population         | text                 | 
 power              | text                 | 
 power_source       | text                 | 
 public_transport   | text                 | 
 railway            | text                 | 
 ref                | text                 | 
 religion           | text                 | 
 route              | text                 | 
 service            | text                 | 
 shop               | text                 | 
 sport              | text                 | 
 surface            | text                 | 
 toll               | text                 | 
 tourism            | text                 | 
 tower:type         | text                 | 
 tunnel             | text                 | 
 water              | text                 | 
 waterway           | text                 | 
 wetland            | text                 | 
 width              | text                 | 
 wood               | text                 | 
 z_order            | integer              | 
 tags               | hstore               | 
 way                | geometry(Point,4326) | 
 way_geo            | geography            | 
 gid                | integer              | not null default nextval('...
Indexes:
    "planet_osm_point_pkey1" PRIMARY KEY, btree (gid)
    "idx_planet_osm_point_amenity" btree (amenity)
    "idx_planet_osm_point_waygeo" gist (way_geo)
    "planet_osm_point_index" gist (way)
    "planet_osm_point_pkey" btree (osm_id)

便利学校、餐厅、酒吧分别有8811、6291、779排。

【问题讨论】:

  • 由于您交叉连接同一个表 3 次,您可能会得到相同记录的 3 个或更多结果。 table1 中有多少条记录,您希望通过匹配获得多少条记录?这可以告诉我们 13 秒是否太长。如果没有完整的连接语法,很难相信“这个查询给了我想要的东西”。
  • 您的编辑改进了一些事情,但您并没有一路走好。基本细节仍然缺失。考虑 instructions here。点击链接并阅读。另外,特别是:类型为ABC 的行(大约)有多少?在 psql 中提供\d table1 的输出(不带无关列)、EXPLAIN (ANALYZE, BUFFERS) 的输出以及所有相关索引。
  • @ErwinBrandstetter 我更改了查询。桌子很稀疏。例如,如果我创建子表,我会创建一个表,该表只包含用于便利设施等的无空值的记录,这将加快进程。但问题是我有 73 列,我可能需要创建大约 70 个新表...
  • @ErwinBrandstetter 原来的表只有way,是几何,但是我需要在缓冲区中使用米所以我创建了一个地理列way_geo,但最终我需要纬度和经度,所以我猜测显示几何或地理都可以。查询可以是任何事物,甚至可以是超过三个事物(最少两个事物),并且每个事物都可以是任何列,除了 id、主键、地理和几何。是的,表很稀疏,很多都是空的,但我认为每一列至少有一个条目
  • "any of the columns" ... 如果只是 amenity 列,我会有一些想法来加快速度,但是大约 70 列中的任何一个都很难索引。我认为你有一个数据库设计问题,就像你已经暗示过自己一样。另一个问题:您在究竟的结果中寻找什么?当前查询返回距离同一学校足够近的所有餐馆和酒吧的交叉连接。对于一个有 10 人休息的学校。附近有 10 家酒吧,您可以获得 100 行。如果您再组合 2 个项目,每个项目有 10 个命中,那已经是 10.000 行。这真的是你想要的吗?

标签: sql postgresql postgis spatial postgresql-performance


【解决方案1】:

如果你使用显式连接有什么不同吗?

SELECT a.id as a_id, a.name as a_name, a.geog as a_geog,
       b.id as b_id, b.name as b_name, b.geog as b_geog,
       c.id as c_id, c.name as c_name, c.geog as c_geog
FROM table1 a
JOIN table1 b ON b.type = 'B' AND ST_DWithin(a.geog, b.geog, 100)
JOIN table1 c ON c.type = 'C' AND ST_DWithin(a.geog, c.geog, 100)
WHERE a.type = 'A';

【讨论】:

  • 它不应该对 Postgres 产生影响。普通内部连接的条件和 WHERE 条件都被视为相同。
  • 没什么区别
【解决方案2】:

用内连接语法试试这个并比较结果,应该没有重复。我的猜测是它应该比原始查询花费 1/3 或更好的时间:

select a.id as a_id, a.name as a_name, a.geog as a_geo,
       b.id as b_id, b.name as b_name, b.geog as b_geo,
       c.id as c_id, c.name as c_name, c.geog as c_geo
from table1 as a
INNER JOIN table1 as b on b.type='B'
INNER JOIN table1 as c on c.type='C'
WHERE a.type='A' and
     (ST_DWithin(a.geo, b.geo, 100) and ST_DWithin(a.geo, c.geo, 100))

【讨论】:

  • 现在它可以工作了,但它会烧毁到与原始查询或 Mike 已经发布的查询计划相同的查询计划。 OP 确实需要提供更多输入。
  • @ErwinBrandstetter,我同意。
  • 没什么区别
【解决方案3】:

您使用的 3 个子选择非常低效。将它们写成LEFT JOIN 子句,查询应该会更有效率:

SELECT
  school.osm_id AS school_osm_id, 
  school.name AS school_name, 
  school.way AS school_way, 
  restaurant.osm_id AS restaurant_osm_id, 
  restaurant.name AS restaurant_name, 
  restaurant.way AS restaurant_way, 
  bar.osm_id AS bar_osm_id, 
  bar.name AS bar_name, 
  bar.way AS bar_way 
FROM planet_osm_point school
LEFT JOIN planet_osm_point restaurant ON restaurant.amenity = 'restaurant' AND
                               ST_DWithin(school.way_geo, restaurant.way_geo, 500, false) 
LEFT JOIN planet_osm_point bar ON bar.amenity = 'bar' AND
                               ST_DWithin(school.way_geo, bar.way_geo, 500, false)
WHERE school.amenity = 'school'
  AND (restaurant.osm_id IS NOT NULL OR bar.osm_id IS NOT NULL);

但是,如果您每所学校有多家餐厅和酒吧,这会产生太多结果。您可以像这样简化查询:

SELECT
  school.osm_id AS school_osm_id, 
  school.name AS school_name, 
  school.way AS school_way, 
  a.osm_id AS amenity_osm_id, 
  a.amenity AS amenity_type,
  a.name AS amenity_name, 
  a.way AS amenity_way, 
FROM planet_osm_point school
JOIN planet_osm_point a ON ST_DWithin(school.way_geo, a.way_geo, 500, false) 
WHERE school.amenity = 'school'
  AND a.amenity IN ('bar', 'restaurant');

这将为每个学校提供每个酒吧和餐厅。 500m 范围内没有餐厅或酒吧的学校未列出。

【讨论】:

  • 我们可以any有效的表达式放入join子句中。这与其他两个答案的查询计划相同。
  • 我知道我们可以,但这并不意味着我们应该JOIN ... ON b.type = 'B' 根本不加入任何东西。
  • 并不是说我们应该。查询也应该对人眼有意义。仍然对查询计划没有影响。原因如下:stackoverflow.com/a/27420244/939860 - 带有权威报价表和手册链接,因此您不必相信我的话。
  • 感谢您的回答,我尝试了您的查询,它并没有什么不同
  • @Patrick 我试过这个查询,但看起来这个查询提供了更多结果,其中一些不正确。因为有些结果只有学校ID、姓名和路,餐厅和酒吧都是空的,你能再查一下吗?
【解决方案4】:

这个查询应该有很长的路要走(快得多):

WITH school AS (
   SELECT s.osm_id AS school_id, text 'school' AS type, s.osm_id, s.name, s.way_geo
   FROM   planet_osm_point s
        , LATERAL (
      SELECT  1 FROM planet_osm_point
      WHERE   ST_DWithin(way_geo, s.way_geo, 500, false)
      AND     amenity = 'bar'
      LIMIT   1  -- bar exists -- most selective first if possible
      ) b
        , LATERAL (
      SELECT  1 FROM planet_osm_point
      WHERE   ST_DWithin(way_geo, s.way_geo, 500, false)
      AND     amenity = 'restaurant'
      LIMIT   1  -- restaurant exists
      ) r
   WHERE  s.amenity = 'school'
   )
SELECT * FROM (
   TABLE school  -- schools

   UNION ALL  -- bars
   SELECT s.school_id, 'bar', x.*
   FROM   school s
        , LATERAL (
      SELECT  osm_id, name, way_geo
      FROM    planet_osm_point
      WHERE   ST_DWithin(way_geo, s.way_geo, 500, false)
      AND     amenity = 'bar'
      ) x

   UNION ALL  -- restaurants
   SELECT s.school_id, 'rest.', x.*
   FROM   school s
        , LATERAL (
      SELECT  osm_id, name, way_geo
      FROM    planet_osm_point
      WHERE   ST_DWithin(way_geo, s.way_geo, 500, false)
      AND     amenity = 'restaurant'
      ) x
   ) sub
ORDER BY school_id, (type <> 'school'), type, osm_id;

与您的原始查询相同,而是您真正想要的,as per discussion in comments

我想要一份 500 以内餐厅和酒吧的学校列表 米,我需要每所学校的坐标及其对应的 餐厅和酒吧。

因此,此查询返回这些学校的列表,然后是附近的酒吧和餐馆。每组行由school_id 列中学校的osm_id 保持在一起。

现在使用 LATERAL 连接,以利用空间 GiST 索引。

TABLE school 只是SELECT * FROM school 的简写:

表达式(type &lt;&gt; 'school')将每组中的学校排在第一位,因为:

最终SELECT 中的子查询sub 只需要按此表达式排序。 UNION 查询将附加的 ORDER BY 列表限制为仅列,没有表达式。

我专注于您为此答案提出的查询 - 忽略过滤其他 70 个文本列中的任何一个的扩展要求。这真是一个设计缺陷。搜索条件应集中在 少数 列中。或者您必须索引所有 70 列,而像我将建议的多列索引几乎不是一种选择。仍然可能 ...

索引

除了现有的:

"idx_planet_osm_point_waygeo" gist (way_geo)

如果总是过滤同一列,您可以创建一个 multicolumn index 覆盖您感兴趣的少数列,这样 index-only scans 就成为可能: p>

CREATE INDEX planet_osm_point_bar_idx ON planet_osm_point (amenity, name, osm_id)

Postgres 9.5

即将推出的 Postgres 9.5 引入了重大改进,恰好可以解决您的问题:

  • 允许查询使用 GiST 索引对边界框索引对象(多边形、圆形)执行准确的距离过滤 (亚历山大·科罗特科夫,海基·林纳坎加斯)

    以前,需要一个公用表表达式才能返回一个大的 按边界框距离排序,然后过滤的行数 进一步使用更准确的非边界框距离计算。

  • 允许 GiST 索引执行仅索引扫描(Anastasia Lubennikova、Heikki Linnakangas、Andreas Karlsson)

您对此特别感兴趣。现在您可以拥有一个单个多列(覆盖)GiST 索引:

CREATE INDEX reservations_range_idx ON reservations
USING gist(amenity, way_geo, name, osm_id)

还有:

  • 提高位图索引扫描性能(Teodor Sigaev,Tom Lane)

还有:

  • 添加 GROUP BY 分析函数 GROUPING SETSCUBEROLLUP(Andrew Gierth,Atri Sharma)

为什么?因为ROLLUP 会简化我建议的查询。相关答案:

第一个 alpha 版本已于 2015 年 7 月 2 日发布。The expected timeline for the release:

这是 9.5 版的 alpha 版本,表示有一些变化 在发布之前,功能仍然是可能的。 PostgreSQL 项目 8月份会发布9.5 beta 1,之后会定期发布 测试所需的额外测试版,直到最终发布 2015 年末。

基础知识

当然,一定不要忽视基础知识:

【讨论】:

  • 我把 b.amenity 改成了 amenity。我收到此错误:错误:错误:无效的 UNION/INTERSECT/EXCEPT ORDER BY 子句第 32 行:ORDER BY school_id, (type 'school'), type, osm_id; ^ DETAIL:只能使用结果列名,不能使用表达式或函数。提示:将表达式/函数添加到每个 SELECT,或将 UNION 移动到 FROM 子句中。
  • @al3xtouch:修复了这个错误。和这里一样:stackoverflow.com/a/31211776/939860。 (我没有测试就从头顶写了这个查询)
  • 第二个UNION ALL下,x应该是b吧?我仍然得到指向最后一行的错误,order by,它指向单词'type',它说DETAIL:只能使用结果列名,而不是表达式或函数。提示:将表达式/函数添加到每个 SELECT,或将 UNION 移动到 FROM 子句中。
  • 它给了我同样的错误,指向(type 'school')中的'type',具有相同的细节和提示......
  • 感谢您的回答,我在运行查询时收到消息,“注意:gserialized_gist_joinsel:jointype 4 not supported”多次,您知道这是什么意思吗?
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-01-30
  • 2010-12-06
  • 2018-01-13
  • 1970-01-01
相关资源
最近更新 更多