Der Ausführungsplan

Werbung
Der Ausführungsplan – das unbekannte Wesen
Martin Hoermann
ORDIX AG
Paderborn – Wiesbaden – Münster – Köln – Neu-Ulm
Zusammenfassung
Die Antwortzeit eines SQL Befehls bei gegebenem Datenbestand und Datenbankstrukturen
wird im Wesentlichen durch den Ausführungsplan bestimmt. Die Interpretation eines Ausführungsplans und die bewusste Auswahl alternativer Pläne versetzt einen Datenbankadministrator oder Programmierer in die Lage, Performanceverbesserungen im Bereich von Faktoren
zu erreichen.
Dieser Beitrag stellt die Ermittlung eines Ausführungsplans vor, diskutiert die wichtigsten
Zugriffs- und Joinstrategien, sowie weitere Elemente des Ausführungsplans. Anschließend
werden verschiedene Varianten zur Beeinflussung von Ausführungsplänen erörtert. Abschließend stellt der Beitrag verschiedene Bewertungskriterien gegenüber. Dieser Beitrag konzentriert sich auf Ausführungspläne von Selects.
Einleitung
Ein Hase und ein Rehbock hatten einst ein Techtelmechtel. Daraus hervorgegangen ist der
bayerische gehörnte Hase. Nun verloren auch andere bayerische Gattungen mehr und mehr
die Hemmungen voreinander und crisensus crisensus, oder crisensus bavaricus, erblickte das
Mondlicht der Welt – der Wolpertinger.
Ähnlich wie der Wolpertinger ist auch der Ausführungsplan für viele ein unbekanntes Wesen.
Es wird von Plänen berichtet, welche nicht existieren. Mystifizierungen und goldene Regeln
versprechen schöne Ergebnisse, auch ohne eine Analyse. Tatsächlich lässt sich ein Ausführungsplan jedoch recht leicht in seine Bestandteile zerlegen, interpretieren und optimieren.
Wie die Bestandteile des Wolpertingers: Hirschgeweih, Hasenkörper, Entenfüße usw. sind
auch die Teile eines Ausführungsplans den meisten doch sehr bekannt ...
Abb. 1: Der Wolpertinger
Ermittlung des Ausführungsplans
Es gibt zahlreiche Möglichkeiten, zu einem SQL Befehl den Ausführungsplan zu ermitteln.
Die einfachste Variante ist sicherlich in SQL*Plus den Befehl set autotrace on abzusetzen. Zuvor muss die Tabelle plan_table mit dem Skript utlxplan.sql angelegt werden. In
Oracle 10g ist dies nicht mehr notwendig. Eine alternative Variante bietet der Befehl explain
plan for. Hierbei muss die Tabelle plan_table allerdings mit SQL ausgelesen werden. Pläne zu allen SQL Befehlen einer Session lassen sich aus der Trace Datei mit tkprof ermitteln.
Ausführungspläne zu SQL Befehlen aus Statspack lassen sich mit dem Skript sprepsql.sql
unter Angabe des Hash Value ermitteln. Für aktuelle SQL Befehle ist der Ausführungsplan in
der View v$sql_plan enthalten, das korrekte Auslesen ist allerdings nicht trivial.
Ausführungsplan lesen
Einen umfangreichen Ausführungsplan zu lesen und zu interpretieren, bedarf grundsätzlich
einiger Übung. Natürlich ist es möglich, eine wissenschaftliche Erläuterung zum Lesen von
Ausführungsplänen zu geben, aber von zu traversierenden Bäumen zu sprechen ist für den
Normalverbraucher doch etwas zu abstrakt. Wie die hebräische und arabische Sprache liest
sich der Ausführungsplan von rechts nach links und von oben nach unten. Jedoch lässt sich,
ausgehend von der Ergebnismenge zurück zu den Daten, auch eine umgekehrte Lesart vertreten.
Plan
----------------------------------SELECT STATEMENT
NESTED LOOPS
TABLE ACCESS BY INDEX ROWID a
INDEX RANGE SCAN a_idx
TABLE ACCESS BY INDEX ROWID b
INDEX RANGE SCAN b_idx
Schritt
-----------6
5 Verbinde 2 und 4
2
1
4
3
Der Autor bevorzugt SQL Befehle in Teile zu zerlegen und für die einzelnen Teile Ausführungspläne zu optimieren. Anschließend ist zu überlegen, wie Teilkomponenten optimal miteinander zu verknüpfen sind. Diese Methode führt meistens, aber nicht zwingend zu guten
Ausführungsplänen. In einigen Fällen kann es sinnvoll sein, z. B. optimierte Subselect oder
Inner Selects auch als Join zu schreiben, auch wenn diese Form unter Umständen schlechter
lesbar ist. Wie die Architekten sagen: Form follows function.
Aufbau
Ein Ausführungsplan besteht immer aus einem oder mehreren Zugriffen auf Objekte, meist
Segmenten. Bei mehreren Zugriffen werden die Ergebnismengen miteinander verknüpft.
Hierzu stehen verschiedene Joinstrategien zur Verfügung. Mit Mengenoperatoren lassen sich
gleichartige Ergebnismengen verbinden (z. B. UNION ALL). Die nächsten beiden Kapitel
beleuchten Zugriffs- und Joinstrategien intensiv.
Zur weitergehenden Verarbeitung von Ergebnismengen stehen zahlreiche Varianten von Sorts
über Groups bis hin zu Connect By und Filter Operationen zur Verfügung.
Zugriffsstrategien
Die sicherlich einfachste Zugriffsstrategie ist der Full Table Scan (FTS). Unter Verwendung
von Multiblock I/O (db_file_multiblock_read_count) liest der Oracle Prozess alle Blöcke
der Tabelle in den Block Buffer oder in die PGA bei parallelen FTS. Dort werden die einzelnen Zeilen bzgl. WHERE Bedingung, nachgelagerten Zugriffen usw. verarbeitet. Der FTS ist
in der Regel bei großen Ergebnismengen oder in Kombination mit Hash Joins sehr effizient.
Was genau „groß“ bedeutet, ist von sehr vielen Faktoren abhängig. Der FTS wird oft mit
langsam assoziiert, ist jedoch eine ernstzunehmende Zugriffsstrategie. Bei großen Tabellen
und kleinen Ergebnismengen ist der Full Table Scan oft ein Hinweis auf fehlende Indizes.
Plan
---------------------SELECT STATEMENT
TABLE ACCESS FULL A
Schritt
------------------------2
1
Eine alternative Zugriffsform auf Tabellendaten ist der Zugriffsplan Table Access by Index
ROWID. Bei diesem Zugriff ermittelt der Oracle Prozess über die physikalische Adresse
(ROWID) den zu lesenden Block und die Nummer des Datensatzes. Der Zugriff über die
ROWID ist die schnellste Möglichkeit, einen Satz aus einer Tabelle zu lesen. Die ROWID
stammt in der Regel aus einem vorgelagerten Index Zugriff. Die Alternative zum Full Table
Scan ist daher immer ein Index Zugriff plus den Zugriff auf die Tabelle. Die verschiedenen
Formen des Index Zugriffs werden nun diskutiert.
Der Index Unique Scan wird z. B. verwendet, wenn der Optimizer eine Spalte in einer WHERE Bedingung findet und auf dieser Spalte ein Unique Index liegt. In diesem Fall liest der
Prozess die Index Blöcke vom Segment Header (Root Block) über die sogenannten Branch
Blocks bis hinunter zum Leaf Block. Im Leaf Block findet Oracle dann den Wert mit einer
ROWID oder nichts, wenn der Wert nicht existiert. Wird ausschließlich der Wert benötigt, ist
der Prozess fertig, ansonsten folgt in der Regel ein Table Access über ROWID. Der Index
Unique Scan ist sehr effizient für den Zugriff auf einen Datensatz. Je häufiger ein Unique
Scan innerhalb eines Joins aufgerufen wird, desto eher lohnen sich alternative Zugriffsstrategien, wie z. B. der FTS.
Findet der Optimizer in der WHERE Bedingung einen Index für eine Treffermenge größer
eins und wählt diesen für den Ausführungsplan aus, handelt es sich um einen Index Range
Scan. Der Prozess startet wie beim Unique Scan. Abhängig von der WHERE Bedingung
sucht der Prozess anschließend solange nach links oder rechts im Index Baum weiter, wie er
passende Werte findet. Der Index Range Scan ist effizient für wenige Datensätze. Weiterhin
ist der Range Scan in Zusammenhang mit einem Order By effizient, da die Daten bereits sortiert erhoben werden. Je größer die Trefferquote und je umfangreicher die Weiterverarbeitung
ist, desto eher lohnt ein Fast Full Index Scan oder ein FTS.
Plan
-------------------------------------SELECT STATEMENT
TABLE ACCESS BY INDEX ROWID A
INDEX RANGE SCAN DESCENDING A_IDX
Schritt
-------3
2 – Daten über Rowid
1 – Index Rückwärts
Beim Index Full Scan traversiert der Oracle Prozess den Index Baum bzgl. seiner logischen
Sortierung. Das Ergebnis ist eine sortierte Ergebnismenge. Der Index Full Scan ist effizient,
wenn eine sortierte Ergebnismenge, z. B. bedingt durch ORDER BY, benötigt wird. Abhängig von diversen Initialisierungsparametern kann ein Fast Full Index Scan mit anschließender
Sortierung oder ein Full Table Scan durchaus performanter sein.
Die letzte hier diskutierte Zugriffsstrategie ist der Fast Full Index Scan. Bei dieser Strategie
liest der Prozess alle Blöcke eines Index mit Multiblock I/O. Dadurch ist ein sehr schnelles
Lesen garantiert. Da in der physikalischen Reihenfolge gelesen wird, ist die Ergebnismenge
anschließend nicht mehr sortiert. Der Zugriff ist ausgesprochen effizient, wenn alle benötigten
Daten, also WHERE und SELECT, im Index vorhanden sind. In dieser Konstellation ist der
Fast Full Index Scan fast immer effizienter als ein Full Table Scan. Bei einer hohen Trefferquote kann der Zugriff wegen des Multiblock I/O auch die Effizienz eines Index Range Scans
oder die eines Index Full Scans übertreffen, obwohl ggf. eine anschließende Sortierung notwendig ist.
Neben den vorgestellten Zugriffsstrategien gibt es zahlreiche weitere. Hierzu gehören beispielsweise der Index Skip Scan, Index Min/Max Scans, Zugriffe auf partitionierte Segmente,
auf Bitmap Indizes, auf externe Tabellen und diverse weitere. Die Kenntnis über Art und
Aufbau des Objektes helfen bei der Interpretation und Beurteilung der Zugriffsstrategie weiter.
Joinstrategien
Die zuvor genannten Zugriffsstrategien liefern jeweils eine Ergebnismenge zurück. Bei der
Verknüpfung von mehreren Tabellen oder Indizes müssen die Ergebnismengen miteinander
verbunden werden. Dies geschieht mit sogenannten Join Operationen, die jeweils genau zwei
Ergebnismengen miteinander verknüpfen. Es existieren drei Formen des Join.
-
Nested Loop
-
Sort Merge Join
-
Hash Join
Beim Nested Loop wird für jeden Datensatz der ersten Ergebnismenge die gesamte zweite
Ergebnismenge durchsucht. Sind beide Ergebnismengen sehr groß, so ist der Nested Loop
i.d.R. die ungünstigste Joinstrategie. Sind die Ergebnismengen sehr klein oder handelt es sich
hierbei um ein Cartesian Produkt (jeder mit jedem), stellt der Nested Loop oft eine effiziente
Strategie dar. Ist eine Ergebnismenge sehr klein, die andere sehr groß, so ist es wichtig, die
kleine gegen die große Menge zu schneiden.
Plan
----------------------------------SELECT STATEMENT
NESTED LOOPS
TABLE ACCESS FULL a
TABLE ACCESS BY INDEX ROWID b
INDEX RANGE SCAN b_idx
Schritt
-----------5
4 Verbinde 1 und 3
1
3
2
Der Sort Merge Join setzt zwei gleich geordnete Ergebnismengen voraus. Die Sortierung
muss dem Join Kriterium genügen. In diesem Fall können die beiden Ergebnismengen sehr
effizient miteinander verknüpft werden. Der Sort Merge Join ist effizient bei etwa gleich großen bereits vorsortierten Ergebnismengen. Im Idealfall können zwei Index Organized Tables
bzgl. ihres Primary Key extrem effizient verbunden werden, schneller geht’s nicht!
Der Sort Merge Join ist weniger effizient, je aufwendiger die vorhergehende Sortierung ist.
Weniger zu empfehlen ist die Strategie, wenn die Ergebnismengen in Größe stark differieren
oder wenn der Join nur wenige Treffer findet.
Plan
----------------------------------SELECT STATEMENT
SORT GROUP BY NOSORT
MERGE JOIN
SORT JOIN
INDEX FAST FULL SCAN A_IDX
SORT JOIN
TABLE ACCESS FULL B
Schritt
------------7
6 – hier ist kein Sort mehr notwendig
5 – verknüpfe 2 und 4, beide sortiert
2 – aber es muss nachsortiert werden
1 – schneller Index Zugriff
4
3
Beim Hash Join wird die vermeintlich kleinere Ergebnismenge in den Speicher gelesen. Bzgl.
des Join Kriteriums wird ein Hash Schlüssel berechnet, der einen Zugriff auf die Ergebnismenge mit minimalem Aufwand ermöglicht. Der Aufwand für einen Einzelzugriff ist, bedingt
durch den Hash-Algorithmus, effizienter als ein Indexzugriff. Der Hash Join ist sehr effizient,
wenn große Ergebnismengen mit kleinen Ergebnismengen verknüpft werden, also auf die
erzeugte Hash Tabelle sehr oft zugegriffen wird. „Klein“ steht dabei in Relation zum verfügbaren Speicher für Sortierungen (PGA_AGGREGATE_TARGET bzw. sort_area_size).
Plan
----------------------SELECT STATEMENT
HASH JOIN
TABLE ACCESS FULL A
Schritt
-----------------------4
3 – suche zu B Sätzen in der Hash Partition
1 – hier wird ge-hash-t
TABLE ACCESS FULL B
2
Der Hash Join ist meist ineffizient, wenn aufgrund der Tabellengröße sehr viele HashPartitionen entstehen.
Varianten
Zu diesen vier Joinstrategien existieren die Unterformen Anti-, Outer-, Cartesian- und SemiJoin. Das Vorgehen ist im Prinzip identisch, lediglich die Auswahl welche Datensätze zusammen passen ist jeweils eine andere. Der Anti-Join ermittelt die Datensätze, zu denen keine
korrespondierenden Sätze existieren, z. B. durch die NOT IN Klausel. Der Outer-Join lässt
auch Zeilen einer Ergebnismenge zu, wenn keine passenden Sätze in der zweiten Ergebnismenge zu finden sind. Der Semi-Join gibt Datensätze zurück, zu welchen eine Subquery mindestens einen Treffer liefert. Der Semi-Join findet bei der Exists Klausel Anwendung. Der
Cartesian-Join verbindet alle Sätze der ersten Ergebnismenge mit allen Sätzen der zweiten
Ergebnismenge.
Noch ein kleiner Nachtrag zum sogenannten Star-Join und zum Cluster-Join. Diese Operationen können Sie im Ausführungsplan vergeblich suchen, denn beide sind kein Join im eigentlichen Sinn. Vielmehr beschreibt der Star Join ein Konzept zur Verknüpfung von mehreren
Tabellen. Im Prinzip geht es hier um eine große Tabelle, welche mit mehreren kleinen – voneinander unabhängigen – Tabellen zu verbinden sind. Der Join selbst erfolgt mit Hash, Nested
Loop oder Sort Merge. Der Cluster-Join besteht aus einen Table Access (Cluster) plus einen
Table Access für jede weitere Tabelle verbunden über einen Nested Loop.
Keine Join Strategie im eigentlichen Sinn, aber Verknüpfungen von mehreren Ergebnismengen sind beispielsweise Bitmap Merge, Bitmap Or und Bitmap And. Bei diesen Verknüpfungen kann Oracle, im Gegensatz zu Join Operationen, auch mehr als zwei Ergebnismengen
miteinander verknüpfen.
Weitere Elemente des Ausführungsplans
Neben den Zugriffsstrategien und den Joinstrategien gibt es zahlreiche weitere Operationen in
einem Ausführungsplan. Die gängigen sind in folgender Liste aufgeführt.
-
Die oberste Operation ist aus der Menge SELECT, INSERT, UPDATE, DELETE und
zeigt an, um welche grundsätzliche Form des Zugriffs es sich handelt.
-
Vollständige Sortierungen einer Ergebnismenge sind an der Operation Sort (Order By) zu
erkennen, Sortierungen von Teilen einer Ergebnismenge, typisch bei der Verwendung von
analytischen Funktionen, zeigen die Operation Window (Sort) an. Sortierungen für Gruppierungen arbeiten mit der Operation Sort (Group By) und für Joins mit der Operation
Sort (Join). Die Verwendung der Distinct Klausel führt zur Operation Sort (Unique).
-
Die Auswahl bestimmter Zeilen durch eine Having Klausel ist im Ausführungsplan an der
Operation Filter zu erkennen.
-
Mengenfunktionen wie UNION ALL, MINUS und INTERSECT sind an gleichnamigen
Operationen im Ausführungsplan zu erkennen. Die Mengenfunktion UNION wird mit der
Operation UNION ALL und einem anschließenden Sort (Unique) verarbeitet.
-
Die Operation Connect By in ihren verschiedenen Varianten verweist auf hierarchische
Abfragen
Es gibt zahlreiche weitere Operationen in einem Ausführungsplan. Mit den hier Vorgestellten
lassen sich sicherlich weit über 90 % aller SQL Ausführungspläne analysieren.
Ausführungspläne beeinflussen
Es gibt zahlreiche Möglichkeiten, Ausführungspläne festzulegen, zu forcieren oder zu beeinflussen. Die nachfolgende Liste stellt, ohne einen Anspruch auf Vollständigkeit, mögliche
Varianten vor. Dabei kann ein Ausführungsplan natürlich grundsätzlich nur erzwungen werden, wenn das Ergebnis mit diesem Ausführungsplan zustande kommen kann.
-
Stored Outlines: Ausführungspläne werden in der Datenbank gespeichert und durch Verwendung des Initialisierungsparameters use_stored_outlines verwendet. Diese Variante ist sehr aufwendig, garantiert aber den gewünschten Ausführungsplan.
-
Hints: Durch Hinweise im SQL Befehl können Zugriffsstrategien, Joinstrategien und ähnliches erzwungen werden. Die Verwendung von Hints stellt in der Programmierung die
einfachste Form dar, bestimmte Ausführungspläne zu erzwingen. Hier ein Beispiel für einen Fast Full Index Scan:
SELECT /*+ INDEX_FFS (t1 i1) */ c1 FROM t1;
-
Initialisierungsparameter: es gibt zahlreiche Initialisierungsparameter, welche einen mehr
oder weniger signifikanten Einfluss auf die Erstellung von Ausführungsplänen haben. So
können z. B. mit optimizer_index_cost_adj oder optimizer_index_caching die geschätzten Kosten beeinflusst werden. Mit star_transformation_enabled und
hash_join_enabled lassen sich Joinstrategien vollständig ausschalten. Die Parameter
sort_area_size und db_file_multiblock_read_count beeinflussen die tatsächlichen
Kosten für bestimmte Operationen und finden dadurch natürlich auch Berücksichtigung
durch den kostenbasierten Optimizer (CBO).
-
Durch neue Indizes oder alternative Speicherformen (IOT) eröffnen sich natürlich grundsätzlich alternative Zugriffsstrategien.
-
Die Wahl des Optimizer (rule, choose) und beim kostenbasierten Optimizer die Wahl
des Optimierungsziels (First Rows, All Rows etc.) haben einen wichtigen Einfluss auf
die Strategien, die der Optimizer zur Erstellung von Ausführungsplänen wählt. First Rows
und All Rows stellen aber keine Operationen im Ausführungsplan dar! Ab Oracle 10g
steht der regelbasierte Optimizer nicht mehr zur Verfügung.
Bei regelbasierten Optimizern haben Statistiken einen wesentlichen Einfluss auf die Erstellung von Ausführungsplänen. Die Statistiken sollten ein möglichst gutes Abbild der tatsächlichen Datenverteilung und der Datenmengen liefern. Es ist allerdings auch möglich – und in
sehr wenigen Fällen sinnvoll – durch falsche Statistiken effizientere Ausführungspläne zu
erstellen.
Erfolg Messen
Die einfachste Beurteilung des Erfolges ist sicherlich die Antwortzeit des SQL Kommandos.
Dieses Verfahren ist allerdings unter stark schwankender Datenbanklast nicht reproduzierbar.
Weiterhin berücksichtigt es auch die Auswirkungen auf andere Benutzer nicht. So kann zwar
ein massiv paralleler Zugriff den Chef glücklich machen, jedoch die gesamte Belegschaft einer Firma der „Eieruhr“ ausliefern.
Eine andere Variante ist die Bewertung der Anzahl der Logical Reads. Eine Minimierung der
Logical Reads führt fast immer auch zu kürzeren Antwortzeiten bei gleichen Rahmenbedingungen, jedoch ist dieser Zusammenhang nicht zwingend. Der Autor bevorzugt diese Variante, da die Bewertung reproduzierbar ist.
Physical Reads sollten in der Regel nicht betrachtet werden, da diese stark abhängig vom aktuellen Caching der Datenbank sind. In einzelnen Fällen macht jedoch auch dies Sinn.
call
count
------- -----Parse
0
Execute
1
Fetch
43
------- -----total
44
cpu
elapsed
disk
query
current
------- --------- -------- -------- ---------0.00
0.00
0
0
0
0.00
0.00
0
0
0
2.10
15.57
58415
60777
30
------- --------- -------- -------- ---------2.10
15.57
58415
60777
30
rows
---------0
0
1055
---------1055
Die oben aufgeführte Statistik aus einer tkprof Analyse zu einem Ausführungsplan zeigt recht
anschaulich die vielfältigen Faktoren bei der Beurteilung von Ausführungsplänen. CPU Zeit
bedingt durch logical Reads (query+current), Elapsed Time durch physical Reads (disk)
und das in Relation zu der Ergebnismenge (rows) müssen sorgfältig untersucht werden. Verschiedene Ausführungspläne sollten hier – abgesehen von der Spalte rows – unterschiedliche
Aufwände zeigen.
Last but not Least stellen die Werte Cost, Card und Bytes im Ausführungsplan eine hilfreiche
Unterstützung bei der Bewertung – insbesondere von Zwischenschritten – dar. Die Kosten
einer Operation misst Oracle in der abstrakten Einheit Cost, also eine Art Oracle Dollar zur
Unterscheidung von besser und schlechter. Die Anzahl Datensätze wird geschätzt und über
die Kardinalität (Card) ausgegeben, ebenso die daraus resultierenden Bytes.
---------------------------------------------------------------------| Id | Operation
| Name
| Rows | Bytes | Cost |
---------------------------------------------------------------------|
0 | SELECT STATEMENT
|
|
7 |
21 |
2 |
|
1 | SORT GROUP BY NOSORT|
|
7 |
21 |
2 |
|* 2 |
INDEX RANGE SCAN
| MESSDATEN_PK |
140 |
420 |
2 |
----------------------------------------------------------------------
Das obige Beispiel zeigt recht anschaulich die Reduzierung der Zeilen (Rows) und daraus resultierend der Datenmenge (Bytes). Die Kosten bewertet der Optimizer dagegen gleich hoch.
Die Analyse wurde mit dem Befehl explain plan for und die anschließende Auswertung
mit select * from table(dbms_xplan.display( 'PLAN_TABLE' )) durchgeführt.
Fazit
Von den weit über 150 unterschiedlichen, möglichen Operationen im Ausführungsplan wurde
ein repräsentativer Teil vorgestellt. Eine Systematisierung zeigt die einzelnen Gruppen von
Operationen (Zugriffsstrategien, Joins usw.) und deren Zusammenspiel. „Teile und Herrsche“
ist das wichtigste Verfahren, um komplexe Ausführungspläne zu analysieren und diese anschließend optimieren zu können.
Literatur
Die Oracle Dokumentation liefert eine gute Basis für das Verständnis von Ausführungsplänen. Darüber hinaus finden sich unter den Adressen http://metalink.oracle.com und
http://technet.oracle.com sehr gute Ergänzungen und Whitepaper. Die mit Abstand umfangreichste Auflistung von Operationen des Ausführungsplans ist unter der Adresse
http://julian.dyke.users.btopenworld.com/ zu finden.
Kontaktadresse:
ORDIX AG
Martin Hoermann
Zentrale Paderborn
An der Alten Ziegelei 5
Westernmauer 12 – 16
D-48157 Münster
D-33098 Paderborn
Telefon: +49 (0) 2 51-9 24 35-00
+49 (0) 52 51-10 63-0
E-Mail: [email protected]
Internet: www.ordix.de
Eine Vollmondnacht in einem bayrischen Wald. Irgendwo im Gebirge ziehen zwei müde
Wanderer ihres Weges. Ein Rascheln erregt ihre Aufmerksamkeit, es kracht und zischt, plötzlich steht ein Tier vor ihnen das einem Alptraum entsprungen zu sein scheint. Sie erinnern
sich an das alte bayrische Sprichwort: "Siehst du einen Wolpertinger, sieh zu dass du wegkommst ..."
Herunterladen