Snapshot too old – Behandlung langlaufender SELECTs

Wer größere Datenbank-Applikationen entwickelt, wird sich mit dem Problem auseinandersetzen müssen, was bei langlaufenden Abfragen eigentlich passiert.

Man hat also ein „SELECT“ am laufen, dass mehrere Sekunden oder sogar Minuten dauert, vielleicht sogar eine Stunde. Das kann durchaus sinnvoll sein, aber es lohnt sich natürlich, dieses mit gutem Wissen über die Datenbank-Software genau anzuschauen und zu optimieren.

Trotz aller Optimierungen muss man aber prinzipiell davon ausgehen, dass sich die Daten während des Lesezugriffs ändern. Es finden dauernd Schreibzugriffe statt, die natürlich erst nach Abschluss der Transaktion mit einem „COMMIT“ wirklich sichtbar werden. Nun können aber während des Lesezugriffs jede Menge Transaktionen stattfinden. Würde man diese berücksichtigen, müsste der Lesezugriff dauernd von vorne anfangen oder er würde inkonsistente Daten zurückliefern, die von verschiedenen Ständen der Datenbank stammen.

Die Lösung ist, den Stand der Datenbank vom Beginn des Lesezugriffs quasi einzufrieren und für die Dauer des Lesezugriffs zur Verfügung zu stellen. Zugriffe, die später beginnen, bekommen einen anderen Stand zu sehen, auch wenn sie früher fertig werden. Im Fall von Oracle-Datenbanken werden hierfür sogenannte Snapshots verwendet. Es werden also ähnliche Mechanismen wie für Transaktionen verwendet, um mehrere Stände der Datenbank parallel zu speichern.

Das bedeutet aber, dass bei einer sehr aktiven Datenbank immer größere Unterschiede zwischen den beiden Ständen entstehen. Bei vielen Transaktionen oder vielen parallelen langlaufenden Lesezugriffen sind sogar oft viel mehr als zwei Stände. Dafür gibt es Bereiche in der Datenbank, sogenannte Rollback-Segmente. Diese muss man definieren und bereitstellen und wenn sie zu klein sind, laufen sie irgendwann über. Dann kommt es im Fall von Oracle zu dem Fehler „ORA-01555: Snapshot too old“ und der lange Lesezugriff scheitert. Wenn man Pech hat nach ein paar Stunden. Wenn es ein Zugriff durch eine Software ist, wird es interessant, ob die Fehlerbehandlung für diesen Fall funktioniert oder ob es eine obskure „Exception“ in der Log-Datei gibt, die niemand bemerkt.

Mehr dazu findet man unter anderem hier:

Grundsätzlich muss diese Fragestellung bei allen ernsthaften transaktionalen Datenbanken gelöst werden und es ist sicher interessant, wie das im Detail aussieht. Vielleicht ist bei anderen Datenbanken so etwas wie eine „Lese-Transaktion“ („read transaction“) die Antwort. Als Isolationslevel muss man mindestens „repeatable read“ oder „serializable“ wählen. Wenn man mit niedrigeren Isolationsleveln arbeitet, muss man genau wissen, was man tut, da es sonst überraschende Fehler gibt.

Bei PostgreSQL muss man das mit
BEGIN TRANSACTION ISOLATION LEVEL REPEATABLE READ;
oder
BEGIN TRANSACTION ISOLATION LEVEL SERIALIZABLE;
explizit einstellen, sonst wird „READ COMMITTED“ gewählt.

Bei mariaDB und mysql hängt es von der DB-Engine ab. InnoDB verwendet als Default „REPEATABLE READ“, DB2 scheint eher auf „READ COMMITTED“ zu setzen, MS-SQL-Server auch auf READ COMMITTED.

Höhere Isolation-Level bedingen natürlich auch das Risiko von Verklemmungen („Deadlocks“), was erkannt werden sollte und zum Rollback einer der beiden Transaktionen führen sollte.

Es lohnt sich also, bei anspruchsvollen DB-Applikationen genau zu schauen, wie man mit diesen Transaktion-Isolationen umgehen will und ob man mit den Default-Eintstellungen der verwendeten Datenbank-Software zufrieden ist.

Vor einigen Jahren habe ich übrigens feststellen müssen, dass man diese niedrigeren Isolationslevel bei Oracle wohl einstellen konnte, dass die Datenbank oder der JDBC-Treiber sich dann aber fehlerhaft verhalten haben. Man sollte also auch herausfinden, ob die entsprechende Eintstellung von der verwendeten Datenbanksoftware unterstützt wird und zwar nicht nur auf dem Papier, sondern real und absolut zuverlässig. Eine unzuverlässige transaktionale Datenbank kann man billig durch einen Zufallszahlengenerator ersetzen.

Share Button

Fehler in Oracle-Datenbanken mit leeren Zeichenketten

English

Es scheint so, dass alle gängigen Oracle-Datenbanken leere Zeichenketten und null nicht unterscheiden. Das widerspricht der SQL-Spezifikation und ist damit ein Fehler.

Leider wäre es für Oracle nicht einmal einfach, den zu entfernen, weil zu viele Applikationen seit Jahrzehnten Oracle benutzen und sich auf dieses Verhalten eingestellt haben.

Entscheidend ist also erst einmal, diesen Fehler zu kennen und bei der Verwendung von Oracle-Datenbanken zu berücksichtigen. Solange man explizit mit SQL, also SQL*Plus, JDBC, DBI, Pro*C u.ä. auf die Datenbank zugreift, ist es noch einfacher, das zu berücksichtigen, aber heikel wird es bei der Verwendung von Frameworks wie Hibernate, wo man merkwürdige Fehler findet. Wenn man zum Beispiel eine Entität (engl. Entity) hat, die persistiert wird, und dort ein Attribut eine Zeichenkette ist, kann man legitimerweise dort die leere Zeichenkette zuweisen. Sobald dieses Objekt durch Hibernate wandert und persistiert wird, hat man plötzlich stattdessen null dort stehen und bekommt unerwartete NullPointerExceptions. Oder das Attribut ist in der Datenbank mit dem NOT-NULL-Constraint versehen und man bekommt beim Schreiben Oracle-Fehler. Außerdem ist es natürlich ein semantisches Problem, man möchte vielleicht gerne leere Zeichenketten und null dort speichern können und mit verschiedenen Bedeutungen behandeln.

Eine hässliche, aber doch praktikable Möglichkeit, damit umzugehen ist die Getter und Setter der Zeichenketten-Attribute entsprechend zu ändern. Man kann eine „magische Zeichenkette“ definieren, die man garantiert nicht als Wert dort speichern möchte. Dann kann man im Getter vergleichen, ob das Attribut diese Zeichenkette als Wert hat und in dem Fall die leere Zeichenkette zurückgeben. Und im Setter prüft man erst, ob der neue Wert diese magische Zeichenkette ist, was man mit einer Exception ablehnt, und andernfalls, ob der neue Wert die leere Zeichenkette ist, was man durch die magische Zeichenkette ersetzt.

Bespiel in Java (ohne JPA-Annotationen, Synchronisation, toString(), hashCode(), equals() u.s.w.; geht analog für andere Programmiersprachen)


public class MyEntity {
  private static String MAGIC_VALUE = "_._._«NULL»_._._";
  private String value;

  public String getValue() {
    if (MAGIC_VALUE.equals(value)) {
      return "";
    } else {
      return value;
    }
  }

  public void setValue(String newValue) {
    if (MAGIC_VALUE.equals(newValue)) {
      throw new IllegalArgumentException("value="+newValue+" not allowed");
    } else if ("".equals(newValue)) {
      this.value = MAGIC_VALUE;
    } else {
      this.value = newValue;
    }
  }
}

Damit das ganze auch mit Attributen funktioniert, die in der Datenbank als VARCHAR(1) oder VARCHAR(2) definiert sind, ist es vielleicht eine gute Idee, Steuerzeichen oder UTF-8-Zeichen aus einer Sprache, die man nie unterstützen will, zu verwenden. Aber es bleibt eine unbefriedigende Lösung.

Hat jemand eine bessere Idee dazu?

Gibt es noch andere Datenbanken, die den gleichen Fehler wie Oracle in diesem Punkt haben?

Share Button