search
subnavi
Werbung

Datenbanken

Innerhalb der Hierarchie von de.comp.lang.php werden Fragen im Zusammenhang mit Datenbanken in der Gruppe news:de.comp.lang.php.datenbanken besprochen. Die Charta dieser Newsgroup ist:

Hier geht es um alle Fragen, die bei der Verwendung von PHP mit den verschiedensten Datenbanken wie MySQL, PostgreSQL, Oracle, MSSQL etc. auftreten können. Fragen zur Datenbank selbst seien auf die jeweilige Datenbank-Gruppe verwiesen.

Fragen, die unabhängig von der Programmiersprache PHP sind, sollten also zum Beispiel in folgenden Gruppen diskutiert werden: de.comp.datenbanken.misc , de.comp.datenbanken.mysql und comp.databases.* (englischsprachig)

Frage: Wie kann ich mehr über SQL lernen?

Antwort von Kristian Köhntopp:

Von Markt und Technik gibt es das Buch "SQL in 21 Tagen". Den vollständigen Text kann man bei IT-4-YOU . online lesen. Man kann das Buch auch kaufen .

Guido Stepken hat sein MySQL Datenbankhandbuch auf seiner Website zum Online-Lesen oder zum Download bereitgestellt. (Dieser Text stammt von September 1999 und ist nicht mehr auf Stand).

Das Buch " PostgreSQL ", welches bei Addison-Wesley erschienen ist, kann auch online gelesen werden.

Ein englischsprachiges Einsteiger-Tutorial findet sich unter www.sqlcourse.com (Teil 1, behandelte Themen von "Table basics" über "Inserting/Updating/Deleting" bis "Advanced Queries"). Teil 2 nimmt sich folgende Themen vor: "Aggregate Functions", "GROUP BY/HAVING/ORDER BY", "Table Joins" etc. Nettes Special: Ein Online-SQL-Interpreter.

Für MySQL-spezifische Fragen gibt es eine eigene deutschsprachige Newsgroup: de.comp.datenbanken.mysql ( FAQ dieser NG )

Frage: Wieso kann ich mehrere, durch Semikolon getrennte Statements nicht ausführen?

Antwort von Kristian Köhntopp:

SQL kennt keine Mehrfachstatements. Einige SQL-Frontends (der MySQL-Kommandoprozessor, phpMyAdmin) kennen Mehrfachstatements, die sie manuell in einzelne Anweisungen zerlegen und nacheinander an den Datenbankserver senden. PHP selbst macht dies nicht. Man muss seine Statements manuell zerlegen und einzeln nacheinander absenden.

Um ein Statement zu zerlegen, ist es nicht ausreichend, auf dieses Statement einfach explode() anzuwenden. Beispiel:

INSERT INTO table VALUES('foo;bar'); 

Wie es richtig geht, kann man im Code von phpMyAdmin nachlesen. Die relevante Stelle ist die Funktion split_sql() in der Datei db_readdump.php .

Frage: Ist es sinnvoll, Bilder in einer Datenbank abzulegen?

Antwort von Kristian Köhntopp:

Aus irgendeinem Grund scheinen viele Leute zu glauben, dass es Bilddaten adeln würde, wenn man sie in eine Datenbank stopft.

Wenn man die Bilddaten selbst in der Datenbank ablegt, hat dies den Vorteil, dass keine broken links auftreten können, weil ja die Bilder selbst genauso wie die Links auf die Bilder aus der Datenbank erzeugt werden. Liegen die Bilddaten dagegen im Dateisystem und die Datenbank enthält nur Pfadnamen, dann ist es problemlos möglich, dass jemand die Dateien umbenennt, ohne diese Änderung in der Datenbank nachzuführen und umgekehrt. Leider ist es speziell bei MySQL so, dass keinerlei Mechanismen vorhanden sind, die die referentielle Integrität der Datenbank sicherstellen, sodass diese Sicherheit nicht wirklich gegeben ist.

Dazu kommen noch eine Reihe von weiteren Nachteilen:

    • Wenn man die Bilddaten selbst in der Datenbank speichert, dann muss man für jedes Bild in einer Webseite ein Script starten. Das bedeutet, für eine Seite wie

      index.php: <html> <body> <h1>Bla</h1> <img src="sendimage.php?img=geniales_logo.gif" /> </body> </html>

      muss nicht nur das Script index.php gestartet werden, um das HTML zu generieren, sondern für jedem Image-Tag auf der Seite muss ein Script sendimage.php gestartet werden, das eine Datenbankverbindung aufmacht und das Bild aus der Datenbank fischt. Wenn CGI PHP verwendet wird, ist der Overhead noch viel größer, denn hier muss für jedes Bild ein 800 kB großer PHP-Prozess erzeugt und gestartet werden.

      Legt man dagegen die Bilder als Dateien im Dateisystem ab, kann man mit der Static Page Engine des Webservers oder gar einem spezialisierten Bilder-Webserver arbeiten und ist um ca. den Faktor 10 effizienter.

    • MySQL kann BLOBs (binary large objects) nicht fragmentarisch bearbeiten, d.h. es ist nicht möglich, ein BLOB in kleinen Teilstücken aus der Datenbank zu holen oder den hinteren Teil eines BLOBs zu holen, ohne die Bytes davor zu lesen. Obendrein ist der Sendepuffer von MySQL für BLOBs begrenzt groß, sodass nicht beliebig große BLOBs in der Datenbank abgelegt werden können.

    • Viele Datenbanken werden sehr ineffizient, wenn vergleichsweise große BLOBs zusammen mit anderen, sehr kleinen Objekten in derselben Tabelle gespeichert werden oder wenn eine Tabellenzeile mehr als ein BLOB enthält.

Wie man Bilder in einer MySQL-Datenbank speichert, wird im Artikel mysql-blob beschrieben.

Frage: Windows: Jeder Zugriff auf meine Datenbank dauert eine halbe Minute!

Antwort von Kristian Köhntopp:

Eine Komponente Deines Netzes versucht, aus IP-Nummern per gethostbyaddr() auf den Hostnamen zu schließen und findet keinen Domain Name Server (DNS). Die Verarbeitung des Requests wird erst nach dem DNS-Timeout fortgesetzt.

Sorge dafür, dass ein DNS-Server mit korrektem Reverse Lookup verfügbar ist oder sorge dafür, dass keine Reverse Lookups gemacht werden. Dazu musst Du zunächst einmal die Komponente identifizieren, die die Lookups macht.

Frage: Wie kann ich meine Datenbankperformance steigern?

Antwort von Kristian Köhntopp:

Bei jeder Art von Performancetuning ist das Wichtigste zunächst einmal eine Messung. Es kommt ganz entscheidend darauf an, als erstes festzustellen, was denn genau langsam ist, bevor man sich daran macht, die Dinge zu verändern. Wenn ein Script mit Datenbankzugriff zu langsam ist, dann kann dies mehrere Ursachen haben:

Die Datenbank steht offsite oder ist nur langsam erreichbar.

Wenn die Datenbank nicht auf derselben Maschine läuft wie der Webserver, dann findet die Kommunikation zwischen Datenbank-Client und Server nicht mehr über schnelle Kommunikationsmethoden wie shared memory oder UNIX Domain Sockets statt, sondern über eine TCP/IP-Verbindung, die eine wesentlich geringere Kapazität und wesentlich höhere Latenzzeiten hat. Dies hat besonders fatale Auswirkungen, wenn die Datenbank und der Webserver durch ein langsames Netzwerk getrennt sind (Umlaufzeiten für Pakete von 10ms und mehr) oder wenn die Netzwerkbandbreite eingeschränkt ist (8 KB/sec und weniger).

Hier kommt es ganz entscheidend darauf an, die Anzahl der Anfragen pro Seitenaufbau zu vermindern und die Menge der übertragenen Daten zu verringern. Die Anzahl der Abfragen lässt sich dadurch vermindern, dass man SQL JOIN-Operationen statt vieler Abfragen verwendet. Ein typisches, falsches Konstrukt ist

// Liste der Treffer bestimmen
$result=do_database_query("select id from tabelle where $bedingung");

// Treffer anzeigen
foreach ($result as $k => $v) {
  $detail = do_detail_query("select * from tabelle2 where id =$v");
  show_detail($detail);
} 

Dieser Code generiert eine Masse von Queries nacheinander. Für jede einzelne Query wird ein Umlauf zur Datenbank und zurück notwendig und so summieren sich diese Umlaufzeiten zu gigiantischen Wartezeiten beim Seitenaufbau. Viel geschickter ist stattdessen

// Treffer bestimmen
$detail = do_database_query("select * from tabelle, tabelle2
 where ( $bedingung ) and tabelle.id = tabelle2.id");
foreach ($detail as $k => $v) {
  show_detail($v);
} 

Dies liefert die gewünschten Daten mit einer einzigen Query.

Die Datenbank hat hohe Verbindungsaufbaukosten und es wird CGI PHP verwendet.

Einigen Datenbanken, wie z. B. MySQL, macht es nichts aus, Datenbanklinks zu öffnen und wieder zu schließen. Andere Datenbanken, wie z. B. Oracle, starten bei jedem Connect einen eigenen Clientprozess. Dies ist ein sehr aufwendiger Vorgang. Wenn CGI PHP verwendet wird, dann endet der CGI Interpreter am Ende jeder Seite und mit dem Interpreter werden auch alle geöffneten Dateihandles und damit auch alle Datenbankverbindungen geschlossen - der Clientprozess der Datenbank endet und muss für eine neue Seite neu geladen und gestartet werden.

In solchen Fällen ist die Verwendung eines PHP-Interpreters als Modul vorzuziehen, weil in dieser Konfiguration die mit pconnect() geöffneten Links über die Lebensdauer einer PHP-Seite hinaus gehalten und auf neuen Seiten wiederverwendet werden können.

Die Queries in der Datenbank sind nicht effizient.

Alle Datenbanken haben Werkzeuge zur Analyse von Anfragen. In MySQL ist dies das EXPLAIN Kommando, in Oracle ist es EXPLAIN PLAN . Die Ausgabe dieser Kommandos sollte man in jedem Fall verstehen lernen und zu Rate ziehen. Nur so kann man erkennen, ob Indizes zur Beschleunigung der Query verwendet werden, ob die Typen von Key und Foreign Key zueinander kompatibel sind und ob die Datenbank die richtige Tabelle als treibende Tabelle in einem JOIN verwendet.

Frage: Wie kann ich zwei Tabellen miteinander verknüpfen?

Antwort von Kristian Köhntopp:

Man kann dies mit Hilfe einer JOIN-Operation tun. Diese ist im Kapitel 7.20 des MySQL-Handbuches beschrieben.

Wenn die Tabellen artikel und email als Primärschlüsselfelder artikel.KundenID und email.eid haben und artikel mit email über den Fremdschlüssel email.KundenID verknüpft ist, dann kann man einen Equi-JOIN mit dem folgenden Statement formulieren:

mysql> select * from artikel;
+----------+
| KundenID |
+----------+
|        1 |
|        2 |
|        3 |
+----------+
3 rows in set (0.00 sec)

mysql> select * from email;
+-----+----------+
| eid | KundenID |
+-----+----------+
|   1 |        1 |
|   2 |        2 |
|   3 |        3 |
+-----+----------+
3 rows in set (0.00 sec)

mysql> select a.KundenID as aid,
     >        e.eid as eid,
     >        e.KundenID as e_aid
     >   from artikel as a,
     >     email as e
     > where a.KundenID = e.KundenID;
+-----+-----+-------+
| aid | eid | e_aid |
+-----+-----+-------+
|   1 |   1 |     1 |
|   2 |   2 |     2 |
|   3 |   3 |     3 |
+-----+-----+-------+
3 rows in set (0.01 sec) 

In keinem Fall können in den herangejointen Tabellen Nullwerte enthalten sein.

Diese Operation ist dann effizient, wenn a.KundenID und t.KundenID denselben Typ haben, und auf auf a.KundenID und t.KundenID ein UNIQUE INDEX oder ein INDEX liegen. In MySQL ist ein PRIMARY KEY immer auch ein UNIQUE INDEX.

Wenn man optionale Werte hat, dann kann man keinen symmetrischen Join (Equijoin) mehr machen, sondern muss einen asymmetrischen Join (Left Join) durchführen. Dadurch können auf der rechten Seite Nullwerte entstehen:

mysql> select * from telefon;
+-----+----------+
| tid | KundenID |
+-----+----------+
|   1 |        1 |
|   2 |        3 |
+-----+----------+
2 rows in set (0.00 sec)


Equijoin (es fehlt KundenID 2, weil keine
Telefonnummer definiert ist):

mysql> select a.KundenID as aid,
     >        e.eid as eid,
     >        e.KundenID as e_aid,
     >        t.tid as tid,
     >        t.KundenID as t_aid
     >   from artikel as a,
     >        email as e,
     >        telefon as t
     > where a.KundenID = e.KundenID
     > and a.KundenID = t.KundenID;
+-----+-----+-------+-----+-------+
| aid | eid | e_aid | tid | t_aid |
+-----+-----+-------+-----+-------+
|   1 |   1 |     1 |   1 |     1 |
|   3 |   3 |     3 |   2 |     3 |
+-----+-----+-------+-----+-------+
2 rows in set (0.02 sec)


Left Join (generiert Nullwerte):

mysql> select a.KundenID as aid,
     >        t.tid as tid,
     >        t.KundenID as t_aid
     >   from artikel as a left join telefon as t
     >        on a.KundenID = t.KundenID;
+-----+------+-------+
| aid | tid  | t_aid |
+-----+------+-------+
|   1 |    1 |     1 |
|   2 | NULL |  NULL |
|   3 |    2 |     3 |
+-----+------+-------+
3 rows in set (0.00 sec)


Unterschiedliche Counts:

mysql> select count(a.KundenID) as acount,
     >        count(t.KundenID) as tcount
     >   from artikel as a left join telefon as t
     >        on a.KundenID = t.KundenID;
+--------+--------+
| acount | tcount |
+--------+--------+
|      3 |      2 |
+--------+--------+
1 row in set (0.01 sec) 

Die Tabelle a ist hier die aufspannende Tabelle, die Tabelle t ist die aufgespannte Tabelle. An den Stellen, an denen t keine zu a passenden Werte hat, tauchen Nullwerte in t auf. Da die Relation nun nicht mehr symmetrisch ist, muss man zwischen a.KundenID und t.KundenID unterscheiden. Insbesondere sind die count() -Werte beider Spalten unterschiedlich.

Da a.KundenID und t.KundenID unterschiedlich sind, muss man auch zwingend mit qualifizierten Namen arbeiten und kann nicht mehr einfach KundenID schreiben.

Ein gemischter Join verwendet Equijoins und Left Joins, wie es gerade passt:

mysql> select a.KundenID as aid,
     >        e.eid as eid, e.KundenID as e_aid,
     >        t.tid as tid, t.KundenID as t_aid
     >   from artikel as a,
     >        email as e left join telefon as t
     >        on a.KundenID = t.KundenID
     >  where a.KundenID = e.KundenID;
+-----+-----+-------+------+-------+
| aid | eid | e_aid | tid  | t_aid |
+-----+-----+-------+------+-------+
|   1 |   1 |     1 |    1 |     1 |
|   2 |   2 |     2 | NULL |  NULL |
|   3 |   3 |     3 |    2 |     3 |
+-----+-----+-------+------+-------+
3 rows in set (0.01 sec) 

Frage: Was ist Aggregation? Was ist GROUP BY?

Antwort von Kristian Köhntopp:

Mit Hilfe der GROUP BY -Clause kann man in SQL Daten aggregieren, also Äquivalenzklassen über den gefundenen Elementen bilden und mit den so gefundenen Teilmengen arbeiten.

Gegeben sei eine Menge von Tupeln, etwa (1, 2) , (1, 3) , (2, 3) , (2, 2) , (3, 17) , (2, 21) . Man kann diese Menge jetzt in Teilmengen unterteilen, das wäre dann in der Mathematik eine Relation. Die Elemente, die gemeinsam in einer Teilmenge stehen, stehen dann in einer Relation zueinander .

Eine Relation ist zum Beispiel kleiner als x . Nimmt man zum Beispiel die Menge N (natürliche Zahlen) und die Relation kleiner als 10 , dann teilt diese Relation die Menge N in zwei Teilmengen: Die Menge der natürlichen Zahlen, die die Relation erfüllen (also die Zahlen 0, 1, 2, 3, ..., 9) und die Menge der natürlichen Zahlen, die die Relation nicht erfüllen (die Zahlen 10, 11, ...).

Ebenso kann man eine Äquivalenzrelation definieren. Eine solche Relation definiert mehrere Teilmengen und die Elemente einer Teilmenge sind gleich. In N mit == als Relation ist das witzlos, da die Teilmengen dann einelementig sind, aber mit den o.a. Tupeln kann man ein sinnvolles Beispiel definieren, wenn man als Äquivalenzrelation Gleichheit des ersten Elementes definiert. Man bekommt dann die folgenden Teilmengen:

Die Menge 1 == { (1, 2), (1, 3) }
Die Menge 2 == { (2, 3), (2, 2), (2, 21) }
Die Menge 3 == { (3, 17) } 

Angenommen, die Tupel seien eine Tabelle

CREATE TABLE beispiel (
  x integer,
  y integer
); 

dann würde man die o.a. Tupel als

INSERT INTO beispiel (x, y) values (1, 2);
INSERT INTO beispiel (x, y) values (1, 3);
INSERT INTO beispiel (x, y) values (2, 3);
INSERT INTO beispiel (x, y) values (2, 2);
INSERT INTO beispiel (x, y) values (2, 21);
INSERT INTO beispiel (x, y) values (3, 17); 

definieren und bekäme die Äquivalenzrelation aus dem Beispiel als

SELECT x AS mengenname FROM beispiel GROUP BY x;
+------------+
| mengenname |
+------------+
|          1 |
|          2 |
|          3 |
+------------+
3 rows in set (0.01 sec) 

d.h. die Tupel (x, y) mit gleichem x bilden jeweils eine Menge. Wir sehen uns von diesen Tupeln jeweils nur die x an.

Die Mächtigkeit der Mengen 1, 2 und 3 kann man mittels count() bestimmen:

SELECT x AS mengenname, COUNT(x) AS maechtigkeit
  FROM beispiel
  GROUP BY x;
+------------+--------------+
| mengenname | maechtigkeit |
+------------+--------------+
|          1 |            2 |
|          2 |            3 |
|          3 |            1 |
+------------+--------------+
3 rows in set (0.00 sec) 

Man kann sich auch das Tupel (x, y) wieder ausgeben lassen:

SELECT x, y FROM beispiel GROUP BY x;
+------+------+
| x    | y    |
+------+------+
|    1 |    2 |
|    2 |    3 |
|    3 |   17 |
+------+------+
3 rows in set (0.00 sec) 

MySQL wählt hier irgendein y , da ja per Definition alle (x, y) innerhalb einer Teilmenge gleich sind und jedes Element der Teilmenge daher als Repräsentant der Teilmenge gewählt werden kann.

Frage: Was ist der Unterschied zwischen connect und pconnect?

Antwort von Kristian Köhntopp:

In PHP bieten die meisten Datenbanken zwei connect() -Funktionen an: Eine gewöhnliche und eine pconnect() -Funktion. Verwendet man CGI PHP, unterscheiden sich beide Funktionen nicht.

Verwendet man das PHP-Modul, werden die mit einem connect() hergestellten Datenbankverbindungen am Ende der Seite geschlossen. Mit pconnect() hergestellte Verbindungen bleiben jedoch geöffnet. Dies dient einzig und alleine dazu, das ständige Öffnen und Schließen von Netzwerkverbindungen zu vermeiden, denn der Verbindungsaufbau ist bei einigen Datenbanken (etwa Oracle) sehr aufwendig.

Es ist daher empfehlenswert, in jedem Fall die pconnect() -Variante zu verwenden (aber: Vergleiche Webserver verstehen und tunen . Es können sehr viele offene Datenbankverbindungen entstehen). Siehe auch: PHP Manual, Persistente Datenbankverbindungen .

Frage: Wie kann ich mein Datenbankpasswort gegen Spionage sichern?

Antwort von Kristian Köhntopp:

Viele PHP-Scripte enthalten Connectinformationen mit Passworten und anderen wichtigen Daten, die nicht in falsche Hände fallen dürfen. Um diese Scripte gegen Zugriff von außen (über den Webserver) zu sichern, gibt es folgende Maßnahmen:

    • Die beste Lösung besteht darin, die Connectinformation in einer Include-Datei außerhalb des Webdateibaumes zu lagern. Wenn der eigene Webserver zum Beispiel die Document-Root /home/www/servers/www.kundenname.de/pages hat, man eigene Dateien jedoch schon ab der Ebene /home/www/servers/www.kundenname.de ablegen darf, dann ist es sinnvoll, sich ein Includeverzeichnis /home/www/servers/www.kundenname.de/php zu definieren und dort die Includedateien mit dem Loginnamen und Passwort abzulegen.

      Da dieses Include-Verzeichnis außerhalb der Document-Root liegt und keine URL hat, kann es auch nicht über den Webserver abgerufen werden.

    • Die nächstschlechtere Lösung besteht darin, ein Verzeichnis unterhalb der Document-Root anzulegen und dieses Verzeichnis mit einem Passwort zu sichern. Niemand, auch der Eigentümer des Servers, muss auf dieses Verzeichnis per HTTP zugreifen können, sondern Wartung erfolgt in der Regel per FTP - es kann also auch ein ungültiges Passwort definiert werden.

    • Die schlechteste Lösung besteht darin, diese Include-Datein mit der Endung .php im normalen Dokumentenbaum zu hinterlegen und darauf zu vertrauen, dass Dateien mit dieser Endung immer geparsed werden. Während dies im Normalbetrieb immer der Fall ist, braucht der Webserver nur einmal ohne PHP-Modul gestartet zu werden und die Datei wird im Klartext ausgeliefert (näheres in " php-code ").

Die connect()-Funktionen in PHP verlangen alle, dass das Datenbankpasswort im Klartext angegeben wird. Das bedeutet, dass das Passwort entweder im PHP-Code im Klartext angegeben ist oder vom Code in Klartext entschlüsselt werden kann. Wenn jemand die Dateien mit den Passworten oder dem Entschlüsselungscode lesen kann, dann bedeutet dies auch, dass das Klartextpasswort dieser Person bekannt wird. Die betreffende Person braucht den Entschlüsselungscode nicht zu verstehen - sie braucht ihn nur auszuführen und er wird zwangsläufig das Klartextpasswort passend für die Connect-Funktion liefern müssen.

Daraus folgt, dass ein Schutz der Datenbankpassworte nur durch einen Schutz der entsprechenden Quelltextdateien möglich ist. Es ist Aufgabe des Hosting-Environments beim Provider, diesen Schutz zu bieten, indem entweder Zugriffsrechte an den Dateien entsprechend gesetzt sind oder indem sogar eine virtuelle Dateiumgebung mit chroot() eingerichtet wird.

Frage: MySQL oder PostgreSQL?

Antwort von Kristian Köhntopp:

von Kristian Köhntopp, Lutz Donnerhacke und Sebastian Bergmann.

MySQL ist eine sehr populäre Datenbank, die sich vor allen Dingen durch Geschwindigkeit und geringen Speicherverbrauch sowie durch einfache Handhabung auszeichnet. MySQL verfügt über eine sehr gute Dokumentation , ist auch für die Windows-Plattform verfügbar und seit Ende Juni 2000 unter der GPL frei verfügbar. Seit Version 3.23.16 gibt es experimentellen Support für Transaktionen auf Basis der Sleepycat DB3-Bibliothek, aber noch keine Trigger oder Rules.

Das Buch MySQL von Paul DuBois ( englische Version ) erläutert die Datenbank umfassend und enthält eine Reihe von allgemeinen und speziell auf MySQL bezogene Optimierungstips. Mit phpMyAdmin von Tobias Ratschiller existiert eine einfach zu bedienende, in PHP geschriebene Administrationsoberfläche für MySQL.

PostgreSQL ist der großteils geglückte Versuch, eine freie Implementation von SQL92 aus einem SQL-fremden Konzept (Ingres) abzuleiten. Dazu gehören Transaktionen in verschiedenen Abschottungsgraden, Subselects, eigene Datentypen, Operatoren und Aggregatfunktionen, Trigger, Rules ('Trigger', die in die Optimierungsplanung eingehen) und Views. Es ist somit möglich, dass die Datenbank die Konsistenz des Datenbestandes aus sich heraus erzwingt und so Direktzugriffe ohne korrigierende Frontends gestattet. Die Geschwindigkeit von PostgreSQL ist dadurch allerdings vermindert. Verzichtet man auf Datanbankkonsistenz auf der Platte im Falle von OS-Abstürzen, so wird PostgreSQL deutlich schneller. Ebenso wie MySQL fehlen auch PostgreSQL noch einige elementare Funktionen zur vollen SQL92-Kompatibilität; im Falle von PostgreSQL sind dies zum Beispiel Outer Joins. Die Möglichkeiten von PostgreSQL machen es notwendig, die Datenbank vorab sorgfältig zu planen.

Inzwischen existiert mit phpPgAdmin eine von Dan Wilson nach PostgreSQL portierte Version von phpMyAdmin.

Eine ausführliche Gegenüberstellung der beiden Datenbanken bietet MySQL im Vergleich mit PostgreSQL aus dem MySQL-Handbuch .

Frage: Wie komme ich bei meinem Provider an die Datenbank?

Antwort von Kristian Köhntopp:

Solche Fragen klärt man am Besten mit dem Telefonsupport des betreffenden Providers.

    • Puretec 1&1:

      mysql_connect("dbxy.puretec.de", "Nummer", "Passwort");

      Dabei steht xy für die Nummer des Datenbankservers, die im 1&1-Online-Konfigurationsmenü unter "Zugangsdaten -> Datenbank-Zugriff -> Host" zu finden ist.

    • Strato:

      mysql_connect("rdbms.strato.de", "www.domainname.de", "Datenbankpasswort");

      Eine neue Datenbank und das dazugehörige Passwort lassen sich im Kundenbereich einrichten.

Frage: Wie kann ich auf einen ODBC-Server (MSSQL, Access) zugreifen?

Antwort von Kristian Köhntopp:

In Windows kann man einfach den mitgelieferten ODBC-Treiber verwenden. Eine Beschreibung befindet sich im PHP-Manual .

In Unix kann man für den Zugriff auf einen Microsoft SQL Server den Sybase-CT Treiber verwenden, der ein weitgehend kompatibles Protokoll verwendet. Sybase bietet eine frei verfügbare Version der benötigten Bibliotheken für Linux zum Download an.

Alternativ kann man auch einen kommerziellen ODBC-Treiber für Unix verwenden, etwa den Treiber von OpenLink Software oder den iODBC-Treiber, der dem Adabas-Paket für Suse Linux beiliegt.

Frage: Wieso wird aus " plötzlich \" und wie geht das wieder weg?

Antwort von Johannes Frömter:

Das sind Escapes, die vor bestimmten Sonderzeichen stehen, um diese zu "entschärfen". Verantwortlich für dieses Verhalten ist die Funktion magic_quotes von PHP, die üblicherweise in der php.ini eingestellt wird. Dabei gilt magic_quotes_gpc für Daten, die per GET, POST oder COOKIE übergeben werden und magic_quotes_runtime für Daten, die aus Datenbanken, Dateien oder anderen externen Quellen kommen. Escaped werden ' (single quote), " (double quote), \ (backslash) und NUL (das Null-Byte).

Um die Escape-Zeichen wieder zu entfernen, benutzt man stripslashes() ; manuell hinzufügen kann man sie mittels addslashes() .

Die Konfiguration von magic_quotes kann man an verschiedenen Stellen beeinflussen:

php.ini:
magic_quotes_runtime = on|off
magic_quotes_gpc     = on|off

.htaccess, httpd.conf:
php_flag magic_quotes_runtime on|off
php_flag magic_quotes_gpc     on|off

.php
ini_set("magic_quotes_runtime", 0|1);
// magic_quotes_gpc geht hier nicht 

In Verbindung mit Sybase-Datenbanken (d.h. bei zusätzlich gesetzter Option magic_quotes_sybase ) gibt es eine Besonderheit: Hier werden single quotes nicht mit einem Backslash, sondern mit einem weiteren single quote escaped.

Frage: Warum soll ich nicht SELECT * schreiben?

Antwort von Johannes Frömter:

Bei der SQL-Anweisung SELECT * FROM ... muss das Datenbank-Management-System (DBMS) alle Spalten der betreffenden Datensätze selektieren, auch wenn in der anschließenden Verarbeitung nur ein Teil davon wirklich gebraucht wird. Das ist langsam und schlicht und einfach unsinnig, und die unnötigen Spalten verhindern unter Umständen, dass der integrierte Optimizer die Query effizient ausführen kann.

Selbst wenn alle Spalten tatsächlich benötigt werden, sollten sie separat aufgeführt werden, weil

    • die Tabelle nachträglich erweitert werden könnte, die neue(n) Spalte(n) (im worst case ein BLOB!) nach der Abfrage aber nicht gebraucht werden

    • die Reihenfolge der Spalten bei der Ausgabe sonst undefiniert ist (bei den meisten Datenbanken ist es die Reihenfolge der Spaltendefinition bei der Anlage der Tabelle). Diese Reihenfolge könnte sich ändern (z.B. durch Einspielen eines Backups nach Erweiterung der Tabelle, durch eine neue Version des DBMS, etc.)

    • die Spalten sonst möglicherweise keinen vernünftigen oder eindeutigen Namen haben. Führt man die Spalten einzeln an, kann man mittels AS einen Namen (Alias) vergeben: SELECT p.pers_p_nr AS personalnummer FROM personal p ORDER BY personalnummer

    • dadurch im Script quasi automatisch dokumentiert wird, welche Spalten anschließend verarbeitet werden

Im MySQL-Manual wird in den Beispielen der Einfachheit halber fast immer SELECT * verwendet. Daran darf man sich jedoch für die Praxis kein Beispiel nehmen, sagt auch das Handbuch explizit - Zitat: " You should NEVER, in an application, use SELECT * and retrieve the columns based on their position, because the order in which columns are returned CANNOT be guaranteed over time; A simple change to your database may cause your application to fail rather dramatically. "

Auch für INSERT gilt: immer alle Spaltennamen angeben! Statt INSERT INTO tabelle VALUES (1, 2, 3) ist also INSERT INTO tabelle (spalte1, spalte2, spalte3) VALUES (1, 2, 3) zu schreiben.

Frage: Wie bekomme ich den letzten Datensatz aus der Tabelle?

Antwort von Johannes Frömter:

In einer relationalen Datenbank gibt es keine Reihenfolge, also keine "letzte Zeile", kein "oben" oder "unten". Jedwede definierte Reihenfolge entsteht erst beim Selektieren von Datensätzen und der Sortierung nach irgendeinem Sortierkriterium.

Um also den "ersten" oder "letzten" Datensatz selektieren zu können, muss die Tabelle eine Spalte vom Typ TIMESTAMP haben, die manuell oder automatisch auf das Eintrags- bzw. Änderungsdatum gesetzt wird. Anhand dieser Spalte kann das Ergebnis sortiert werden ( ASC sortiert aufsteigend, DESC absteigend). Mit LIMIT wird das Ergebnis dann auf den ersten (oder die ersten n) Datensätze beschränkt:

SELECT xy FROM tabelle
ORDER BY datum DESC
LIMIT 1 

Wenn das DBMS Subselects zulässt (für MySQL ist dies ab Version 4.x geplant), kann man alternativ auch folgendes schreiben:

SELECT xy FROM tabelle
WHERE datum = (SELECT MAX(datum) FROM tabelle) 

Damit diese Selects effektiv vonstatten gehen, muss auf die Timestamp-Spalte ein Index gelegt werden. Die Verwendung eines Primärschlüssel- statt Timestamp-Feldes ist nicht empfohlen.

Frage: Meine IDs haben Lücken - wie vergebe ich sie neu?

Antwort von Johannes Frömter:

Die IDs einer AUTO_INCREMENT-Spalte haben den Sinn, jede Zeile in der Tabelle, d.h. jeden Datensatz, eindeutig zu kennzeichnen. Durch Löschen von Datensätzen entstehen zwar "Lücken" in der Nummerierung, da für neue Datensätze stets eine unverbrauchte ID vergeben wird. An dieser vermeintlichen "Unordnung" sollte man aber auf keinen Fall etwas ändern - dies würde dem Prinzip der Eindeutigkeit zuwiderlaufen.

Der höchste Wert der ID hat auch nichts mit der Anzahl der Datensätze in der Tabelle zu tun. Diese ermittelt man mit einer SQL-Funktion: SELECT COUNT(*) AS anzahl FROM tabelle

MySQL kennt (noch) keine referentielle Integrität, deshalb muss man selbst dafür sorgen, dass Verknüpfungen zwischen Tabellen (Primary/Foreign Keys) konsistent bleiben. Deshalb: Finger weg von den IDs! Bei geeigenter Wahl des Datentyps braucht man auch keine Sorge zu haben, dass Zahlenraum knapp werden könnte. Siehe auch: " mysql-inkrement ".

Frage: Meine Datenbankabfrage funktioniert nicht

Antwort von Clemens Koppensteiner:
Fehlermeldungen

Als erster Anhaltspunkt sind die Fehlermeldungen der verwendeten Datenbank gut geeignet. Für viele Datenbanken bietet PHP hierfür eigene Funktionen. Dies sind zum Beispiel mysql_error() , pg_last_error() , ora_error() , odbc_errormsg() und sybase_get_last_message() .

Hier ein Beispiel für MySQL

function mysql_errorhandler($problem, $query = "")
{
  echo "<font color='#FF0000'><b>Datenbankfehler:</b></font><br />\n";
  echo "Problem: $problem <br />\n";
  if($query != "")
  {
    echo "Query: $query <br />\n";
  }
  echo "MySQL: " . mysql_errno() . " - " . mysql_error() . "<br /><br />\n";
}

// Verbindung zum Datenbankserver herstellen
if(!$db = @mysql_connect("host", "user", "password"))
{
  mysql_errorhandler("Verbindungsaufbau gescheitert.");
}

// Datenbank auswählen
if(!@mysql_select_db("database"))
{
  mysql_errorhandler("Auswahl der Datenbank gescheitert.");
}

// Beispiel für ein SQL-Statement
$query = "SELECT * FROM table WHERE x = '$x'";
$result = @mysql_query($query);

if(!$result)
{
  mysql_errorhandler("Datenbankabfrage gescheitert", $query);
} 

Klassen zur Datenbankabstraktion (z.B. PEAR :: DB ) können das Fehler-Handling wesentlich vereinfachen.

Reservierte Wörter

Bestimmte Wörter sollten nicht als Namen von Feldern vorkommen, da sie andere Bedeutungen haben. Diese "Reserved Words", oder auch "Keywords", sollten im Handbuch der verwendeten Datenbank aufgelistet sein.

Bei MySQL und PostgreSQL kann man das Problem umgehen, indem man den Feldnamen in Anführungszeichen einschließt:

SELECT * FROM users WHERE 'group' = 2 
Zuständige Newsgroups

Falls das alles nichts hilft, frage in den zuständigen Newsgroups nach. Das sind unter anderem de.comp.datenbanken.misc , de.comp.datenbanken.mysql und die englischsprachigen Gruppen comp.databases.*

Frage: Wie kann ich bösartigen Code in SQL-Abfragen unterbinden?

Antwort von Alex Kiesel:

In nahezu allen Einsatzgebieten einer Datenbank wird die Datenbank durch ein bestimmtes Benutzerinterface (z.B. über das Web) abgefragt und auch gefüllt. Oft darf ein bestimmter Benutzer nicht alles sehen, was sich in der Datenbank befindet - und schon gar nicht ändern oder löschen. Falsch programmierte Scripte ermöglichen jedoch genau dies - dabei ist ein Schutz gegen die als SQL-Injection bekannte Technik gar nicht so schwer. Auf die Prüfung der übergebenen Parameter kommt es an. (Siehe dazu auch sicherheit-parameter ).

Zunächst mal ein Beispiel, wie eine solche SQL-Injektion aussehen könnte: in einer Benutzerverwaltung darf ein User genau sein eigenes Passwort ändern. Wir haben also die Tabelle "account", die insbesondere die Logindaten des Users und des Admins enthält. Der Benutzer sendet das Passwort-Ändern-Formular in seinem Browser ab und serverseitig wird das neue Passwort in die Datenbank geschrieben:

$result= $db->update ('
    update account
    set password= "'.$_REQUEST['newpassword'].'"
    where username= "'.$_REQUEST['username'].'"'
  ); 

Hier wird die Variable $_REQUEST['newpassword'] nicht geprüft - der Benutzer kann eintragen, was er möchte. Mit etwas Trial-and-Error findet der Benutzer heraus, dass er die doppelten Anführungszeichen (") als String-Delimiter benutzen muss; er gibt nun ein Passwort seiner Wahl und als Username userxy" or username="admin . Somit ergibt sich nach der Textersetzung folgendes SQL:

update account
  set password= "adminsuxx"
  where username="userxy" or username="admin" 

Es kommt hier also hochgradig darauf an, niemals fremden Parametern zu trauen. Auf was muss also getestet werden? Parameter, die innerhalb von Quotes als String an die Datenbank übergeben werden sollen, dürfen selbst keine ungeschützten Quotes enthalten. Parameter, die IDs oder Zahlen repräsentieren, dürfen nur Ziffern enthalten. Die beiden Funktionen sichern genau dies, und sorgen zudem dafür, dass NULL-Values korrekt wiedergegeben werden:

function sqlSafeString($param) {
    // Hier wird wg. der grossen Verbreitung auf MySQL eingegangen
    return (NULL === $param ? "NULL" : '"'.mysql_escape_string ($param).'"');
  }

  function sqlSafeInt($param) {
    return (NULL === $param ? "NULL" : intVal ($param));
  } 

(Die genauen Vorgaben zum Schützen von Sonderzeichen hängen vom SQL-Dialekt des RDBMS ab und müssen gegebenenfalls angepasst werden.) Man hätte so oben beschriebenes SQL so umschreiben können:

$result= $db->update ('
    update account
    set password= '.sqlSafeString ($_REQUEST['newpassword']).'
    where username= '.sqlSafeString ($_REQUEST['username'])
  ); 

Obiger SQL-Injection-Versuch wäre dann gescheitert (weil keine Zeile auf diesen Usernamen matcht):

update account
  set password= "adminsuxx"
  where username="userxy\" or username=\"admin\"" 

Eine Funktion, die komfortable SQL-Manipulation mit automatischem Escaping bietet, wird in sybase-quoting vorgestellt; sie ist zwar für Sybase geschrieben worden, lässt sich aber leicht für andere Datenbanksysteme adaptieren.