Wegen Performance auf Normalisierung verzichten?
Christian Seiler
- datenbank
Hallo Forum,
Erst einmal: Ich stehe bei MySQL vor genau dem Problem, was in http://www.mysql.de/doc/de/example-Maximum-column-group-row.html beschrieben wird.
Ich programmiere gerade mein eigenes Forum/Board mit PHP und MySQL. [1]
Dabei will in der Board-Ansicht ich das Datum des letzten Postings (mit GROUP BY kein Problem) und den Autor des letzten Postings anzeigen. Und hier ist der Knackpunkt: Das, was im MySQL-Manual als ausweg beschrieben wird, gefällt mir alles nicht. Ich setze nämlich so im Endeffekt eine ganze Menge Datenbankabfragen an die Datenbank ab. (Da ich PHP verwende, muss ich für jeden Befehl ein mysql_query starten) Das kostet Performance. Ich habe an einigen anderen Stellen die Anzahl der DB-Abfragen, die das Script produziert, von 22 auf 10 reduziert; und 10 kommt mir schon ziemlich viel vor. (und bei dieser Reduzierung war ein _deutlicher_ Performanceanstieg spürbar gewesen)
Dann kommt wieder so ein Punkt: Bei der Forumsansicht hole ich mir einfach die Liste aller Postings und bau mir dann im Speicher daraus den Thread-Baum. Das geht recht flott - allerdings möchte ich dem Benutzer auch anbieten, dass er den Thread-Baum anders sortieren kann. (das Sortieren wird in der Datenbank erledigt) Ich will zum Beispiel auch die Möglichkeit anbieten, dass man die Threads nach dem letzten Posting sortieren kann. Mit MySQL könnte ich das auf zwei Weisen lösen: a) ich hole mir zuerst die Thread-Liste mit einem GROUP BY auf die Postings (um das letzte Posting mit MAX(posting_date) zu bekommen) in den Speicher und hole mir dann für _jeden einzelnen Thread_ die Postings noch mal sortiert - oder b) ich hole mir wieder die Threadliste und dann _alle_ Postings auf einmal und sortiere sie dann im Speicher neu. Beide Möglichkeiten gefallen mir ehrlich gesagt überhaupt nicht, denn sie kosten sehr viel Performance, vor allem bei vielen Threads und Postings.
Mein Datenmodell sieht im Moment (auszugsweise) so aus:
threads
-----------
threadid
title
origposter
starting_date
postings
-----------
postingid
title
poster
message
posting_date
threadid
Die Lösung meiner Probleme wäre ein kompletter Bruch mit "gutem" Datenbankdesign: ich werfe die Normalisierung einfach über den Haufen. Wenn ich zu jedem Thread speichere, wann und von wem das letzte Posting "gepostet" worden ist, dann kann die Anzahl der DB-Abfragen, die ich durchführen muss, gering halten. Ich würde damit sogar so weit kommen, dass jetzt pro Scriptaufruf nur noch ca. 8 DB-Abfragen durchgeführt werden müßten, was zwar IMHO immer noch sehr viel ist, aber deutlich besser als vorher.
Die Nachteile, die mir dabei in den Sinn kommen:
1. potentieller Verlust der Integrität der Daten
2. "unelegant"
3. redundante Speicherung von Informationen
Was meint ihr dazu? Oder anders: konnte ich mein Problem verdeutlichen? Habe ich etwas übersehen? Ist mein Datenmodell suboptimal?
Viele Grüße,
Christian
[1] Ich unterlasse jetzt einfach mal die Rechtfertigungen. ;-)
Hallo Christian,
nur so eine fixe Idee:
Die Lösung meiner Probleme wäre ein kompletter Bruch mit "gutem" Datenbankdesign: ich werfe die Normalisierung einfach über den Haufen. Wenn ich zu jedem Thread speichere, wann und von wem das letzte Posting "gepostet" worden ist, dann kann die Anzahl der DB-Abfragen, die ich durchführen muss, gering halten. Ich würde damit sogar so weit kommen, dass jetzt pro Scriptaufruf nur noch ca. 8 DB-Abfragen durchgeführt werden müßten, was zwar IMHO immer noch sehr viel ist, aber deutlich besser als vorher.
Und wenn Du statt dessen nur die ID des letztem Posts mit in den Datensatz reinnimmst?
Gruss, Thoralf
Hallo Thoralf,
Und wenn Du statt dessen nur die ID des letztem Posts mit in den Datensatz reinnimmst?
Auf den ersten Blick: Genial!
Auf den zweiten Blick: Mist, kann nicht funktionieren. Mein SELECT zum auslesen der Postings für die Threadansicht sieht (vereinfacht) so aus:
SELECT
forum_postings.postingid as postingid,
forum_postings.title as title,
forum_postings.poster as poster,
forum_postings.message as message,
forum_postings.posting_date as posting_date,
forum_postings.parentid as parentid,
forum_postings.threadid as threadid
FROM
forum_postings,
forum_threads
WHERE
forum_threads.threadid = forum_postings.threadid
ORDER BY
...
Das Problem hierbei ist, dass ich nicht noch mal auf die forum_postings-Tabelle "zurückjoinen" kann - ich bräuche Subselects, welche es in MySQL nun mal (noch) nicht gibt.
Trotzdem vielen Dank für den Vorschlag.
Viele Grüße,
Christian
Nochmal hallo,
hast Du einfach mal in den gängigen PHP-Boards geschaut, wie das dort gelöst ist? Dürfte gerade im Hinblick auf die Performance aufschlussreich sein.
Und kennst Du diesen Link: http://www.develnet.org/36.html (Das 'Nested Sets' Modell - Bäume mit SQL)
Gruss, Thoralf
Hallo Thoralf,
hast Du einfach mal in den gängigen PHP-Boards geschaut, wie das dort gelöst ist?
Naja, die haben ja auch nur eine Board-Struktur und keine Forum-Struktur.
Und kennst Du diesen Link: http://www.develnet.org/36.html
Kannte ich noch nicht - danke. Allerdings bin ich nicht unbedingt erpicht darauf, mein Datenmodell _so_ radikal umzustellen.
Aaaaaaber:
Der SELECT, der dort verwendet wird:
--------------------------------------
SELECT node1.payload,
COUNT(*) AS level
FROM node AS node1,
node AS node2
[...]
--------------------------------------
hat mir das Brett vorm Kopf gerissen. Ich kann ja eine Tabelle mehrfach verwenden, indem ich einfach mehrere Aliase nehme. So kann ich Deinen Vorschlag mit der ID des letzten Postings realisieren.
Vielen Dank - Du hast mir sehr weitergeholfen!
Viele Grüße,
Christian
Gute Nacht und sehr zum Wohl,
hast Du einfach mal in den gängigen PHP-Boards geschaut, wie das dort gelöst ist?
Naja, die haben ja auch nur eine Board-Struktur und keine Forum-Struktur.
Schon klar, es sind eben grösstenteils Boards. Aber ich habe beim Stöbern in fremdem Code schon oft Anregungen für eigene Ideen gefunden, ohne darauf angewiesen zu sein, diesen Code letztlich zu kopieren. Denkanstösse findet man aber immer wieder einmal.
Und kennst Du diesen Link: http://www.develnet.org/36.html
Kannte ich noch nicht - danke. Allerdings bin ich nicht unbedingt erpicht darauf, mein Datenmodell _so_ radikal umzustellen.
Der Link ist m.E. auch eher für neue Projekte oder eben zum Verständnis gedacht. Ich wollte (und will irgendwann immer noch) ein eigenes Board auf die Beine stellen und da dümpelt dieser Link in meinen Lesezeichen herum. Gerade beim Verständnis des Zusammenwirkens von Nomalisierung (oder Normalisation?) von Tabellen und der berühmten Baumstruktur eines Forums vs. Boards hat mir der Artikel unheimlich geholfen.
hat mir das Brett vorm Kopf gerissen. Ich kann ja eine Tabelle mehrfach verwenden, indem ich einfach mehrere Aliase nehme. So kann ich Deinen Vorschlag mit der ID des letzten Postings realisieren.
Freut mich sehr, genau das war Absicht meines Posting, da wie gesagt, nur eine fixe Idee. Wenn das Board fertig ist, kann man es sicher hier bewundern, trotz der Forumsfetischisten? ;)
Vielen Dank - Du hast mir sehr weitergeholfen!
Immer wieder gern, da das Forum hier seit Jahren immer noch eine zweiseitige Geschichte ist und ich immer wieder wertvolle Tipps, Erfahrungen und Ideen auch von Dir mitnehme. :)
Wein'lich-Juristische Nachtgrüsse,
Thoralf
PS.: Etwaige komische Passagen sind auf [pref:t=43246&m=236408] und insbesondere die Folgebeiträge zurückzuführen. ;)
Hallo Thoralf,
Freut mich sehr, genau das war Absicht meines Posting, da wie gesagt, nur eine fixe Idee. Wenn das Board fertig ist, kann man es sicher hier bewundern, trotz der Forumsfetischisten? ;)
Im Moment ist das Ding noch Teil eines größeren Projektes - aber ich hatte sowieso vor, das mal standalone als OpenSource-Projekt freizugeben - wenn ich die Zeit finde. Ach ja - das Ding wird ein Forum _und_ ein Board. Wenn es nach mir alleine ginge, dann hätte ich nur ein Forum gemacht. ;-) So, ich muss mich jetzt an die Arbeit machen, wenn ich die Features noch fertig bekommen will...
Immer wieder gern, da das Forum hier seit Jahren immer noch eine zweiseitige Geschichte ist und ich immer wieder wertvolle Tipps, Erfahrungen und Ideen auch von Dir mitnehme. :)
Oh - danke. :)
Viele Grüße,
Christian
Hallo Christian!
Die Lösung meiner Probleme wäre ein kompletter Bruch mit "gutem" Datenbankdesign: ich werfe die Normalisierung einfach über den Haufen.
Tja, das habe ich auch schon oft gehört und gemerkt. Schön ist nicht unbedingt schnell. Der größte Teil der Daten dürfte wohl auf die Postings an sich fallen, da macht es IMH überhaupt nichts die ein oder andere kleine Spalte redundant zu speichern - wenn sich dadurch ein bedeutender Performance-Sprung erreichen läßt.
Das Problem ist zwar schon gelöst, aber trotzdem nochwas dazu:
Wenn ich zu jedem Thread speichere, wann und von wem das letzte Posting "gepostet" worden ist, dann kann die Anzahl der DB-Abfragen, die ich durchführen muss, gering halten. Ich würde damit sogar so weit kommen, dass jetzt pro Scriptaufruf nur noch ca. 8 DB-Abfragen durchgeführt werden müßten, was zwar IMHO immer noch sehr viel ist, aber deutlich besser als vorher.
Die Frage ist - wie machst Du Deine Tests? Mit entsprechenden Datenmengen wie es später sein wird? Denn dann kann das ganze auch anders aussehen, vielleicht sind die Flaschenhälse ja woanders als Du bisher glaubst. 8 Datenbank-Abfragen ist sicher nicht viel, die bekannten Boards haben teilweise 50 oder sogar 100 auf einer Seite, und die müssen sehr portabel geschrieben sein, können also nicht kompromisslos auf Performance getrimmt werden wie z.B. das Forum hier. Und wirklich langsam kommen die mir nicht vor - sind natürlich meist nicht so belastet wie das Forum hier!
Jedenfalls kann man IMHO die Performance nicht direkt an der Anzahl der DB-Abfragen festmachen, auch wenn es den Verbindungs-Overhead gibt, aber solange ein Script läuft bleibt die Verbindung eh bestehen, und dass mysql_query() an sich besondern viel kostet habe ich noch nicht bemerkt, es kommt ganz stark auf die Abfrage und deren Verwendung von indices an. Sortieren über die ganze Tabelle ist aber im allgemeinen immer schlecht, ich habe gute Erfahrungen gemacht indem ich die Daten zur Sortierung in einer temporäre HEAP-Tabelle geschrieben habe und darin sortiert habe, das hat bei großen Datenmengen ne Menge gebracht.
Die Nachteile, die mir dabei in den Sinn kommen:
- potentieller Verlust der Integrität der Daten
kommt drauf an wie Du das implementierst
- "unelegant"
ist wohl nicht wirklich wichtig, oder?
- redundante Speicherung von Informationen
Wenn s nur 5% mehr Daten werden ist das IMHO egal, bei 50% würde ich mir vielleicht Gedanken machen, aber auch da - wenn Du auf dem Server eh noch das 100-fache frei hast - who cares?
Viele Grüße
Andreas
Hallo Andreas,
Die Frage ist - wie machst Du Deine Tests?
Ich verwende PEAR::DB - da habe ich einfach die Klasse "manipuliert", dass sie bei jeder Anfrage ein "A" ausspuckt und die Anzahl der "A" habe ich dann per Hand gezählt. :-)
Mit entsprechenden Datenmengen wie es später sein wird?
Leider nicht. Die Datenmengen, die ich im Moment zu bewältigen habe, sind relativ gering.
und dass mysql_query() an sich besondern viel kostet habe ich noch nicht bemerkt,
Es kommt darauf an. Ich habe für das Projekt insgesamt ein Rechtesystem entworfen, das im Prinzip Zugriffskontrolllisten (ACL) umsetzt. Man kann da ziemlich genau festlegen, was jeder tun und lassen darf. Ich habe eine Funktion has_right. Diese Funktion hatte bei jedem Aufruf eine DB-Anfrage produziert. Da kam es schon mal vor, dass so ein Script bis zu 32 DB-Anfragen pro Aufruf produziert hat - und das war mir _definitiv_ zu viel. Dann habe ich eine Art "Cache" eingeführt und die Anzahl der Datenbankabfragen ist bei dem einem Script auf 6 heruntergegangen. Dabei ist das Script _merklich_ schneller geworden.
Danke auf jeden Fall, Du hast mich auf einen wichtigen Punkt hingewiesen, nämlich dass ich kleine Ahnung habe, wie das ganze mit größeren Datenmengen reagieren wird.
Viele Grüße,
Christian
Hi Christian,
Ich verwende PEAR::DB - da habe ich einfach die Klasse "manipuliert", dass sie bei jeder Anfrage ein "A" ausspuckt und die Anzahl der "A" habe ich dann per Hand gezählt. :-)
Wenn es Dir auf Performance ankommt - IMHO kann PEAR::DB ne ganz schöne Bremse darstellen.
Ich habe für das Projekt insgesamt ein Rechtesystem entworfen, das im Prinzip Zugriffskontrolllisten (ACL) umsetzt. Man kann da ziemlich genau festlegen, was jeder tun und lassen darf. Ich habe eine Funktion has_right. Diese Funktion hatte bei jedem Aufruf eine DB-Anfrage produziert.
Mal eine andere Frage. Ich bin auch gerade dabei für ein Projekt global Rechte zu vergeben, und entsprechend für Funktionalitäten... zu prüfen. Ich will wirklich nichts großes, ich muss nicht jede Kleinigkeit festlegen können, ich habe nur 4 Verschiedene Benutzer-Level, die sich wie folgt unterscheiden:
a) Web-Frontend hat andere/weniger Funktionen(andere Navigation...)
b) Bestimmte PHP-Funktionen können nur ausgeführt werden wenn der Benutzer in einem bestimmten der 4 Benutzer-Level ist.
Dazu habe ich noch verschiedene Benutzergruppen - die etwa den Abteilungen innerhalb eines Unternehmens entsprechen, darin gibt es zwar auch Benutzer mit verschiedenem Benutzer-Level, aber alle Benutzer dieser Gruppe haben für sich eigene Daten, also eigene Kundendaten, eigene Projekte... die nur Benutzer dieser Gruppe sehen und von denen wiederum nur Benutzer in dem entsprechenden Level bearbeiten können.
Aber ich weiß nicht so recht wie ich das jetzt einfach und gut implementiere. Ich habe 3 Benutzer-Level:
Level 1: Admin -> kann Benutzer anlegen, kann auf alle Daten schreibend zugreifen...
Level 2: 0815-Benutzer -> kann nur noch auf bestimmte Daten innerhalb seiner Abteilung schreibend zugreifen
Level 3: Gast -> kann nur seine eigenen Einstellungen für die Software verändern und sonst nur lesend auf Daten innerhalb seiner Abteilung zugreifen.
So, das ist es im Prinzip was ich abbilden will. Der "gast" soll jetzt z.B. in der Oberfläche gar keinen Menüpunkt wie "neuer Kunde" mehr bekommen, und beim Anlegen eines neuen Kunden soll trotzdem zur Sicherheit noch die Rechte geprüft werden.
Naja, bisher code ich das ganze eigentlich fest mit ein, ich habe halt eine Tabelle user, in der ich das Benutzerlevel zu einem "user" speichere, und eine Tabelle "groups", in der ich die Zuordnung der User zu den Benutzergruppen speichere. Bei einem Datensatz in der Tabelle "kunde" speichere ich dann die Benutzergruppe desjenigen der ihn angelegt habe, so kann ich bei Zugriff auf diesen Kunden prüfen ob der user in der richtigen Gruppe ist. Bis dahin war es nicht schwer. Schwerer ist das mit den Leveln. Die code ich wie gesagt immer direkt mit ein, also ich schreibe z.B. vor ein UPDATE der Kundentabelle: if($userlevel = 1 || serlevel = 2)...
Halt solche Sachen, aber da bin ich nicht wirklich glücklich mit. Auf der anderen Seite finde ich es übertrieben für jede noch so kleine Aktion Rechte festzulegen und zu prüfen, das wäre IMHO ein ganz schöner overhead, oder?
Die andere genau so blöde Sache ist die Oberfläche. An allen möglichen Stellen sowas wie:
if($userlevel = 1 || serlevel = 2) {
echo "<a href="neuer_kunde.php">neuer Kunde</a>";
}
das ist nicht wirklich schön. Hat jemand vielleicht einen Tipp wie ich zum einen Die Rechteprüfung bei Aktionen vernünftig automatisieren kann, und vielleicht auch wie ich mir die Oberfläche entsprechend den Rechten zusammenbasteln kann? Ich brauche wohl eine Tabelle in die ich alle Aktionen schreiben die beschränkt werden sollen, und da schreibe ich dann die Level rein, also sowas:
aktion | level 1 | level 2 | level 3
------------+---------+---------+--------
neuer_kunde | yes | yes | no
del_kunde | yes | no | no
(ich weiß nicht schön, aber zweckmäßig, oder? So genaue Rechte wie in einem Unix-Filesystem oder sowas brauche ich nicht wirklich)
und dann frage ich halt "select $userlevel from table where aktion = del_kunde", und reagiere entsprechend.
Aber was mache ich mit der Oberfläche? Vielleicht eine extra-Tabelle dafür?
Naja, bin für jeden Tipp dankbar!
Viele Grüße
Andreas
PS: Sorry für diesen vielleicht etwas rabiaten Threrad-Drift ;-)
Hallo Andreas,
Wenn es Dir auf Performance ankommt - IMHO kann PEAR::DB ne ganz schöne Bremse darstellen.
Du könntest Recht haben. Das Problem ist, das Ding ist auch ohne die Abstraktionsfähigkeiten ganz nett...
Aber was mache ich mit der Oberfläche? Vielleicht eine extra-Tabelle dafür?
Also ich habe das bei mir so gelöst: (ich vereinfache hier ziemlich stark, um möglichst nahe an das ranzukommen, was Du willst) Ich habe eine Tabelle global_acl, die sieht so aus:
CREATE TABLE global_acl (
id_type INT(1) UNSIGNED NOT NULL,
id INT(6) UNSIGNED NOT NULL,
right_name VARCHAR(255) NOT NULL,
PRIMARY KEY (id_type, id, right_name)
);
id_type ist entweder 1 für Gruppe oder 0 für Benutzer. ID ist dann die entsprechende Benutzer/Gruppen-ID. Bei Dir bräuchtest Du nur ein Feld: userlevel. right_name ist dann der Name des Rechts.
Ich habe dann zum einen folgende Backend-Funktionen:
has_right ($right_name)
=> hat der aktuelle Benutzer das Recht (geprüft über JOIN mit der Benutzer/Gruppen-Tabelle)
list_rights ()
=> listet alle Rechte in der Form:
array (
0 => array (
'id_type' => 1,
'id' => 1,
'rights' => array (
'rechta',
'rechtb',
'rechtc'
)
),
1 => array (
'id_type' => 1,
'id' => 2,
'rights' => array (
'rechta',
'rechtc',
'rechtd',
'rechte',
'rechtf'
)
)
);
Das ist praktisch für die Anzeigefunktionien (s.u.) Den Teilsourcecode, um so eine Baumdarstellung zu bekommen, hänge ich ans Ende des Postings.
has_rights ()
=> Listet alle Recht des aktuellen Benutzers auf und gibt sie als normales Array zurück: array ('rechta', 'rechtb', 'rechtc') => über JOIN mit der Benutzer/Gruppendatenbank
set_right ($id_type, $id, $right_name)
=> setzt ein Recht
unset_right ($id_type, $id, $right_name)
=> löscht ein Recht
Dann habe ich noch vier weitere Funktionen zur Verwaltung:
display_acl_form ($addurl, $changeurl, $rights_array)
=> zeigt das Rechte-Formular an:
+-----------+----------+------------+------------+-----------+---------+
| | Recht A | Recht B | Recht C | Recht D | Recht E |
+-----------+----------+------------+------------+-----------+---------+
| Gruppe 1 | [ ] | [ ] | [ ] | [X] | [X] |
| Gruppe 2 | [X] | [ ] | [X] | [ ] | [X] |
| Benutzer1 | [ ] | [X] | [ ] | [ ] | [ ] |
+-----------+----------+------------+------------+-----------+---------+
- $addurl ist die URL des Scripts, das display_acl_add_form aufrufen wird. (su.)
- $changeurl ist die URL des Scripts, das bei einem POST-Request process_acl_form aufrufen wird (su.)
- $rights_array sind alle _verfügbaren_ Rechte (array ('rechta', 'rechtb', 'rechtc'))
process_acl_form ($rights_array)
=> Verarbeitet das Rechte-Formular
display_acl_add_form ($addurl, $backurl, $rights_array)
=> Zeit ein Rechte-Hinzufüge-Formular an:
+------------+--------------------------------+
| Typ | [Gruppe_________] |
+------------+--------------------------------+
| Gruppe | [Gruppe 1_______] |
+------------+--------------------------------+
| Benutzer | [Benutzer1______] |
+------------+--------------------------------+
| Rechte | [ ] Recht A |
| | [ ] Recht B |
| | [ ] Recht C |
| | [ ] Recht D |
| | [ ] Recht E |
| | [ ] Recht F |
| | [ ] Recht G |
+------------+--------------------------------+
- $addurl ist die URL des Scripts, das bei einem POST-Request process_acl_add_form aufrufen wird (su.)
- $backurl ist die URL des Scripts, das wieder display_acl_form aufrufen wird
- $rights_array sind wieder alle Rechte
process_acl_add_form ($rights_array)
=> verarbeitet das Hinzufügen-Formular
Bei den Formularen sind jeweils immer noch Links zum Springen zum anderen Formular gegeben, daher die einen URL-Parameter, die anderen URL-Parameter sind für den Wert des action-Attributs des Formulars zuständig.
Wie oben erwähnt kommt hier noch ein Teilsourcecodeauszug aus list_rights; diese Struktur ist enorm hilfreich, wenn man dieses Tabellenformular anzeigen will: Man geht erst alle Zeilen durch; geht dann die Liste mit allen Rechten durch und prüft einfach mit in_array, ob das Recht auch für diese Zeile vorhanden ist.
Beachte, dass die Sortierung in der Datenbank richtig abgestimmt sein muss, sonst funktioniert es nicht.
$rows = array ();
$cur_row = null;
$cur_rights = null;
while ($row = $res->fetchRow(DB_FETCHMODE_ASSOC)) {
// if its the first time
if ($cur_row === null) {
$cur_row = array ('id' => $row['id'], 'id_type' => $row['id_type']);
$cur_rights = array ($row['right_name']);
// else if it has changed
} else if ($cur_row['id'] != $row['id'] || $cur_row['id_type'] != $row['id_type']) {
$cur_row['rights'] = $cur_rights;
$rows[] = $cur_row;
$cur_row = array ('id' => $row['id'], 'id_type' => $row['id_type']);
$cur_rights = array ($row['right_name']);
} else {
$cur_rights[] = $row['right_name'];
}
}
$cur_row['rights'] = $cur_rights;
$rows[] = $cur_row;
Ich hoffe, das hat Dir ein bisschen einen Denkansatz gegeben. :-)
Ach ja: Eventuell erscheinen einige Funktionen oder Parameter überflüssig - das liegt an der Vereinfachung. Das ganze ist nämlich bei mir noch etwas komplexer.
Viele Grüße,
Christian
Hallo Andreas,
vielleicht sind die Flaschenhälse ja woanders als Du bisher glaubst.
Nachtrag: (falls es jemanden interessiert)
Du hast vollkommen Recht: Ich habe gerade mal ein paar Tests durchgeführt: (mit getmicrotime) Die meiste Zeit vergeudet das Script beim Anzeigen der Template. Ich muss also dort noch kräftig optimieren.
Viele Grüße,
Christian