Auf nicht vorhandene Datensätze abfragen :)

Hallo,

ich denke jetzt schon ein paar Tage darüber nach wie das gehen könnte aber was gescheites soll heissen schnell und einfach ist mir nicht eingefallen.

Mein übliches Problem eine Suchfunktion die Produkte aus einer Datenbank liest. Als Beispiel die Suche eines Stichwortes in der Titel-Tabelle. Als Anmerkung am Rand ein Produkt kann mehrere Titel haben.

Der Aufbau ist folgender
ProductKey, ist die Root-Tabelle genau ein Satz pro Produkt
ProductTitle, einthält die Produkttitel mindestens ein Satz pro Produkt
ProductLanguage, keiner, einer, oder viel pro Produkt

Ich will jetzt den Titel ‘Ich kann auch anders’ finden

z.B. so

[font="Fixedsys"]
select *
from  ProductKey Key, ProductTitle Title, ProductLanguage Lang
where Key.MyProductNr = Title.MyProductNr and
      Key.MyProductNr = Lang.MyProductNr and
      Title.TitleText = 'Ich kann auch anders' and
      Lang.LanguageCode = 'ger';
[/font]

(Die Suche in der Titel-Tabelle erfolgt per TSearch2 habe ich hier weggelassen da es zu diesem Problem keinen Unterschied macht.)

So nun zum eigentlichen Problem: Jetzt gibt es Dummer weise Produkte die keine Sprache haben z.B. Musik-CDs. Es gibt dann auch keinen Datensatz dafür in der ProductLanguage-Tabelle. Ich möchte auch keine leeren Datensätze dort anlegen weil das andere Tabellen mitgerechnet einige Millionen leere Datensätze in der gesamten DB erzeugen würde.

Ich müsste in diesem Fall die in der ProductTitle Tabelle gefundenen Datensätze die keinen Sprachdatensatz haben und die mit ‘ger’ in meine Ergebnismenge einbeziehen.

Würde mich freuen wenn einer eine Idee hat ich hab grad keine.

Grüße
Alexander

Ich habe gerade keine Zeit mehr zum Testen aber so könnte es vielleicht gehen.

[font="Fixedsys"]
select *
from ProductKey Key, 
         (select * from ProductTitle where TitleText = 'Ich kann auch anders') as Title, 
         ProductLanguage Lang
where Key.MyProductNr = Title.MyProductNr and
      ((Title.MyProductNr = Lang.MyProductNr and
        Lang.LanguageCode = 'ger') or
        Title.MyProductNr not in (select * from ProductLanguage));
[/font]

Wenn Ihr auch noch Ideen habt wie es vielleicht besser gehen könnte immer her damit.

ich habe gerade auch keine Zeit mich da genauer mit zu beschäftigen
aber so auf den ersten Blick sollte das doch mit z.b einem left join lösbar sein?
PostgreSQL: Documentation: Manuals: PostgreSQL 8.2: Table Expressions

So sollte es gehen, (wenn ich keinen Fehler reingebastelt hab :wink: :

SELECT *
  FROM (ProductKey Key
      INNER JOIN
        ProductTitle Title
      ON (Title.TitleText = 'Ich kann auch anders' AND Key.MyProductNr = Title.MyProductNr)
      )
      LEFT JOIN
        ProductLanguage Lang
      ON (Key.MyProductNr = Lang.MyProductNr AND Lang.LanguageCode = 'ger');

Hallo,

danke für die Antworten. Fröstels SQL funktioniert wunderbar. Ich muß das Teil jetzt nur noch umbauen weil ich wahlweise vier verschiedene Suchbegriffe habe die in beliebiger kombination vorkommen können.

Titel
Autor
Verlag
EAN

Wenn ich jetzt nach Titel und Autor suchen will dann muß ich doch die Tabelle mit den Autoren ebenfalls mit einem ‘inner join’ mit den anderen Verknüpfen und danach alle ‘left joins’ oder sehe ich da was falsch. Meine Test sahen zumindest bisher gut aus. Meine Anwendung lasse ich dann den SQL-Befehl entsprechend zusammenbauen und ausführen. Hm, könnte ein SQL auch selbst feststellen welcher Suchbegriff undgleich leer ist und nur nach denen suchen.

Hier mal das bisherige:

[font="Fixedsys"]
select distinct (Key.MyProductNr),
       Buch.PublicationDate
from   (ProductKey Key INNER JOIN ProductSchluessel Schl
        ON (--Title.idxfti @@ to_tsquery ('default', 'Ich & kann & auch & anders') and
            --Contr.idxfti @@ to_tsquery ('default', 'möhre') and
            Schl.Ean = '0828766153521'         and
            Key.MyProductNr = Schl.MyProductNr and
            Key.NotificationType in ('01', '02', '03', '04')))

        --INNER JOIN ProductTitle Title
        --ON (Key.MyProductNr = Title.MyProductNr and
        --    Title.idxfti @@ to_tsquery ('default', 'Ich & kann & auch & anders'))

        --LEFT JOIN ProductContributor Contr
        --ON (Key.MyProductNr = Contr.MyProductNr and
        --    Contr.idxfti @@ to_tsquery ('default', 'möhre'))


        LEFT JOIN ProductLanguage Lang
        ON (Key.MyProductNr = Lang.MyProductNr and
            Lang.LanguageCode = 'ger')

        LEFT JOIN ProductBuch Buch
        ON (Key.MyProductNr = Buch.MyProductNr)
order by Buch.PublicationDate desc
;
[/font]

Grüße
Alexander

Hi Alexander,

wie du deine Abfragen gestaltest, hängt von Struktur und den möglichen Inhalten deiner Tabellen ab. Wenn du Werte einer Spalte abfragst, die NULL-Einträge erlaubt (also nicht explizit mit “NOT NULL” deklariert ist), dann musst du davon ausgehen, das es Situationen gibt, in denen du nicht das erwartete Ergebnis erhältst (weil du einen inner join machst :wink: ).

Ein Abfrage nach Standard-(Kurz-)Schema

SELECT ... FROM tabelle1, tabelle2 WHERE tabelle1.id = tabelle2.id

ist nur eine andere Schreibweise für

SELECT ... FROM tabelle1 INNER JOIN tabelle2 ON (tabelle1.id = tabelle2.id)

. Wenn du in der select list (also die Spalten, die du als Ausgabe willst, stehen zwischen SELECT und FROM) Spalten mit möglichen NULL-Werten hast, kann das zu unerwarteten Ergebnissen führen, denn ein solcher NULL-Wert killt dir die ganze Zeile. (Kann man sich prinzipiell merken: NULL tötet alles, ob es ein SELECT ist, ob du einen String aus Variablen zusammenbaust, NULL ist ein schwarzes Loch in dem alles verschwindet.) Deshalb musst du NULL-Fälle immer gesondert behandeln.

Das geht mit einem LEFT JOIN (auch mit einem RIGHT JOIN). Die Spalten der Tabelle, die vor den Schlüsselwörtern LEFT JOIN steht, werden zurückgegeben. Die dazu passenden Spalten der Tabelle, die nach LEFT JOIN steht, werden nur dann angezeigt, wenn sie sinnvolle (d.h. nicht-NULL) Werte enthalten. Beim RIGHT JOIN ist es umgekehrt.

D.h. also ein LEFT JOIN sichert die Rückgabe der Zeilen der linken Tabelle und liefert Werte der rechten Tabelle, wo möglich und sinnvoll. Du musst dir also überlegen, was du wo brauchst.

Anmerkung:
Queries, die viele explizit geschrieben JOINs haben, explodieren recht schnell und werden unübersichtlich. Evt. schreibst du dir eine Funktion (zum in PL/PGSQL), der du die Suchbegriffe als Parameter übergibst. Je nachdem, welche Inhalte diese Parameter haben, kannst du dann auf eine passende, kleinere und speziellere Query verzweigen.

Zu deinen Fragen:
Wenn für jeden Artikel Titel und Autor vorhanden sind, dann kannst du sie per INNER JOIN verwenden, sollte bei einigen der Autor fehlen, dann empfiehlt sich ein LEFT JOIN. Alles was bei der Beschreibung eines Artikels in seiner Gesamtheit leer (also NULL) sein könnte mit LEFT JOIN anbinden.

In (Postgre)SQL kannst du mit einer CASE WHEN … THEN-Konstruktion was machen, ich bevorzuge Funktionen z.B. in PL/PGSQL. Damit kann man etwas mehr programmiersprachen-artig mit einzelen Suchbegriffen umgehen. :sunglasses:

CU
Fröstel

Ach so, noch ein Nachtrag:
Wie Du deine JOINS ordnest bleibt dir überlassen. Du kannst nach Bedarf auch mit einem LEFT JOIN anfangen und dann INNER JOINs folgen lassen usw.
Die Überlegung sollte dahin gehen, wie das Zwischnergebnis nach dem jeweiligen JOIN aussieht. Über den Daumen gepeilt, sollten JOINs, die die Ergebnismenge erheblich verkleinern, möglichst am Anfang stehen. Ist nur eine Vermutung, dass das die Performance steigern könnte :confused: - aber abgesehen davon ist es für den Menschen an sich leichter, den Überblick zu behalten. :wink:

CU
Fröstel

Hallo Fröstel und alle anderen,

danke für die genaue Erklärung. Ich montiere jetzt den SQL-Befehl in meinem C/C++ Programm zusammen. Das mit den PL/PGSQL-Funktionen muß ich mir mal ansehen wenn ich etwas mehr Zeit habe. Diese Funktionen sind sicher interessant weil diese dann auch in anderen Programmen zur Verfügung stehen.

Für alle die es interessiert unter: Abygale.de - weil lesen Spass macht kann man die Suchfunktion mal ausprobieren. Die ist aber noch etwas langsam. Wenn die Suche nicht zu viele Treffer ergibt ca. 2 Minuten. Bei vielen Treffern läuft meistens vorher der Timeout der Verbindung ab. Wenn jemand zufällig beim anschauen noch einen Fehler entdeckt bin ich für Hinweise dankbar. :rolleyes:

CU
Alexander

Hi Alexander,

du kannst deine C-Funktionen auch direkt in Postgres verwenden, wenn du die enthaltene Schnittstelle verwendest. Hab mich zwar noch nicht damit befasst, aber die Doku scheint da recht ausführlich zu sein. Damit kriegst du vielleicht deine Geschwindigkeitsprobleme in den Griff.

CU
Fröstel

Hi Fröstel,

das mit den C-Funktionen auf Postgres Seite ist für mich vielleicht einfacher da kenne ich mich besser aus. Was ich allerdings nicht verstehe ist warum das schneller sein soll?

Beim Testen ist mir jetzt aufgefallen dass der SQL so wie ich ihn jetzt habe noch nicht ganz das tut was er soll.

so siehts aus:

[font="Fixedsys"]
select distinct (Key.MyProductNr),
       Buch.PublicationDate,
       Lang.LanguageCode

from   ProductKey Key INNER JOIN ProductTitle Title
       ON (Key.MyProductNr = Title.MyProductNr and
           Key.NotificationType in ('01', '02', '03', '04') and
           Title.idxfti @@ to_tsquery ('default', 'Blender'))

       LEFT JOIN ProductLanguage Lang
       ON (Key.MyProductNr = Lang.MyProductNr and
           Lang.LanguageCode = 'ger')

       LEFT JOIN ProductBuch Buch
       ON (Key.MyProductNr = Buch.MyProductNr)

order by Buch.PublicationDate desc;
[/font]

und das kommt raus:

[font="Fixedsys"]
 myproductnr | publicationdate | languagecode
-------------+-----------------+--------------
    16652222 | 200806          | ger
    15409803 | 200804          | ger
    16809182 | 200712          |
    17298633 | 200710          |
    15702607 | 200709          |
    15668508 | 200708          |
    17720510 | 200708          | ger
    15841632 | 200706          |
    17607134 | 200705          | ger
    17649184 | 200702          |
    15641862 | 200610          |
    10359836 | 200609          | -- Dieser ist Englisch ???
    17503661 | 200503          | ger
     8378337 | 200407          |
    16222600 | 200309          |
    ...
(37 Zeilen)

abygale.de=# select * from productlanguage where myproductnr = '10359836';
 myproductnr | languagerole | languagecode |          modtime
-------------+--------------+--------------+----------------------------
    10359836 | 01           | eng          | 2007-07-29 09:47:19.783265
(1 Zeile)
[/font]

Ich habe nur einen überprüft. Der hat aber einen Satz in der Sprachtabelle der nicht ‘ger’ sondern ‘eng’ ist und somit eigentlich nicht in der Ergebnismenge auftauchen soll.

Grüße
Alexander

Hi Alexander,

@C-Functions
Nicht dass ich es genau wüsste, aber wenn die C-Funktion Bestandteil des DBMS ist, dann kann der Query-Planer diese vermutlich in seine Planung einbeziehen und dadurch den schnellsten Weg für die Query finden. Wär’ aber nur so ne Vermutung :confused: … Hab noch nicht mit C-Funktionen innerhalb und außerhalb von PostgreSQL gearbeitet, geschgweige denn die Performance verglichen.

@seltsames Ergebnis deiner Query
Hab jetzt nicht gerade parat, was genau bei der Query-Planung im DBMS passiert, würde aber an deiner Stelle mal an folgenden Stellen etwas experimentieren:
1.) Du LEFT JOINst immer die Tabelle Key mit jeweils den Tabellen Lang und Buch und zwar über

Key.MyProductNr = Lang.MyProductNr

bzw.

Key.MyProductNr = Buch.MyProductNr

Beide sind damit unabhängig voneinander und nur an Key geknüpft. Ich vermute mal, die von dir kommentierte Ausgabe-Zeile ist in der Spalte languagecode leer. Die Schlussfolgerung daraus würde lauten, dass der LEFT JOIN mit Lang auch wirklich nur Einträge mit ‘ger’ geliefert hat.

2.) Evt. löst sich das Problem, wenn du die Query clever mit Klammern versorgst. Etwa so (ohne Garantie, da nicht ausprobiert):

SELECT distinct (Key.MyProductNr),
       Buch.PublicationDate,
       Lang.LanguageCode
FROM
      (
        (
          (
            ProductKey Key INNER JOIN ProductTitle Title
            ON (Key.MyProductNr = Title.MyProductNr AND
              Key.NotificationType in ('01', '02', '03', '04') AND
              Title.idxfti @@ to_tsquery ('default', 'Blender'))
          )
          LEFT JOIN ProductLanguage Lang
          ON (Key.MyProductNr = Lang.MyProductNr AND
            Lang.LanguageCode = 'ger')
        )
       LEFT JOIN ProductBuch Buch
       ON (Key.MyProductNr = Buch.MyProductNr)
      )
ORDER BY Buch.PublicationDate DESC;

(Das äußerste Klammernpaar nach FROM dürfte überflüssig sein.)
Falls nicht, geht es vermutlich, wenn du in die JOIN-Bedingung einer der beiden Tabellen, die andere mit hineinnimmst, z.B. (auch wieder ohne Gewähr)

SELECT distinct (Key.MyProductNr),
       Buch.PublicationDate,
       Lang.LanguageCode
FROM
      (
        (
          (
            ProductKey Key INNER JOIN ProductTitle Title
            ON (Key.MyProductNr = Title.MyProductNr AND
              Key.NotificationType in ('01', '02', '03', '04') AND
              Title.idxfti @@ to_tsquery ('default', 'Blender'))
          )
          LEFT JOIN ProductLanguage Lang
          ON (Key.MyProductNr = Lang.MyProductNr AND
            Lang.LanguageCode = 'ger')
        )
       LEFT JOIN ProductBuch Buch
       -- entweder
       ON (Key.MyProductNr = Buch.MyProductNr
              AND Lang.MyProductNr = Buch.MyProductNr)
       -- is aber doppelt gemoppelt - oder
       -- ON (Lang.MyProductNr = Buch.MyProductNr)
      )
ORDER BY Buch.PublicationDate DESC;

Evt. solltest du die JOINs etwas rearrangieren, dann kannst du ein übersichtliches “ID-Chaining” machen, z.B. erst LEFT JOIN mit Buch ON Key.MyProductNr = Buch.MyProductNr und dann LEFT JOIN mit Lang ON Buch.MyProductNr = Lang.MyProductNr. Macht aber wohl inhaltlich keinen Unterschied, liest sich aber etwas besser, wie ich finde.

CU
Fröstel

Beide sind damit unabhängig voneinander und nur an Key geknüpft. Ich vermute mal, die von dir kommentierte Ausgabe-Zeile ist in der Spalte languagecode leer. Die Schlussfolgerung daraus würde lauten, dass der LEFT JOIN mit Lang auch wirklich nur Einträge mit ‘ger’ geliefert hat.

Die Spalte languagecode ist nicht leer steht auf ‘eng’. Im unteren Teil des Postings mit dem Ergebnis ist eine Abfrage die genau diese Zeile zeigt.

Den Rest muß ich erst noch testen

Hallo Fröstel,

leider funktioniert das so nicht. Die Klammern machen keinen Unterschied. Wenn ich die Join Bedingung ändere kommt ein etwas anderes Ergebnis aber leider immer noch falsch.

sieht so aus:

[font="Fixedsys"]
 myproductnr | publicationdate | languagecode
-------------+-----------------+--------------
      102801 |                 |
     1259377 |                 |
     1758891 |                 |
     8255195 |                 |
     8378337 |                 |
     8728710 |                 |
     9413073 |                 |
    10345043 |                 |
    10359836 |                 |
    11559735 |                 |
    11904395 |                 |
    13233878 |                 |
    13836451 |                 |
    14182244 |                 |
    14393067 |                 |
    14393068 |                 |
    14970916 |                 |
    15222057 |                 |
    15319855 |                 |
    15585854 |                 |
    15628730 |                 |
    15641862 |                 |
    15668508 |                 |
    15702607 |                 |
    15841632 |                 |
    16222600 |                 |
    16788815 |                 |
    16809182 |                 |
    17071335 |                 |
    17298633 |                 |
    17455480 |                 |
    17649184 |                 |
    16652222 | 200806          | ger
    15409803 | 200804          | ger
    17720510 | 200708          | ger
    17607134 | 200705          | ger
    17503661 | 200503          | ger
(37 Zeilen)
[/font]

Es gibt auch die dazugehörigen Sprachdatensätze die korrekt ‘eng’ und andere Sprachen enthalten. In diese Liste sind nur zwei dabei die keinen Sprachdatensatz haben und zusammen mit ‘ger’ im Ergebnis enthalten sein sollten. Also 7 Treffer und nicht 37.

Mit der Join Bedingung auf ‘ger’ werden alle ungleich ‘ger’ doch aus der Menge ausgeblendet. Interpretiert der das dann so also ob es dafür keinen Satz gäbe und beläst ihn in der Ergebnismenge. Kann das sein kommt mir zumindest komisch vor die Erklärung. :wink:

Grüße
Alexander

Cool it’s run… with Linux :smiley:

was hälts Du davon:

[font="Fixedsys"]
select distinct (Key.MyProductNr),
       Buch.PublicationDate,
       Lang.LanguageCode

from   ProductKey Key INNER JOIN ProductTitle Title
       ON (Key.MyProductNr = Title.MyProductNr and
           Key.NotificationType in ('01', '02', '03', '04') and
           Title.idxfti @@ to_tsquery ('default', 'Blender'))

       LEFT JOIN ProductLanguage Lang
       ON (Key.MyProductNr = Lang.MyProductNr) -- and
           --Lang.LanguageCode = 'ger')

       LEFT JOIN ProductBuch Buch
       ON (Key.MyProductNr = Buch.MyProductNr)

where  (Lang.LanguageCode = 'ger' or
        Lang.LanguageCode is null)

order by Buch.PublicationDate desc;
[/font]

liefert das:

[font="Fixedsys"]
 myproductnr | publicationdate | languagecode
-------------+-----------------+--------------
    14393067 |                 |
    14393068 |                 |
    16652222 | 200806          | ger
    15409803 | 200804          | ger
    17720510 | 200708          | ger
    17607134 | 200705          | ger
    17503661 | 200503          | ger
(7 Zeilen)
[/font]

Zu diesem Beispiel liefert es jetzt das korrekte Ergebnis. Ich muß es noch im Detail testen aber vielleicht hab ich jetzt endlich mal eine allgemein gültige Lösung. Leider verspricht der Explain eine etwas schlechter Performance.

Hallo Fröstel,

nach einigen Test scheint die Variante von mir die auf Deiner aufbaut zu funktionieren. Danke nochmal für die vielen ausführlichen Erklärungen ohne die hätte ich sicher noch eine ganze weile gesucht bis ich die passenden Joins gefunden hätte.

Eine Frage hätte ich aber noch: Mit meinem Rechener ist diese Suche etwas langsam was hauptsächlich daran liegt dass die Daten von der Platte zu lesen zu lange dauert. Ich habe im Durchschnitt 25% iowait und 25% CPU. Da ich 24 Stunden am Tag Productupdates verarbeite um hinter her zu kommen bleiben einmal gelesene Daten die für die Volltextsuche gebraucht werden nicht im Speicher. Meine Idee war nun per Script im Abstand von einigen Minuten eine Suche nach verschiedenen Stichworten zu Starten um die Speicherverwaltung zu zwingen die Daten im Speicher zu halten. Ich habe mal getestet wenn ich über meine Internetseite einige verschiedene Stichworte Suche sinkte die Antwortzeit deutlich.

Grüße
Alexander

Hallo Alexander,

hast du dich schonmal mit PostgreSQL Tuning befasst, vielleicht hilft es was… z.B. http://www.westnet.com/~gsmith/content/postgresql/pg-5minute.htm mit weiterführenden Links am Ende der Seite.

CU
Fröstel

Hi Fröstel,

ja die Seite und einige andere kenne ich. Meine shared_buffers steht auf 1000 = 8 MB, ein GB hat der Rechner RAM. Ich hatte es schon mal mit deutlich höheren Werten versucht das machte aber Problem ich kann nicht mehr sagen welcher Wert oder welche Probleme. Ich teste es nochmal vielleicht hatte ich ja was falsch gemacht damals. Der Kernel kann als default glaube ich nur 32 MB aber da gibts irgendwo eine Datei in der man dann größere Werte konfigurieren kann. Über das proc Filesystem geht das wenn ich mich nicht irre sogar im laufenden Betrieb.

effectiv_cach_size = 112500 // 900 MB nach top cach gesetzt

und dann halt die Platten mehr schneller und einen guten Controller. Aber da muß ich erst mal einige Bücher, CDs und so weiter verkaufen um das zu finanzieren. Ich hatte mal versucht bei E-Bay so einen 3ware 9500 Controller zu bekommen aber bisher hat immer einer noch etwas mehr geboten.

Das andere ist halt noch den Arbeitsspeicher ausbauen. 3 GB kann das Motherboard aber der Speicher ist im verhältnis zu neueren sehr teuer. Ich konnte mich noch nicht entscheiden ob gleich ein neues Motherboard oder doch nur mehr Speicher. :wink:

Grüße
Alexander

P.S.: Ich habe grad mal nachgesehen und laut ipcs ist der SHM für postgres 22 MB groß. Ich dachte immer die rechnen in 8KB Blöcken und 8 mal 1000 sind knapp 8MB ???

Ich habe es jetzt auf shared_buffers=12000 geänder sind etwa die zur Zeit nach shmmax möglichen 128 MB.

Hallo fröstel,

ich habe jetzt mal die Speichereinstellungen für shared_mem und work_mem erhöht auf die empfohlenen Werte. Ich habe aber nicht den Eindruck dass das an der Performance dieser Suche irgend etwas ändert.

Mit dem select wie ich ihn zuletzt gepostet hatte braucht das bei umfangreichen treffern ca. 2Minuten (z.B. Physik, PHP). Bei wenigen Treffern etwa 1 Minute (z.B. Gimp).

Während die Suche läuft zeigt top Werte von bis zu 98% iowait.

Grüße
Alexander

Was ich hier vermisse ist ein EXPLAIN ANALYZE der fraglichen Queries. Kannst du die bitte mal liefern?

Gerne mach ich das.

die Abfrage:

[font="Fixedsys"]
explain analyze
select distinct (Key.MyProductNr),
       Buch.PublicationDate

from   ProductKey Key INNER JOIN ProductTitle Title
       ON (Key.MyProductNr = Title.MyProductNr and
       Key.NotificationType in ('01', '02', '03', '04') and
       Title.idxfti @@ to_tsquery ('default', 'PHP'))

       LEFT JOIN ProductLanguage Lang
       ON (Key.MyProductNr = Lang.MyProductNr)

       LEFT JOIN ProductBuch Buch
       ON (Key.MyProductNr = Buch.MyProductNr)

where  (Lang.LanguageCode = 'ger' or Lang.LanguageCode is null)
order  by Buch.PublicationDate desc;
[/font]

der Explain:

[font="Fixedsys"]
 Unique  (cost=99959.76..99989.29 rows=3938 width=18) (actual time=80309.809..80310.001 rows=102 loops=1)
   ->  Sort  (cost=99959.76..99969.60 rows=3938 width=18) (actual time=80309.807..80309.860 rows=184 loops=1)
         Sort Key: buch.publicationdate, "key".myproductnr
         ->  Nested Loop Left Join  (cost=106.66..99724.60 rows=3938 width=18) (actual time=56626.787..80307.971 rows=184 loops=1)
               ->  Nested Loop Left Join  (cost=106.66..77266.43 rows=3938 width=8) (actual time=56606.961..78052.619 rows=184 loops=1)
                     Filter: (("inner".languagecode = 'ger'::text) OR ("inner".languagecode IS NULL))
                     ->  Nested Loop  (cost=106.66..54772.84 rows=3938 width=8) (actual time=56587.148..71918.743 rows=500 loops=1)
                           ->  Bitmap Heap Scan on producttitle title  (cost=106.66..21121.13 rows=5902 width=8) (actual time=56573.978..63492.266 rows=650 loops=1)
                                 Filter: (idxfti @@ '''php'''::tsquery)
                                 ->  Bitmap Index Scan on idxfti_producttitle_idx  (cost=0.00..106.66 rows=5902 width=0) (actual time=56564.165..56564.165 rows=866 loops=1)
                                       Index Cond: (idxfti @@ '''php'''::tsquery)
                           ->  Index Scan using productkey_pkey on productkey "key"  (cost=0.00..5.69 rows=1 width=8) (actual time=12.953..12.954 rows=1 loops=650)
                                 Index Cond: ("key".myproductnr = "outer".myproductnr)
                                 Filter: ((notificationtype = '01'::text) OR (notificationtype = '02'::text) OR (notificationtype = '03'::text) OR (notificationtype = '04'::text))
                     ->  Index Scan using keyproductlanguage on productlanguage lang  (cost=0.00..5.70 rows=1 width=15) (actual time=12.018..12.256 rows=1 loops=500)
                           Index Cond: ("outer".myproductnr = lang.myproductnr)
               ->  Index Scan using keyproductbuch on productbuch buch  (cost=0.00..5.69 rows=1 width=18) (actual time=11.595..12.232 rows=1 loops=184)
                     Index Cond: ("outer".myproductnr = buch.myproductnr)
 Total runtime: 80310.442 ms
[/font]

Grüße
Alexander