SQL Triky III.

Z PostgreSQL
Skočit na navigaci Skočit na vyhledávání

Starších dvacet triků Novějších dvacet triků

Pomoc při migraci z TSearch2 na vestavěný fulltext

Ve verzi 8.3 došlo k integraci modulu TSearch2 - bohužel integrovaný fulltext není plně kompatibilní s starším modulem, což působí problémy už při migraci databáze. Pro zjednodušení migraci vývojáři přepsali modul TSearch2 tak, aby maskoval rozdíly mezi integrovaným fulltextem a rozšiřujícím modulem. Modul se musí registrovat do databáze ještě před vlastním načtením dumpu - to v případě importu jednotlivých databází není problém. Problémy nastanou v případě, že dumpujete všechny db naráz. Dump se totiž odkazuje nikoliv na template1, kam můžete TSearch2 předinstalovat, ale na template0, s kterou nelze nijak pracovat. Pokud se setkáte s chybou:

ERROR:  type "tsvector" already exists

musíte poeditovat dump (pomůže rozdělit dump na strukturu a data), a přidat za vytvořením databáze import registračního souboru TSearch2.sql:

DROP DATABASE brigady;
CREATE DATABASE brigady WITH TEMPLATE = template0  ENCODING = 'UTF8';
--
-- PostgreSQL database dump complete
--

\connect brigady
\i /usr/local/pgsql/share/contrib/tsearch2.sql

SET search_path = public, pg_catalog;
...

Nepodceňujte SQL funkce

Snad jediné pomalé operace, které jsou v PL/pgSQL je změna pole a změna řetězce. V obou případech se totiž nemění pouze jeden prvek (jeden znak), ale generuje se nové pole nebo nový řetězec. Pro malé objekty to není nijak zásadní režie, ale pro velké objekty, a velký počet změn, již může být tato režie znatelná. A proto je dobré se ji vyhnout. V následující ukázce porovnávám rychlost řazení v SQL a v PL/pgSQL (metoda quick sort):

CREATE OR REPLACE FUNCTION quicksort(l integer, r integer, a int[])
RETURNS int[] AS $$
DECLARE akt int[] = a;
  i integer := l; j integer := r; x integer = akt[(l+r) / 2]; 
  w integer;
BEGIN
  LOOP
    WHILE akt[i] < x LOOP i := i + 1; END LOOP;
    WHILE x < akt[j] loop j := j - 1; END LOOP;
    IF i <= j THEN
      w := akt[i];
      akt[i] := akt[j]; akt[j] := w;
      i := i + 1; j := j - 1;
    END IF;
    EXIT WHEN i > j;
  END LOOP;
  IF l < j THEN akt := quicksort(l,j,akt); END IF;
  IF i < r then akt := quicksort(i,r,akt); END IF;
  RETURN akt;
END;
$$ LANGUAGE plpgsql STRICT IMMUTABLE;

postgres=# SELECT array_upper(quicksort(1,10000,array_agg(a)),1) FROM test;
 array_upper
-------------
       10000
(1 row)
Time: 5918,531 ms

CREATE OR REPLACE FUNCTION sort(anyarray) 
RETURNS anyarray AS $$
  SELECT array(SELECT * FROM unnest($1) ORDER BY 1); 
$$ language sql;

postgres=# SELECT array_upper(sort(array_agg(a)),1) FROM test;
 array_upper
-------------
       10000
(1 row)
Time: 35,980 ms

Na výsledku není nic překvapivého - řazení v SQL je implementováno přímo v C, podstatně efektivněji než kdy může být implementováno v PL/pgSQL (v obou případech je ovšem použit quicksort). Pro zajímavost Bublesort během 5min nedoběhl (pro 1000 prvků SQL 7ms, Quick 150ms, Buble 7000ms). Je důležité si stále připomínat, že jednoduchý kód může být velice rychlý, zvlášť pokud je nativně implementován.

Přístup ke změněným datům z statement triggerů

V případě statement triggerů PostgreSQL nenabízí přímo žádnou podporu pro identifikaci řádků, které modifikoval SQL příkaz zodpovědný za aktivaci triggeru. Gurjeet Singh publikoval způsob, jak se v triggeru dotázat na nové hodnoty změněné v aktuální transakci:

CREATE TABLE foo(a integer);
INSERT INTO foo VALUES(0),(0);

CREATE OR REPLACE FUNCTION trghdx() 
RETURNS trigger as $$ 
DECLARE r record; 
BEGIN 
  FOR r IN 
     SELECT * FROM foo 
       WHERE xmin::text::bigint = txid_current() 
  LOOP
    RAISE NOTICE '%', r.a; 
  END LOOP; 
  RETURN NULL; 
END
$$ LANGUAGE plpgsql;

CREATE TRIGGER trg AFTER INSERT OR UPDATE 
   ON foo FOR EACH STATEMENT EXECUTE PROCEDURE trghdx();

postgres=# INSERT INTO FOO VALUES(10),(10);
NOTICE:  10
NOTICE:  10
INSERT 0 2
postgres=# UPDATE foo SET a = 5 WHERE a = 10;
NOTICE:  5
NOTICE:  5
UPDATE 2

Pozor! V určitých okrajových případech tato metoda není funkční - v každém případě se jedná o nehezký hack.

Rychlý posun indexů na počátek v nule

Tato funkce by se dala ideálně vyřešit v C, kdy stačí nastavit binárně jednu hodnotu ve struktuře popisující pole. PL/pgSQL tuto možnost nenabízí. Iterace po prvku je pomalá. Lze ale využít schopnost SQL parseru rozpoznat rozsah indexů pole:

CREATE OR REPLACE FUNCTION shift_idx(anyarray) 
RETURNS anyarray AS $$
DECLARE 
  mi int := array_upper($1,1); 
  offset int := array_lower($1,1); 
BEGIN
  RETURN '[0:' || mi - offset || '] = ' || $1::varchar; 
END 
$$ LANGUAGE plpgsql IMMUTABLE STRICT;
CREATE FUNCTION

postgres=# SELECT shift_idx(array[1,2,3]);
   shift_idx   
---------------
 [0:2]={1,2,3}
(1 row)

Autor: Pavel Stěhule

Rozvinutí hodnot typu record

V případě, že používáme funkce, které vrací record a nemůžeme je umístit do klauzule FROM, musíme použít následující syntax pro rozvinutí hodnoty typu record ve výsledné tabulce:

CREATE FUNCTION foo(OUT a int, OUT b int, IN c int)
RETURNS record AS $$
BEGIN
  a := c + 1; b := c + 2;
  RETURN;
END;
$$ LANGUAGE plpgsql;

CREATE TABLE t(c int);
INSERT INTO t VALUES(10),(20);

postgres=# SELECT c, foo(c) FROM t;
 c  |   foo   
----+---------
 10 | (11,12)
 20 | (21,22)
(2 rows)

postgres=# SELECT c, (foo(c)).* FROM t;
 c  | a  | b  
----+----+----
 10 | 11 | 12
 20 | 21 | 22
(2 rows)

Konverze mezi hexadecimální a dekadickými čísly

Dekadické číslo do hexadecimálního tvaru převedeme velice snadno - PostgreSQL nabízí funkci to_hex. O něco obtížnější je převod v opačném směru - z hexadecimální do dekadické soustavy. PostgreSQL má podporu pro tuto transformaci na úrovni parseru (tj. snadno lze transformovat konstanty), nenabízí ovšem žádnou funkci. Tu si ovšem můžeme poměrně snadno napsat:

CREATE OR REPLACE FUNCTION to_dec(text) 
RETURNS integer AS $$
DECLARE r int; 
BEGIN 
  EXECUTE 'SELECT x'||quote_literal($1)|| '::integer' INTO r; 
  RETURN r; 
END
$$ LANGUAGE plpgsql IMMUTABLE STRICT;

postgres=# select to_dec('ff');
 to_dec 
--------
    255
(1 row)

Ještě rychlejší kód bude s použitím jazyka SQL (Michael Glaesemann):

CREATE FUNCTION hex2dec(in_hex TEXT) 
RETURNS INT 
IMMUTABLE STRICT LANGUAGE sql AS $body$
SELECT CAST(CAST(('x' || CAST($1 AS text)) AS bit(8)) AS INT);
$body$;

Získání defaultních hodnot sloupců tabulky

Jedním z méně častých požadavků je získání výchozích hodnot atributů záznamu. Definice tabulky včetně výchozích hodnot je uložena v systémových tabulek, takže není problém se k těmto hodnotám dostat - jen je s nimi nutné nakládat opatrně - zvlášť s hodnotamy typu timestamp, které se používají k zachycení okamžiku skutečného přidání záznamu do tabulky a nikoliv okamžiku zobrazení formuláře:

CREATE OR REPLACE FUNCTION eval(varchar) 
RETURNS varchar AS $$
DECLARE result varchar;
BEGIN
 EXECUTE 'SELECT ' || $1 INTO result;
 RETURN result;
END;$$ LANGUAGE plpgsql STRICT;

CREATE OR REPLACE FUNCTION defaults(text, 
                                    OUT attname name, OUT type varchar, OUT default_val varchar)
RETURNS SETOF RECORD AS $$
SELECT a.attname,
       pg_catalog.format_type(a.atttypid, a.atttypmod),
       (SELECT eval(pg_catalog.pg_get_expr(d.adbin, d.adrelid))
           FROM pg_catalog.pg_attrdef d
          WHERE d.adrelid = a.attrelid AND d.adnum = a.attnum AND a.atthasdef)
   FROM pg_catalog.pg_attribute a
  WHERE a.attrelid = $1::regclass::oid AND a.attnum > 0 AND NOT a.attisdropped
  ORDER BY a.attnum
$$ LANGUAGE SQL STRICT;

Číslování řádků po x-té

Opět inspirace z blogu Rolanda Baumana. Při číslování lze použít tzv. session variables (globální proměnné). Ty zatím nejsou implementovány a jejich neexistence se obchází několika způsoby. Takže prvním problémem je jejich implementace, druhým umístění inicializační části. Až v Rolandově blogu jsem se seznámil s trikem, kdy se proměnné inicializují CROSS JOIN poddotazem. Jelikož tento dotaz vrací vždy jeden řádek, není ovlivněn ani výsledek dotazu a ani náročnost dotazu:

create or replace function set_var(varchar, int) 
returns int as $$
select set_config($1, $2::text, false)::int
$$ language sql;

create or replace function add_var(varchar, int) 
returns int as $$
select set_config($1, (current_setting($1)::int + $2)::text, false)::int
$$ language sql;

postgres=# select add_var('myvar.i', 1) as num, * 
              from (select length, title, description 
                       from film 
                            cross join 
                            (select set_var('myvar.i', 0)) p --< Inicializace!!!!
                      order by length ) p 
             offset 100 
             limit 10;

 num | length |         title         |                                                    description                                                    
-----+--------+-----------------------+-------------------------------------------------------------------------------------------------------------------
 101 |     60 | PITY BOUND            | A Boring Panorama of a Feminist And a Moose who must Defeat a Database Administrator in Nigeria
 102 |     60 | ROOM ROMAN            | A Awe-Inspiring Panorama of a Composer And a Secret Agent who must Sink a Composer in A Shark Tank
 103 |     60 | SHAKESPEARE SADDLE    | A Fast-Paced Panorama of a Lumberjack And a Database Administrator who must Defeat a Madman in A MySQL Convention
 104 |     60 | SMILE EARRING         | A Intrepid Drama of a Teacher And a Butler who must Build a Pastry Chef in Berlin
 105 |     61 | BIRDS PERDITION       | A Boring Story of a Womanizer And a Pioneer who must Face a Dog in California
 106 |     61 | BULWORTH COMMANDMENTS | A Amazing Display of a Mad Cow And a Pioneer who must Redeem a Sumo Wrestler in The Outback
 107 |     61 | CAMELOT VACATION      | A Touching Character Study of a Woman And a Waitress who must Battle a Pastry Chef in A MySQL Convention
 108 |     61 | CASSIDY WYOMING       | A Intrepid Drama of a Frisbee And a Hunter who must Kill a Secret Agent in New Orleans
 109 |     61 | CHINATOWN GLADIATOR   | A Brilliant Panorama of a Technical Writer And a Lumberjack who must Escape a Butler in Ancient India
 110 |     61 | DRIFTER COMMANDMENTS  | A Epic Reflection of a Womanizer And a Squirrel who must Discover a Husband in A Jet Boat
(10 rows)

Prefix myvar musí být zaregistrován v postgresql.conf.

Výpočet percentilů a mediánu

Určení percentilů a mediánu založeném na funkcích SUBSTRING_INDEX a GROUP_CONCAT je myšlenka Rolanda Boumana. Tato metoda je určitě rychlejší než klasické řešení založené na spojení nebo poddotazů a o něco horší než při použití analytických funkcí. Ovšem MySQL, stejně tak PostgreSQL nemají (červenec 2008) analytické funkce, takže jde o jedno z nejlepších dostupných řešení. PostgreSQL nemá funkci GROUP_CONCAT. Sic by bylo možné použít funkci array_accum, v té ale nemáme možnost určit pořadí. Na druhou stranu v PostgreSQL máme pole, které lze s výhodou využít - neobejdeme se ale bez zákaznických funkcí:

create or replace function nth_percentil(anyarray, int) 
returns anyelement as $$
  select $1[$2/100.0 * array_upper($1,1) + 1];
$$ language sql immutable strict;

pagila=# select nth_percentil(array(select length from film order by 1),90);
 nth_percentil 
---------------
           173
(1 row)

-- ověření
pagila=# select count(case when length < 173 then 1 end)::float / count(*) * 100.0 
            from film;
 ?column? 
----------
     89.6
(1 row)

Podobný postup lze uplatnit i pro výpočet mediánu:

create or replace function median(anyarray) 
returns float as $$
  select ($1[array_upper($1,1)/2+1]::double precision + $1[(array_upper($1,1)+1) / 2]::double precision) / 2.0; 
$$ language sql immutable strict;

pagila=# select median(array[1,2]), median(array[1,2,3]), median(array[1,2,3,4]);
 median | median | median 
--------+--------+--------
    1.5 |      2 |    2.5
(1 row)

pagila=# select median(array(select length from film order by 1));
 median 
--------
    114
(1 row)

Správa indexů

Na svém blogu zveřejnil Robert Treat několik užitečných dotazů,které mohou pomoci při správě indexů:

-- nepoužité indexy
select indexrelid::regclass as index, relid::regclass as table
   from pg_stat_user_indexes
        JOIN 
        pg_index USING (indexrelid)
  where idx_scan = 0 and indisunique is false;

-- duplicitní indexy, array_accum z http://www.postgresql.org/docs/8.3/static/xaggr.html
-- POZOR, NEBERE V POTAZ ROZDÍLY MEZI PODMÍNĚNÝMI INDEXY, NEMAZAT ZBRKLE!
select indrelid::regclass, array_accum(indexrelid::regclass)
   from pg_index
  group by indrelid, indkey
  having count(*) > 1;

-- zbytečné indexy
select starelid::regclass, indexrelid::regclass, array_accum(staattnum), relpages, reltuples, array_accum(stadistinct)
   from pg_index
        join 
        pg_statistic 
        on starelid=indrelid and staattnum = ANY(indkey)
        join 
        pg_class 
        on indexrelid=oid
  where case 
            when stadistinct < 0 then stadistinct > -.8 
            else reltuples/stadistinct > .2 end
    and not (indisunique or indisprimary)
    and (relpages > 100 or reltuples > 1000)
  group by starelid, indexrelid, relpages, reltuples
  order by starelid ;

Před zrušením indexu je dobré se zamyslet - všechny tyto dotazy použijte pro orientaci.

Zrcadlové otočení pole

aneb ukázka nové funkce generate_subscripts (PostgreSQL 8.4):

postgres=# create or replace function array_reverse(anyarray) 
           returns anyarray as $$
             select array(
                          select $1[i] 
                             from generate_subscripts($1,1,true) g(i))
           $$ language sql immutable strict;
CREATE FUNCTION
postgres=# select array_reverse(array[10,20,30]);
 array_reverse 
---------------
 {30,20,10}
(1 row)

Výpočet rozdílu po sobě jdoucích hodnot pomocí korelovaného poddotazu a klauzule LIMIT

Jednou z možností, jak získat rozdíl z po sobě jdoucích hodnot je použití následujícího korelovaného dotazu. Tento způsob výpočtu je vhodný pouze pro malou výslednou množinu (do sta). Pro větší množiny je výhodnější použití SELF JOINu nebo uložené procedury generující tabulku (viz Korelované vnořené dotazy)- Autor: Gurjeet Singh:

       ts          | size
-------------------+-----
 2002-03-16        | 11 
 2002-03-17        | 16 
 2002-03-18        | 18
 2002-03-19        | 12 

select ts, size,
       t1.size - (select t2.size
                     from foo.view as t2
                    where t2.ts < t1.ts
                    order by ts desc
                    limit 1) as diff
   from foo.view as t1
  order by ts asc;

Překlad modulů na platformě Win32

PostgreSQL je primárně vyvíjen na platformě Linux s překladačem gcc. I to je jeden z důvodů, že vývoj rozšiřujících modulů je komplikovanější na platformě win32 než v systémech typu UNIX. Starší verze jsou přeložené v systému MinGW. Verze 8.3 již může být přeložená v prostředí Microsoft Visual Studio. Základ v úspěchu je ve správném linkování. Pokud je PostgreSQL slinkován s knihovnou MSCVR80.DLL, pak i rozšiřující moduly musí být linkovány proti této knihovně. Dalším problémem je správné určení atributů funkce. Doporučuje se použít makro:

#if defined(_MSC_VER) || defined(__MINGW32__)
#define COPYTEXT_EXPORT __declspec (dllexport)
#else
#define COPYTEXT_EXPORT
#endif

COPYTEXT_EXPORT Datum copytext2(PG_FUNCTION_ARGS) {
   // blah blah
}

Použití domény a plperlu pro validaci emailové adresy

Emailovou adresu lze v PostgreSQL kontrolovat poměrně jednoduše pomocí regulárních výrazů. Komplexnější kontrolu umožňuje knihovna Email::Valid. Opět příklad je ukázkou obsáhlosti archivu CPAN. Požití domény je ovšem v tomto případě diskutabilní. V průběhu let se mění mx záznamy - uložené emailové adresy se mohou stát nevalidní (což je na jednu stranu správné, na druhou stranu toto chování může způsobit více problémů než užitku). Osobně bych doporučil používat funkci check_email explicitně, než implicitně. Automatické volání může jednak opravdu značně zpomalit import (kontroluje se validita mx záznamů) a obnovu ze zálohy, dále pak veškeré operace s proměnnými tohoto typu.

CREATE OR REPLACE FUNCTION check_email(varchar)
RETURNS boolean AS $$
use strict;
use Email::Valid;
my $address = $_[0];
my $checks = {
   -address => $address,
   -mxcheck => 1,
   -tldcheck => 1,
   -rfc822 => 1,
};
if (defined Email::Valid->address( %$checks )) {
    return 'true'
}
elog(WARNING, "address failed $Email::Valid::Details check.");
return 'false';
$$ LANGUAGE plperlu IMMUTABLE STRICT;

postgres=# CREATE DOMAIN email AS varchar CHECK(check_email(value));
CREATE DOMAIN
postgres=# SELECT 'pavel@'::email;
WARNING:  address failed rfc822 check.
postgres=# select 'stehule@kix.fsv.cvut.cz'::email;
          email
-------------------------
 stehule@kix.fsv.cvut.cz
(1 row)

Autor: David Fetter

Akcelerace výběru intervalů splňujících podmínku starti <= x <= endi

Dotazy pro výběr intervalu (interval je určen hodnotami ve sloupcích starti and endi) jsou ve větších tabulkách pomalé. Jádro pudla je v závislosti mezi sloupci starti a endi a dost často i mezi řádky. Řešením je použití prostorových indexů:

postgres=# EXPLAIN ANALYZE SELECT * FROM testip WHERE 19999999 BETWEEN startip AND endip;
                           QUERY PLAN
----------------------------------------------------------------
 Seq Scan on testip  (cost=0.00..19902.00 rows=200814 width=12) (actual time=3.457..434.218 rows=1 loops=1)
   Filter: ((19999999 >= startip) AND (19999999 <= endip))
 Total runtime: 434.299 ms
(3 rows)

Time: 435,865 ms

postgres=# CREATE INDEX ggg ON testip USING gist ((box(point(startip,startip),point(endip,endip))) box_ops);
CREATE INDEX
Time: 75530,079 ms
postgres=# EXPLAIN ANALYZE 
              SELECT * 
                 FROM testip 
                WHERE box(point(startip,startip),point(endip,endip)) @> box(point (19999999,19999999), point(19999999,19999999));
                                                                                                QUERY PLAN                                                            
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on testip  (cost=60.50..2550.14 rows=1000 width=12) (actual time=0.169..0.172 rows=1 loops=1)
   Recheck Cond: (box(point((startip)::double precision, (startip)::double precision), point((endip)::double precision, (endip)::double precision)) @> '(19999999,19999999),(19999999,19999999)'::box)
   ->  Bitmap Index Scan on ggg  (cost=0.00..60.25 rows=1000 width=0) (actual time=0.152..0.152 rows=1 loops=1)
         Index Cond: (box(point((startip)::double precision, (startip)::double precision), point((endip)::double precision, (endip)::double precision)) @> '(19999999,19999999),(19999999,19999999)'::box)
 Total runtime: 0.285 ms
(5 rows)

Time: 2,805 ms

Více na Indexace intervalů - období příp. rozsah ip pomocí prostorových indexů.

Zkrácený zápis pro vložení obsahu proměnných typu row do tabulky

postgres=# CREATE TABLE foo(a integer, b integer);
CREATE TABLE
postgres=# CREATE OR REPLACE FUNCTION fx() 
           RETURNS void as $$
             DECLARE r foo; 
           BEGIN 
             SELECT INTO r * FROM foo; 
             INSERT INTO foo VALUES(r.*); 
             RETURN; 
           END; 
           $$ LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# SELECT fx();
 fx 
----
 
(1 row)

ALTER TABLE ALTER COLUMN USING

Přiznám se, že klauzuli USING v ALTER COLUMN jsem neznal. Žil jsem v domnění, že při změně typu musí být hodnota přetypovatelná na noý typ. Což může být problém např. pro česky zapsané logické konstanty (tj. ano, ne). Pokud bych stávající hodnoty nepřevedl pouhým přetypováním, mohu použít klauzuli USING:

postgres=# CREATE TABLE foo(a varchar);
CREATE TABLE
postgres=# INSERT INTO foo VALUES ('ano');
INSERT 0 1
postgres=# ALTER TABLE foo ALTER COLUMN a TYPE boolean ;
ERROR:  column "a" cannot be cast to type "pg_catalog.bool"
postgres=# ALTER TABLE foo 
              ALTER COLUMN a TYPE boolean 
             USING CASE a 
                       WHEN 'ano' THEN true 
                       ELSE false END;
ALTER TABLE
postgres=# SELECT * FROM foo;
 a 
---
 t
(1 row)

Quote_ident pro dvojici schéma.název

Vložení identifikátorů mezi uvozovky je jedním ze způsobů ochrany proti SQL injektáži. Funkce qoute_ident projede zadaný řetězec. Pokud zjistí, že řetězec obsahuje nějaký potenciálně nebezpečný řetězec, tak tento řetězec zapouzdří bezpečně do uvozovek, jinak ponechá vstupní řetězec beze změn. Problém nastává ve chvíli, kdy je parametrem kvalifikovaný identifikátor, protože:

postgres=# select quote_ident('public.foo');
 quote_ident  
--------------
 "public.foo"
(1 row)

postgres=# select * from "public.foo";
ERROR:  relation "public.foo" does not exist
postgres=# select * from public."foo";
 a 
---
(0 rows)

postgres=# select * from "public"."foo";
 a 
---
(0 rows)

Toto omezení funkce quote_ident, lze celkem snadno obejít vlastní funkcí:

CREATE OR REPLACE FUNCTION quote_array(text[]) 
RETURNS text AS $$
SELECT array_to_string(array(SELECT quote_ident($1[i]) 
                                FROM generate_series(1, array_upper($1,1)) g(i)),
                       '.') 
$$ LANGUAGE SQL IMMUTABLE;

CREATE OR REPLACE FUNCTION quote_schema_ident(text) 
RETURNS text AS $$
SELECT quote_array(string_to_array($1,'.'))
$$ LANGUAGE SQL IMMUTABLE;

postgres=# select quote_schema_ident('public.foo tab');
 quote_schema_ident 
--------------------
 public."foo tab"
(1 row)

Autor: Pavel Stěhule.

Rychlé dohledání nejdelšího prefixu

Dohledání nejdelšího prefixu z množiny známých prefixů je poměrně častá úloha. Typicky v oblasti komunikací. Na základě shody prefixu se obvykle určuje tarifní pásmo. Tuto úlohu jsem před několika lety řešil uloženou procedurou a modifikovaným algoritmem pro vyhledávání metodou půlení intervalu. Možná o něco pomalejší, nicméně o dost jednodušší je technika generování prefixů ze zadaného řetězce a přímé dohledání těchto prefixů (autor Hubert Lubaciewski - Depesz).

--tabulka obsahující známé prefixy cca 5000
postgres=# select * from prefixesx limit 5;
   costcode_name    | costcode 
--------------------+----------
 Afghanistan        | 93
 Afghanistan Mobile | 9370
 Afghanistan Mobile | 9379
 Afghanistan Mobile | 9380
 Alaska (USA)       | 1907
(5 rows)

Nejjednodušší a nejhorší řešení je použití LIKE:

postgres=# SELECT * 
              FROM prefixesx 
             WHERE '420724181000' LIKE costcode || '%' 
             ORDER BY length(costcode) DESC 
             LIMIT 1;
          costcode_name          | costcode 
---------------------------------+----------
 Czech Republic Mobile - EuroTel | 42072

Další možností je vygenerovat z každého čísla pole prefixů a tyto prefixy přímo dohledávat:

CREATE OR REPLACE FUNCTION prefixes(varchar) 
RETURNS varchar[] AS $$
SELECT ARRAY(SELECT substring($1 FROM 1 FOR i) 
                FROM generate_series(1, length($1)) g(i))::varchar[]; 
$$ LANGUAGE sql IMMUTABLE;

a použít dotaz:

postgres=# SELECT * 
              FROM prefixesx 
             WHERE costcode = ANY (prefixes('420724191000')) 
             ORDER BY length(costcode) DESC 
             LIMIT 1;
          costcode_name          | costcode 
---------------------------------+----------
 Czech Republic Mobile - EuroTel | 42072

Na zhruba 5000 řádcích, které představují aktuální ceník, je tento dotaz zhruba 4x rychlejší nežli LIKE (který se bude s rostoucím počtem prefixů zpomalovat).

O něco pomalejší (cca 1ms) je dotaz vynucující si index s LIKE:

SELECT * 
   FROM prefixesx
  WHERE '420724181000' LIKE costcode || '%'
    AND costcode LIKE substring('420724191000' FROM 1 FOR 1) || '%'  
  ORDER BY length(costcode) DESC 
  LIMIT 1;

Zjištění počtu výskytu subřetězce v řetězci

Následující funkce je ukázkou docela již zlidovělého triku pro operace s řetězci a to použítí funkcí replace a length, přičemž vlastní úlohu řešíme bez cyklu - to je typické pro interprety, kde impementace vestavěných funkcí je rychlejší než iterace:

CREATE OR REPLACE FUNCTION CountInString(text,text)
RETURNS integer AS $$
 SELECT(Length($1) - Length(REPLACE($1, $2, ''))) / Length($2) ;
$$ LANGUAGE SQL IMMUTABLE;

Tuto funkci zaslal Rodrigo E. De León Plicet. Napadlo mne ještě další řešení a to s využítím polí:

CREATE OR REPLACE FUNCTION CountInString(text, text) 
RETURNS integer AS $$
  SELECT Array_upper(String_to_array($1,$2),1) - 1;
$$ LANGUAGE SQL IMMUTABLE;

Starších dvacet triků Novějších dvacet triků