sql >> Databasteknik >  >> RDS >> PostgreSQL

Varför saktar en liten förändring i söktermen ner frågan så mycket?

Varför?

orsaken är detta:

Snabb fråga:

->  Hash Left Join  (cost=1378.60..2467.48 rows=15 width=79) (actual time=41.759..85.037 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* (...)

Långsam fråga:

->  Hash Left Join  (cost=1378.60..2467.48 rows=1 width=79) (actual time=35.084..80.209 rows=1129 loops=1)
      ...
      Filter: (unaccent(((((COALESCE(p.abrev, ''::character varying))::text || ' ('::text) || (COALESCE(p.prenome, ''::character varying))::text) || ')'::text)) ~~* unacc (...)

Att utöka sökmönstret med en annan karaktär gör att Postgres antar ännu färre träffar. (Vanligtvis är detta en rimlig uppskattning.) Postgres har uppenbarligen inte tillräckligt exakt statistik (ingen, faktiskt, fortsätt läsa) för att förvänta dig samma antal träffar som du verkligen får.

Detta orsakar ett byte till en annan frågeplan, vilket är ännu mindre optimalt för den faktiska antal träffar rader=1129 .

Lösning

Antar nuvarande Postgres 9.5 eftersom den inte har deklarerats.

Ett sätt att förbättra situationen är att skapa ett uttrycksindex på uttrycket i predikatet. Detta gör att Postgres samlar statistik för det faktiska uttrycket, vilket kan hjälpa frågan även om själva indexet inte används för frågan . Utan indexet finns det ingen statistik för uttrycket överhuvudtaget. Och om det görs rätt kan indexet användas för frågan, det är ännu mycket bättre. Men det finns flera problem med ditt nuvarande uttryck:

unaccent(TEXT(coalesce(p.abrev,'')||' ('||coalesce(p.prenome,'')||')')) ilike unaccent('%vicen%' )

Överväg den här uppdaterade frågan, baserat på några antaganden om dina hemliga tabelldefinitioner:

SELECT e.id
     , (SELECT count(*) FROM imgitem
        WHERE tabid = e.id AND tab = 'esp') AS imgs -- count(*) is faster
     , e.ano, e.mes, e.dia
     , e.ano::text || to_char(e.mes2, 'FM"-"00')
                   || to_char(e.dia,  'FM"-"00') AS data    
     , pl.pltag, e.inpa, e.det, d.ano anodet
     , format('%s (%s)', p.abrev, p.prenome) AS determinador
     , d.tax
     , coalesce(v.val,v.valf)   || ' ' || vu.unit  AS altura
     , coalesce(v1.val,v1.valf) || ' ' || vu1.unit AS dap
     , d.fam, tf.nome família, d.gen, tg.nome AS gênero, d.sp
     , ts.nome AS espécie, d.inf, e.loc, l.nome localidade, e.lat, e.lon
FROM      pess    p                        -- reorder!
JOIN      det     d   ON d.detby   = p.id  -- INNER JOIN !
LEFT JOIN tax     tf  ON tf.oldfam = d.fam
LEFT JOIN tax     tg  ON tg.oldgen = d.gen
LEFT JOIN tax     ts  ON ts.oldsp  = d.sp
LEFT JOIN tax     ti  ON ti.oldinf = d.inf  -- unused, see @joop's comment
LEFT JOIN esp     e   ON e.det     = d.id
LEFT JOIN loc     l   ON l.id      = e.loc
LEFT JOIN var     v   ON v.esp     = e.id AND v.key  = 265
LEFT JOIN varunit vu  ON vu.id     = v.unit
LEFT JOIN var     v1  ON v1.esp    = e.id AND v1.key = 264
LEFT JOIN varunit vu1 ON vu1.id    = v1.unit
LEFT JOIN pl          ON pl.id     = e.pl
WHERE f_unaccent(p.abrev)   ILIKE f_unaccent('%' || 'vicenti' || '%') OR
      f_unaccent(p.prenome) ILIKE f_unaccent('%' || 'vicenti' || '%');

Huvudpunkter

Varför f_unaccent() ? Eftersom unaccent() kan inte indexeras. Läs detta:

Jag använde funktionen som beskrivs där för att tillåta följande (rekommenderas!) flerkolumns funktionella trigram GIN index :

CREATE INDEX pess_unaccent_nome_trgm_idx ON pess
USING gin (f_unaccent(pess) gin_trgm_ops, f_unaccent(prenome) gin_trgm_ops);

Om du inte är bekant med trigramindex, läs detta först:

Och möjligen:

Se till att köra den senaste versionen av Postgres (för närvarande 9.5). Det har skett avsevärda förbättringar av GIN-index. Och du kommer att vara intresserad av förbättringar i pg_trgm 1.2, planerad att släppas med den kommande Postgres 9.6:

Förberedda uttalanden är ett vanligt sätt att köra frågor med parametrar (särskilt med text från användarinmatning). Postgres måste hitta en plan som fungerar bäst för en given parameter. Lägg till jokertecken som konstanter till söktermen så här:

f_unaccent(p.abrev) ILIKE f_unaccent('%' || 'vicenti' || '%')

('vicenti' skulle ersättas med en parameter.) Så Postgres vet att vi har att göra med ett mönster som varken är förankrat till vänster eller höger - vilket skulle tillåta olika strategier. Relaterat svar med mer information:

Eller kanske omplanera frågan för varje sökterm (eventuellt med dynamisk SQL i en funktion). Men se till att planeringstiden inte äter någon möjlig prestationsvinst.

WHERE villkor på kolumner i pess motsäger LEFT JOIN . Postgres tvingas konvertera det till en INNER JOIN . Vad värre är att sammanfogningen kommer sent i sammanfogningsträdet. Och eftersom Postgres inte kan ordna om dina anslutningar (se nedan), kan det bli väldigt dyrt. Flytta tabellen till den första position i FRÅN klausul för att eliminera rader tidigt. Följer LEFT JOIN s eliminerar inte några rader per definition. Men med så många tabeller är det viktigt att flytta sammanfogningar som kan föröka sig rader till slutet.

Du går med i 13 bord, 12 av dem med LEFT JOIN vilket lämnar 12! möjliga kombinationer - eller 11! * 2! om vi tar den LEFT JOIN det är verkligen en INNER JOIN . Det är för många för Postgres att utvärdera alla möjliga permutationer för den bästa frågeplanen. Läs om join_collapse_limit :

Standardinställningen för join_collapse_limit är 8 , vilket innebär att Postgres inte kommer att försöka ändra ordning på tabeller i din FROM sats och tabellordningen är relevant .

Ett sätt att kringgå detta skulle vara att dela upp den prestandakritiska delen i en CTE gillar @joop kommenterade . Ställ inte in join_collapse_limit mycket högre eller tider för frågeplanering som involverar många sammanfogade tabeller kommer att försämras.

Om ditt sammansatta datum heter data :

cast(cast(e.ano som varchar(4))||'-'||right('0'||cast(e.mes som varchar(2)),2)||' -'|| right('0'||cast(e.dia som varchar(2)),2) som varchar(10)) som data

Förutsatt du bygger från tre numeriska kolumner för år, månad och dag, vilka är definierade NOT NULL , använd det här istället:

e.ano::text || to_char(e.mes2, 'FM"-"00')
            || to_char(e.dia,  'FM"-"00') AS data

Om FM mallmönstermodifierare:

Men egentligen borde du lagra datumet som datatyp datum till att börja med.

Även förenklat:

format('%s (%s)', p.abrev, p.prenome) AS determinador

Kommer inte att göra frågan snabbare, men den är mycket renare. Se format() .

Till sist, alla vanliga råd för prestandaoptimering gäller:

Om du får allt detta rätt bör du se mycket snabbare frågor för alla mönster.



  1. 2018 i recension:7 MariaDB-milstolpar du kanske har missat

  2. MySQL-frågeordning efter flera objekt

  3. Har Oracle en motsvarighet till SQL Servers tabellvariabler?

  4. PHP - hämta data från databasen jämför den med variabel och sortera efter mindre värde