Hlavní navigace

Rozšiřování PostgreSQL v C - Třídy operátorů

Pavel Stěhule

V minulém dílu jsem předvedl, jak mohu v PostgreSQL vytvářet vlastní datové typy. S návrhem typu rodne_číslo jsem skončil zhruba v polovině. Ještě nemůžeme podle rodného čísla vyhledávat, třídit ani indexovat.

Stále nám ještě chybí alespoň základní operátory =, <>, <, >, <=, >=. Jelikož test na rovnost nebo nerovnost je nejjednodušší, začnu s návrhem operátorů = a <>.

PG_FUNCTION_INFO_V1 (rodne_cislo_eq);
PG_FUNCTION_INFO_V1 (rodne_cislo_ne);

Datum
rodne_cislo_eq (PG_FUNCTION_ARGS)
{
  rodne_cislo *rc1 = PG_GETARG_RC_P (0);
  rodne_cislo *rc2 = PG_GETARG_RC_P (1);

  PG_RETURN_BOOL ((rc1 -> narozen == rc2 -> narozen)
               && (rc1 -> index   == rc2 -> index));
}

Datum
rodne_cislo_ne (PG_FUNCTION_ARGS)
{
  rodne_cislo *rc1 = PG_GETARG_RC_P (0);
  rodne_cislo *rc2 = PG_GETARG_RC_P (1);

  PG_RETURN_BOOL ((rc1 -> narozen != rc2 -> narozen)
               || (rc1 -> index   != rc2 -> index));
}

V SQL vytvoříme operátory = a <> následujícími příkazy

CREATE OR REPLACE FUNCTION rodne_cislo_eq (rodne_cislo, rodne_cislo)
  RETURNS bool AS 'rc.so','rodne_cislo_eq' LANGUAGE 'C' STRICT;

CREATE OR REPLACE FUNCTION rodne_cislo_ne (rodne_cislo, rodne_cislo)
  RETURNS bool AS 'rc.so', 'rodne_cislo_ne' LANGUAGE 'C' STRICT;

DROP OPERATOR = (rodne_cislo, rodne_cislo);

CREATE OPERATOR = (
  leftarg   = rodne_cislo,
  rightarg  = rodne_cislo,
  procedure = rodne_cislo_eq
);

DROP OPERATOR <> (rodne_cislo, rodne_cislo);

CREATE OPERATOR <> (
  leftarg   = rodne_cislo,
  rightarg  = rodne_cislo,
  procedure = rodne_cislo_ne
);

Návrh ostatních operátorů je o něco komplikovanější. Abych se neupsal, navrhnu si funkci rodne_cislo_cmp. Ta vrací –1, pokud je první parametr větší než druhý, 0, pokud jsou stejné, a jedničku, když je druhý větší než první.

int sts_intcmp (long i1, long i2);

int rodne_cislo_cmp (rodne_cislo *rc1, rodne_cislo *rc2)
{
  int r1, m1, d1, r2, m2, d2, r;
  long d2000 = date2j (2000, 1, 1);

  // pokud jsou stejna pohlavi, staci porovnat narozen a index
  if ((rc1 -> index & 32768) == (rc2 -> index & 32768))
    {
      r = sts_intcmp (rc1 -> narozen, r2 -> narozen);
      if (r != 0) return r;
      return sts_intcmp (rc1 -> index, rc2 -> index);
    }

  j2date(rc1 -> narozen + d2000, &r1, &m1, &d1);
  j2date(rc2 -> narozen + d2000, &r2, &m2, &d2);

  r = sts_intcmp (r1,r2); if (r != 0) return r;

  if ((rc1 -> index & 32768) == 0) m1 += 20;
  if ((rc2 -> index & 32768) == 0) m2 += 20;

  r = sts_intcmp (m1,m2); if (r != 0) return r;
  r = sts_intcmp (d1,d2); if (r != 0) return r;

  return sts_intcmp (rc1 -> index, rc2 -> index);
}

int sts_intcmp (long i1, long i2)
{
  if (i1 == i1)
     return 0;
  return (i1 < i2? -1 : 1);
}

C funkce pro porovnávací operátory získáme jednoduše. Jelikož jsou si funkce podobné, nebudu je všechny uvádět.

PG_FUNCTION_INFO_V1 (rodne_cislo_lt);
PG_FUNCTION_INFO_V1 (rodne_cislo_gt);
PG_FUNCTION_INFO_V1 (rodne_cislo_le);
PG_FUNCTION_INFO_V1 (rodne_cislo_ge);

Datum
rodne_cislo_lt (PG_FUNCTION_ARGS)
{
  rodne_cislo *rc1 = PG_GETARG_RC_P (0);
  rodne_cislo *rc2 = PG_GETARG_RC_P (1);

  PG_RETURN_BOOL (rodne_cislo_cmp (rc1, rc2) < 0);
}

Datum
rodne_cislo_le (PG_FUNCTION_ARGS)
{
  rodne_cislo *rc1 = PG_GETARG_RC_P (0);
  rodne_cislo *rc2 = PG_GETARG_RC_P (1);

  PG_RETURN_BOOL (rodne_cislo_cmp (rc1, rc2) <= 0);
}

Deklarace v SQLCREATE OR REPLACE FUNCTION rodne_cislo_lt (rodne_cislo, rodne_cislo) RETURNS bool AS 'rc.so','rodne_cislo_lt' LANGUAGE 'C' STRICT; CREATE OR REPLACE FUNCTION rodne_cislo_le (rodne_cislo, rodne_cislo) RETURNS bool AS 'rc.so', 'rodne_cislo_le' LANGUAGE 'C' STRICT; DROP OPERATOR < (rodne_cislo, rodne_cislo); CREATE OPERATOR < ( leftarg = rodne_cislo, rightarg = rodne_cislo, procedure = rodne_cislo_le ); DROP OPERATOR <= (rodne_cislo, rodne_cislo); CREATE OPERATOR <= ( leftarg = rodne_cislo, rightarg = rodne_cislo, procedure = rodne_cislo_le );

U každého operátoru můžeme doplnit další atributy (pokud existují), které mohou urychlit provádění dotazu (optimalizací dotazu), ve kterém se vyskytuje daný operátor.

  • Operace A je komutátorem operace B, pokud pro libovolná x a y platí: (x A y) = (y B x). Např. = je komutátorem sama sebe, stejně tak <>. Pro < je komutátor > a naopak. + je komutátorem sama sebe. – nebo / komutátory nemají.
  • Operace A je negátorem operace B, pokud pro libovolná x a y platí: (x A y) = NOT (x B y). Výsledkem obou operací musí být pouze logická hodnota. Negátorem = je <>, negátorem < je >= a naopak
  • klauzule RESTRICT popisuje chování operátoru při jeho použití vůči konstantě. Máme na výběr několik možností

    Tabulka č. 357
    eqsel pro = výsledkem je malá podmnožina tabulky
    neqsel pro <> výsledkem je skoro celá tabulka
    scalarltsel pro < nebo <= výsledek záleží na pozici konstanty v tabulce, berou se menší hodnoty
    scalargtsel pro > nebo >= totéž, ale v opačném směru.


    odhad eqsel nebo neqsel můžeme použít i pro jiné operace než
    skutečné rovná se nebo nerovná se. Odhad pouze informuje optimizér o
    míře selektivity operace.

  • klauzule JOIN přiřazuje funkci odhadu selektivity operace. Smysl je podobný jako u RESTRICT. K dispozici jsou funkce eqjoinsel, neqjoinsel, scalarltjoinsel a scalargtjoinsel.

Po doplnění výše zmíněných klauzulí bude vypadat definice operátoru porovnání rodných čísel následovně:

DROP OPERATOR = (rodne_cislo, rodne_cislo);

CREATE OPERATOR = (
  leftarg    = rodne_cislo,
  rightarg   = rodne_cislo,
  procedure  = rodne_cislo_eq,
  commutator = =,
  negator    = <>,
  restrict   = eqsel,
  join       = eqjoinsel
);

DROP OPERATOR <> (rodne_cislo, rodne_cislo);

CREATE OPERATOR <> (
  leftarg   = rodne_cislo,
  rightarg  = rodne_cislo,
  procedure = rodne_cislo_ne,
  commutator = <>,
  negator    = =,
  restrict   = neqsel,
  join       = neqjoinsel
);

Pokud zkusíte indexovat dle sloupce rc v tabulce osoby, pravděpodobně obdržíte chybové hlášení o tom, že pro daný datový typ není určena třída operátoru pro přístup do „btree“.

To proto, že v PostgreSQL musí mít každý typ určen jednu (nejčastější případ) nebo několik tzv. tříd operátorů (např. pro komplexní čísla mohu budovat index pro absolutní velikost čísla, pro reálnou nebo imaginární část). Třída operátorů určuje, které operátory se budou používat pro indexování a jaký mají sémantický význam, tzv. Strategy Number.

Pro B-tree jsou určena tato Strategy Number

Tabulka č. 358
Menší než 1
Menší nebo roven 2
Roven 3
Větší nebo roven 4
Větší než 5

PostgreSQL umožňuje používat několik indexovacích metod (R-tree, Hash, GiST). Každá metoda má i vlastní tabulku Strategy Numbers. Kromě toho je potřeba definovat i tzv. Support rutinu. Pro B-tree index je pouze jedna (Strategy Number je 1). Jejími parametry jsou dva klíče a výsledkem celé číslo. Záporné, pokud je první parametr menší než druhý, nula, pokud jsou si rovny, a kladné číslo, pokud je první parametr větší než druhý.

Základ Support funkce máme. Stačí ji jen zpřístupnit v PostgreSQL

PG_FUNCTION_INFO_V1 (rodne_cislo__cmp);

Datum
rodne_cislo__cmp (PG_FUNCTION_ARGS)
{
  rodne_cislo *rc1 = PG_GETARG_RC_P (0);
  rodne_cislo *rc2 = PG_GETARG_RC_P (1);

  PG_RETURN_INT32 (rodne_cislo_cmp (rc1, rc2));
}
CREATE OR REPLACE FUNCTION rodne_cislo_cmp (rodne_cislo, rodne_cislo)
  RETURNS int AS 'rc.so', 'rodne_cislo__cmp' LANGUAGE 'C' STRICT;

CREATE OPERATOR CLASS rodne_cislo_ops
  DEFAULT FOR TYPE rodne_cislo USING btree AS
    OPERATOR  1    < ,
    OPERATOR  2    <=,
    OPERATOR  3    = ,
    OPERATOR  4    >=,
    OPERATOR  5    > ,
    FUNCTION  1    rodne_cislo_cmp (rodne_cislo, rodne_cislo);

V tuto chvíli je již náš typ rodne_cislo plnohodnotný datový typ, který můžeme použít jako primární klíč. Pro praktické použití je třeba poznamenat, že rodná čísla vydaná před rokem 1969 nemusí být jedinečná, jak se zjistilo při kuponové privatizaci.

Celý příklad naleznete v archivu.

Zdroje

Našli jste v článku chybu?