LATERAL JOIN不使用trigram索引

Ben*_*ing 8 postgresql indexing query-optimization nearest-neighbor postgresql-9.4

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

=> \d addresses
  Table "public.addresses"
 Column  | Type | Modifiers
---------+------+-----------
 address | text |
Run Code Online (Sandbox Code Playgroud)

我还有一张位置数据表:

=> \d locations
   Table "public.locations"
   Column   | Type | Modifiers
------------+------+-----------
 id         | text |
 country    | text |
 postalcode | text |
 latitude   | text |
 longitude  | text |
Run Code Online (Sandbox Code Playgroud)

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

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;
Run Code Online (Sandbox Code Playgroud)

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

                                      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))
Run Code Online (Sandbox Code Playgroud)

我尝试在地址列中添加一个gist trigram索引,如/sf/answers/941676991/中所述,但上述查询的查询计划没有使用它,查询计划在不变.

CREATE INDEX idx_address ON addresses USING gin (address gin_trgm_ops);
Run Code Online (Sandbox Code Playgroud)

我必须删除顺序和限制横向连接查询以使用索引,这不会给我我想要的结果.这是不带ORDER或的查询的查询计划LIMIT:

                                          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))
Run Code Online (Sandbox Code Playgroud)

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

Erw*_*ter 5

为什么?

查询不能使用主体上的索引。您需要在桌子上建立一个索引locations,但您拥有的索引在桌子上addresses

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

SET enable_seqscan = off;
Run Code Online (Sandbox Code Playgroud)

(在你的会话只,并仅用于调试。切勿将其投入生产。)它不是像指数会比顺序扫描更昂贵,就是没有办法Postgres的使用它为您的查询在所有

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

为什么在删除ORDER和之后使用索引LIMIT

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

SELECT *
FROM   addresses a
JOIN   locations l ON a.address ILIKE '%' || l.postalcode || '%';
Run Code Online (Sandbox Code Playgroud)

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

解决方案

你需要一个索引locations.postalcode。并且在使用LIKEor 时,ILIKE您还需要将索引表达式 ( postalcode) 带到运算符的左侧ILIKE是用运算符实现的~~*,这个运算符没有COMMUTATOR(逻辑上的必要性),所以不可能翻转操作数。这些相关答案中的详细解释:

一种解决方案是使用三元语法相似性运算符%或其倒数,所述距离操作者<->在一个最近邻查询,而不是(各为本身换向器,因此操作数可以自由地交换位置):

SELECT *
FROM   addresses a
JOIN   LATERAL (
   SELECT *
   FROM   locations
   ORDER  BY postalcode <-> a.address
   LIMIT  1
   ) l ON address ILIKE '%' || postalcode || '%';
Run Code Online (Sandbox Code Playgroud)

postalcode为 each找到最相似的address,然后检查它是否postalcode真正完全匹配。

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

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

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

手册:

这可以通过 GiST 索引非常有效地实现,但不能通过 GIN 索引实现。

所以:

CREATE INDEX locations_postalcode_trgm_gist_idx ON locations
USING gist (postalcode gist_trgm_ops);
Run Code Online (Sandbox Code Playgroud)