在一个查询中执行多次PgSQL全文搜索

时间:2018-04-12 15:17:02

标签: sql postgresql performance join full-text-search

我在PgSQL中使用这样的查询进行全文搜索(目的是根据搜索到的城市名称返回地理编码城市列表):

SELECT * 
FROM   cities 
WHERE  city_tsvector @@ to_tsquery('Paris') 
AND    postcode LIKE '75%'

此查询在我的数据库上执行得非常快(城市表中的425626个条目):大约100毫秒。到目前为止,非常好。

现在,我必须同时在400个城市进行搜索。

400 x 100ms = 40秒,这对我的用户来说太长了。

我正在尝试编写单个查询,以便一次执行此搜索。一个细节:我必须搜索的城市没有存储在数据库中。

所以我写了这样的查询:

SELECT DISTINCT ON (myid) *
FROM unnest(
    array[19977,19978,19979, (and so on)]::int[],
    array['SAULXURES','ARGENTEUIL','OBERHOFFEN&SUR&MODER', (and so on)]::text[],
    array['67','95','67','44', (and so on))]::text[]
) AS t(myid, cityname,mypostcode)
 LEFT JOIN cities gc2 ON gc2.city_tsvector @@ to_tsquery(cityname) AND gc2.postcode LIKE CONCAT(mypostcode,'%')
 ORDER BY myid
;

结果只是灾难性的:对于相同的搜索,查询的速度要慢4倍!

是否可以执行此类查询以便执行时间更短?

由于

修改

这是表城市结构(425626行):

table structure

使用@ The-Impaler回复编辑:

  • 选项#1:邮政编码前两个字符的索引

查询需要11秒

EXPLAIN VERBOSE:


    Unique  (cost=71133.21..71138.53 rows=100 width=40)
      Output: t.myid, (st_astext(gc2.gps_coordinates))
      ->  Sort  (cost=71133.21..71135.87 rows=1064 width=40)
            Output: t.myid, (st_astext(gc2.gps_coordinates))
            Sort Key: t.myid
            ->  Hash Right Join  (cost=2.26..71079.72 rows=1064 width=40)
                  Output: t.myid, st_astext(gc2.gps_coordinates)
                  Hash Cond: (left((gc2.postcode)::text, 2) = t.mypostcode)
                  Join Filter: (gc2.city_tsvector @@ to_tsquery(t.cityname))
                  ->  Seq Scan on public.geo_cities gc2  (cost=0.00..13083.26 rows=425626 width=69)
                        Output: gc2.id, gc2.country_code, gc2.city, gc2.postcode, gc2.gps_coordinates, gc2.administrative_level_1_name, gc2.administrative_level_1_code, gc2.administrative_level_2_name, gc2.administrative_level_2_code, gc2.administrative_level_ (...)
                  ->  Hash  (cost=1.01..1.01 rows=100 width=72)
                        Output: t.myid, t.cityname, t.mypostcode
                        ->  Function Scan on t  (cost=0.01..1.01 rows=100 width=72)
                              Output: t.myid, t.cityname, t.mypostcode
                              Function Call: unnest('{289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,28914887922 (...)

  • 选项#2:通过邮政编码过滤时使用=而不是LIKE

查询需要12秒

EXPLAIN VERBOSE:


    Unique  (cost=71665.25..71670.57 rows=100 width=40)
      Output: t.myid, (st_astext(gc2.gps_coordinates))
      ->  Sort  (cost=71665.25..71667.91 rows=1064 width=40)
            Output: t.myid, (st_astext(gc2.gps_coordinates))
            Sort Key: t.myid
            ->  Hash Right Join  (cost=2.26..71611.75 rows=1064 width=40)
                  Output: t.myid, st_astext(gc2.gps_coordinates)
                  Hash Cond: ((substring((gc2.postcode)::text, 1, 2))::text = t.mypostcode)
                  Join Filter: (gc2.city_tsvector @@ to_tsquery(t.cityname))
                  ->  Seq Scan on public.geo_cities gc2  (cost=0.00..13083.26 rows=425626 width=69)
                        Output: gc2.id, gc2.country_code, gc2.city, gc2.postcode, gc2.gps_coordinates, gc2.administrative_level_1_name, gc2.administrative_level_1_code, gc2.administrative_level_2_name, gc2.administrative_level_2_code, gc2.administrative_level_ (...)
                  ->  Hash  (cost=1.01..1.01 rows=100 width=72)
                        Output: t.myid, t.cityname, t.mypostcode
                        ->  Function Scan on t  (cost=0.01..1.01 rows=100 width=72)
                              Output: t.myid, t.cityname, t.mypostcode
                              Function Call: unnest('{289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,289148879225,28914887922 (...)

1 个答案:

答案 0 :(得分:1)

第一部分 - 在postcode上执行索引范围扫描。

关键优化是让每个搜索工作在4000行而不是400k行,如果按照邮政编码的前两位数进行过滤,这应该很容易。

选项#1 :在邮政编码的前两个字符上创建索引:

create index ix1 on cities (substring(postcode from 1 for 2));

选项#2 :按=过滤时,请使用LIKE代替postcode。您需要创建一个伪列minipostcode并在其上创建索引:

create or replace function minipostcode(cities)
returns char(2) as $$
  select substring($1.postcode from 1 for 2)
$$ stable language sql;

create index ix_cities_minipostcode on cities(minipostcode(cities));

您的SQL将更改为:

SELECT DISTINCT ON (myid) *
FROM unnest(
    array[19977,19978,19979, (and so on)]::int[],
    array['SAULXURES','ARGENTEUIL','OBERHOFFEN&SUR&MODER', (and so on)]::text[],
    array['67','95','67','44', (and so on))]::text[]
) AS t(myid, cityname,mypostcode)
 LEFT JOIN cities gc2 ON gc2.city_tsvector @@ to_tsquery(cityname) 
       AND gc2.minipostcode = mypostcode
 ORDER BY myid
;

请参阅那里的gc2.minipostcode =

检查执行计划

获取上述每个选项的执行计划,并使用以下方法进行比较:

explain verbose <my-query>

请发布两个选项的执行计划。

第二部分 - 减少扫描次数。

一旦您确定它使用postcode上的索引范围扫描,您就可以进一步优化它。

考虑到您的搜索有400个值,每个postcode可能重复约4次。然后,为每个邮政编码执行一次索引范围扫描,只需执行此操作即可将执行时间缩短75%。

但是,您不能使用纯SQL执行此操作,您需要预处理SQL,每postcode生成一个查询。您将不再使用unnest,但您将使用您的应用程序语言预构建SQL。

例如,由于67在您的示例中显示两次,因此应将其合并为索引范围扫描,从而产生如下内容:

select from cities gc2 
  where (gc2.city_tsvector @@ to_tsquery('SAULXURES') 
     or gc2.city_tsvector @@ to_tsquery('OBERHOFFEN&SUR&MODER')) 
    and gc2.minipostcode = '67'
union
(next select for another postcode here, and so on...)

这比unnest选项更优化,因为它执行最多 100索引范围扫描,即使您将搜索增加到1000或5000条件。

试试这个,并发布新的执行计划。