代码之家  ›  专栏  ›  技术社区  ›  Fred Hors

如果我使用带有5个字符的搜索文本,Postgresql不会使用索引。有了6个,它就成功了。为什么?

  •  1
  • Fred Hors  · 技术社区  · 5 年前

    我用的是Postgresql 13。

    通过此查询,PostgreSQL将使用以下索引:

    SELECT *
    FROM
        "players"
    WHERE team_id = 3
        AND (
        code ILIKE 'lushij'
        OR
        REPLACE(lastname||firstname,' ','') ILIKE '%lushij%'
        OR REPLACE(firstname||lastname,' ','') ILIKE '%lushij%'
        OR personal_info->>'houses' ILIKE '%lushij%'
        )
    LIMIT 15
    
    Limit  (cost=333.01..385.77 rows=15 width=360)
      ->  Bitmap Heap Scan on players  (cost=333.01..4061.29 rows=1060 width=360)
            Recheck Cond: ((code ~~* 'lushij'::text) OR (replace((lastname || firstname), ' '::text, ''::text) ~~* '%lushij%'::text) OR (replace((firstname || lastname), ' '::text, ''::text) ~~* '%lushij%'::text) OR ((personal_info ->> 'houses'::text) ~~* '%lushij%'::text))
            Filter: (team_id = 3)
            ->  BitmapOr  (cost=333.01..333.01 rows=1060 width=0)
                  ->  Bitmap Index Scan on players_code_trgm  (cost=0.00..116.75 rows=100 width=0)
                        Index Cond: (code ~~* 'lushij'::text)
                  ->  Bitmap Index Scan on players_replace_last_first_name_trgm  (cost=0.00..66.40 rows=320 width=0)
                        Index Cond: (replace((lastname || firstname), ' '::text, ''::text) ~~* '%lushij%'::text)
                  ->  Bitmap Index Scan on players_replace_first_last_name_trgm  (cost=0.00..66.40 rows=320 width=0)
                        Index Cond: (replace((firstname || lastname), ' '::text, ''::text) ~~* '%lushij%'::text)
                  ->  Bitmap Index Scan on players_personal_info_houses_trgm_idx  (cost=0.00..82.40 rows=320 width=0)
                        Index Cond: ((personal_info ->> 'houses'::text) ~~* '%lushij%'::text)
    

    使用相同的查询,但搜索文本少一个字符(从 lushij lushi ) 没有使用索引 :

    SELECT *
    FROM
        "players"
    WHERE team_id = 3
        AND (
        code ILIKE 'lushi'
        OR
        REPLACE(lastname||firstname,' ','') ILIKE '%lushi%'
        OR REPLACE(firstname||lastname,' ','') ILIKE '%lushi%'
        OR personal_info->>'houses' ILIKE '%lushi%'
        )
    LIMIT 15
    
    Limit  (cost=0.00..235.65 rows=15 width=360)
      ->  Seq Scan on players  (cost=0.00..76853.53 rows=4892 width=360)
            Filter: ((team_id = 3) AND ((code ~~* 'lushi'::text) OR (replace((lastname || firstname), ' '::text, ''::text) ~~* '%lushi%'::text) OR (replace((firstname || lastname), ' '::text, ''::text) ~~* '%lushi%'::text) OR ((personal_info ->> 'houses'::text) ~~* '%lushi%'::text)))
    

    为什么?

    使现代化 :

    如果我评论 LIMIT 15 使用索引行。


    以下是结构:

    玩家表结构
    -- ----------------------------
    -- Table structure for players
    -- ----------------------------
    DROP TABLE IF EXISTS "public"."players";
    CREATE TABLE "public"."players" (
      "id" int8 NOT NULL DEFAULT nextval('players_id_seq'::regclass),
      "created_at" timestamptz(6) NOT NULL DEFAULT now(),
      "updated_at" timestamptz(6),
      "team_id" int8 NOT NULL,
      "firstname" text COLLATE "pg_catalog"."default",
      "lastname" text COLLATE "pg_catalog"."default",
      "code" text COLLATE "pg_catalog"."default",
      "personal_info" jsonb
    )
    ;
    
    -- ----------------------------
    -- Indexes structure for table players
    -- ----------------------------
    CREATE INDEX "players_personal_info_houses_trgm_idx" ON "public"."players" USING gin (
      (personal_info ->> 'houses'::text) COLLATE "pg_catalog"."default" "public"."gin_trgm_ops"
    );
    CREATE INDEX "players_code_trgm" ON "public"."players" USING gin (
      "code" COLLATE "pg_catalog"."default" "public"."gin_trgm_ops"
    );
    CREATE INDEX "players_lower_code" ON "public"."players" USING btree (
      lower(code) COLLATE "pg_catalog"."default" "pg_catalog"."text_ops" ASC NULLS LAST
    );
    CREATE INDEX "players_replace_first_last_name_trgm" ON "public"."players" USING gin (
      replace(firstname || lastname, ' '::text, ''::text) COLLATE "pg_catalog"."default" "public"."gin_trgm_ops"
    );
    CREATE INDEX "players_replace_last_first_name_trgm" ON "public"."players" USING gin (
      replace(lastname || firstname, ' '::text, ''::text) COLLATE "pg_catalog"."default" "public"."gin_trgm_ops"
    );
    
    -- ----------------------------
    -- Primary Key structure for table players
    -- ----------------------------
    ALTER TABLE "public"."players" ADD CONSTRAINT "players_pkey" PRIMARY KEY ("id");
    
    -- ----------------------------
    -- Foreign Keys structure for table players
    -- ----------------------------
    ALTER TABLE "public"."players" ADD CONSTRAINT "players_team_id_fkey" FOREIGN KEY ("team_id") REFERENCES "public"."teams" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
    
    1 回复  |  直到 5 年前
        1
  •  1
  •   seanb    5 年前

    好啊这是基于我对SQL Server和SQL的总体了解,但它可能也适用于这里。

    一开始因为你在做一件事 SELECT * ,它将需要在某个时刻转到聚集索引。

    非聚集索引(如果使用的话)的用途是识别相关的行,然后它会逐个挑选这些行(a) 嵌套循环联接 ,或有时称为索引查找/扫描+键查找)。

    如果有太多的行,这实际上是低效的——你最终会进行更多的读取/等等,而不仅仅是读取整个表。

    减少LIKE过滤器的长度会增加基数估计,例如,增加过滤器在查询计划器/优化程序中预期匹配的行数。

    我猜SQL引擎会进行猜测(包括索引/数据的统计数据),并确定简单地从聚集索引中读取所有数据可能比确定行并逐个读取更有效。


    更新操作后更新重新删除限制。

    好同样,这取决于它基于过滤器估计的行数。

    想象一下,如果您在原始查询中执行类似于“%e%”的操作。每一排都可能与之匹配。由于您没有排序,它只需要读取(比如)聚集索引的前30行,就可以得到您的答案。再一次,查询规划者/优化者可能会得出结论,这将是获得这些信息的最有效方式。

    但是,如果没有限制,它将需要读取所有行以获得所有结果。

    • 对于%e%,只进行一次完整的聚集索引扫描可能更有效,因为它需要匹配多行
    • 对于更复杂/选择性更强的过滤,首先搜索索引(然后直接搜索聚集索引中的数据)通常更有效
        2
  •  1
  •   Laurenz Albe    5 年前

    字符串越短,条件的选择性就越低。根据它的估计,PostgreSQL认为,对于短字符串,足够多的行符合以下条件:只按顺序获取行并丢弃不匹配的行,直到找到15个匹配的行,这样成本更低。

    众多 OR 条件可能会使优化器低估选择性,因为条件被认为是不相关的,而事实可能并非如此。