postgresql - LATERAL JOIN 不使用三元组索引

标签 postgresql indexing query-optimization nearest-neighbor postgresql-9.4

我想使用 Postgres 对地址进行一些基本的地理编码。我有一个包含大约 100 万个原始地址字符串的地址表:

=> \d addresses
  Table "public.addresses"
 Column  | Type | Modifiers
---------+------+-----------
 address | text |

我还有一个位置数据表:

=> \d locations
   Table "public.locations"
   Column   | Type | Modifiers
------------+------+-----------
 id         | text |
 country    | text |
 postalcode | text |
 latitude   | text |
 longitude  | text |

大多数地址字符串都包含邮政编码,所以我的第一个尝试是做一个类似的横向连接:

EXPLAIN SELECT * FROM addresses a
JOIN LATERAL (
    SELECT * FROM locations
    WHERE address ilike '%' || postalcode || '%'
    ORDER BY LENGTH(postalcode) DESC
    LIMIT 1
) AS l ON true;

这给出了预期的结果,但速度很慢。这是查询计划:

                                      QUERY PLAN
--------------------------------------------------------------------------------------
 Nested Loop  (cost=18383.07..18540688323.77 rows=1008572 width=91)
   ->  Seq Scan on addresses a  (cost=0.00..20997.72 rows=1008572 width=56)
   ->  Limit  (cost=18383.07..18383.07 rows=1 width=35)
         ->  Sort  (cost=18383.07..18391.93 rows=3547 width=35)
               Sort Key: (length(locations.postalcode))
               ->  Seq Scan on locations  (cost=0.00..18365.33 rows=3547 width=35)
                     Filter: (a.address ~~* (('%'::text || postalcode) || '%'::text))

我尝试在地址列中添加一个主旨三元组索引,如 https://stackoverflow.com/a/13452528/36191 中所述,但是上面查询的查询计划没有使用它,查询计划没有改变。

CREATE INDEX idx_address ON addresses USING gin (address gin_trgm_ops);

我必须删除横向连接查询中的 order by 和 limit 才能使用索引,这不会给我想要的结果。下面是没有 ORDERLIMIT 的查询的查询计划:

                                          QUERY PLAN
-----------------------------------------------------------------------------------------------
 Nested Loop  (cost=39.35..129156073.06 rows=3577682241 width=86)
   ->  Seq Scan on locations  (cost=0.00..12498.55 rows=709455 width=28)
   ->  Bitmap Heap Scan on addresses a  (cost=39.35..131.60 rows=5043 width=58)
         Recheck Cond: (address ~~* (('%'::text || locations.postalcode) || '%'::text))
         ->  Bitmap Index Scan on idx_address  (cost=0.00..38.09 rows=5043 width=0)
               Index Cond: (address ~~* (('%'::text || locations.postalcode) || '%'::text))

我可以做些什么来让查询使用索引,还是有更好的方法来重写这个查询?

最佳答案

为什么?

查询不能使用主体上的索引。您需要表上的索引 locations , 但你的那个在 table 上 addresses .

您可以通过设置来验证我的声明:

SET enable_seqscan = off;

(仅在您的 session 中,仅用于调试。切勿在生产中使用它。)索引不会比顺序扫描更昂贵,Postgres 没有办法将它用于您的查询完全

旁白:[INNER] JOIN ... ON true只是一种尴尬的说法 CROSS JOIN ...

为什么去掉ORDER后还要用索引和 LIMIT

因为 Postgres 可以将这个简单的形式重写为:

SELECT *
FROM   addresses a
JOIN   locations l ON a.address ILIKE '%' || l.postalcode || '%';

您将看到完全相同的查询计划。 (至少我在 Postgres 9.5 上的测试中是这样做的。)

解决方案

您需要关于 locations.postalcode 的索引.在使用 LIKE 时或 ILIKE您还需要将索引表达式 ( postalcode ) 放在运算符的左侧 侧。 ILIKE由运算符 ~~* 实现而这个运营商没有 COMMUTATOR (逻辑上的必要),所以不可能翻转操作数。这些相关答案中的详细解释:

一个解决方案是使用 trigram similarity operator % 或者它的倒数,distance operator <-> 相反,在最近的邻居查询中(每个都是自身的换向器,因此操作数可以自由切换位置):

SELECT *
FROM   addresses a
JOIN   LATERAL (
   SELECT *
   FROM   locations
   ORDER  BY postalcode <b><-></b> a.address
   LIMIT  1
   ) l ON address ILIKE '%' || postalcode || '%';

找到最相似的 postalcode对于每个 address ,然后检查是否 postalcode实际上完全匹配。

这样,更长的postalcode将自动成为首选,因为它比更短的 postalcode 更相似(距离更小)这也匹配。

仍然存在一些不确定性。根据可能的邮政编码,由于字符串其他部分中的三元组匹配,可能会出现误报。题中信息不足,不多说。

在这里[INNER] JOIN而不是 CROSS JOIN这是有道理的,因为我们添加了一个实际的连接条件。

The manual:

This can be implemented quite efficiently by GiST indexes, but not by GIN indexes.

所以:

CREATE INDEX locations_postalcode_trgm_gist_idx ON locations
USING gist (postalcode gist_trgm_ops);

关于postgresql - LATERAL JOIN 不使用三元组索引,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/37267109/

相关文章:

sql - 从 Postgres 中的复合类型数组中进行选择

java - 使用 Spring 的 NamedParamterJdbcTemplate 和命名参数进行分组

ruby-on-rails - Postgres 索引对原始 SQL 和 ActiveRecord 查询的影响截然不同

java - 使 GWT (Ajax) URL 可抓取/可索引的正确方法

mysql - 优化按时间戳排序的大表的 SQL 查询

ruby-on-rails - 使用 Postgresql 数据库创建 Rails 3.1 应用程序 (OSX Lion)

sql-server - Power BI 和 SQL Server 索引

c++ - 返回二维 vector 的结束索引

Mysql 在非常大的表上计算性能

sql - 日期时间上的 "BETWEEN AND"和 "> AND <="有什么区别?