874 lines
54 KiB
TeX
874 lines
54 KiB
TeX
% !TeX root = ../../thesis.tex
|
|
|
|
\chapter{Performance-Untersuchung der Anwendung}
|
|
\label{ch:performance-investigation-application}
|
|
|
|
Nun werden die unterschiedlichen Schichten betrachtet und mögliche Performance"=Verbesserungen untersucht und deren
|
|
Vor"= und Nachteile herausgearbeitet.
|
|
|
|
Für die Tests wird ein aktuelles Manjaro"=System mit frisch installierten Payara als Serverhost und der IntelliJ IDEA
|
|
als Entwicklungsumgebung verwendet. Der Computer ist mit einer Intel CPU i7-12700K, 32 GB Arbeitsspeicher und einer SSD
|
|
als Systemfestplatte ausgestattet.
|
|
|
|
Zur ersten Untersuchung und der Bestimmung der Basis-Linie, wurde das Script ohne eine Änderung am Code und der
|
|
Konfiguration mehrfach aufgerufen. Hierbei hat sich gezeigt, dass der erste Aufruf nach dem Deployment circa 1500 ms
|
|
gedauert hat. Die weiteren Aufrufe benötigen im Durchschnitt noch 600 ms. Beim achten Aufruf des Scripts hat der
|
|
Server nicht mehr reagiert und im Log ist ein \textit{OutOfMemoryError} protokolliert worden.
|
|
|
|
Nach einem Neustart des Servers, konnte das gleiche Verhalten wieder reproduziert werden. Daraufhin wurde das Test"=Script
|
|
um die Anzeige der aktuellen Speicherverwendung des Payara"=Servers erweitert um diese zeitgleich zu beobachten. Diese
|
|
Auswertung zeigte, dass der Server mit circa 1500 MB RSS Nutzung an seine Grenzen stößt. Diese Grenzen wurde durch die
|
|
Konfigurationsänderung im Payara-Server von \texttt{-Xmx512m} auf \texttt{-Xmx4096m} nach oben verschoben. Nun werden
|
|
circa 60 Aufrufe des Scripts benötigt, damit der Server nicht mehr reagiert. Hierbei wird aber kein \textit{OutOfMemoryError}
|
|
in der Log-Datei protokolliert und der Server verwendet nun circa 4700 MB RSS. Bei allen Tests war noch mehr als die
|
|
Hälfte des verfügbaren Arbeitsspeichers des Computers ungenutzt.
|
|
|
|
Mit der Konfiguration \texttt{-Xmx} wird der maximal verwendbare Heap"=Speicher in der \ac{JVM} definiert.
|
|
Dies zeigt direkt, dass es ein Problem in der Freigabe der Objekte gibt, da dass Erhöhen des verwendbaren
|
|
Arbeitsspeicher das Problem nicht löst, sondern nur verschiebt.
|
|
|
|
Für alle nachfolgenden Messungen wird das Skript im \autoref{ap:calling_script} verwendet, welches die einzelnen
|
|
Aufrufe steuert. Die Ergebnisse werden in eine Tabelle überführt, wie in \autoref{tbl:measure-without-cache}.
|
|
Hierbei werden die Aufrufzeiten der Webseite aus dem Skript für die Zeitmessung mit Mindest"~, Durchschnitt"~ und
|
|
Maximalzeit aufgenommen, hierbei ist eine kürzere Zeit besser. Zusätzlich wird die Anzahl der aufgerufenen SQL Abfragen
|
|
ermittelt, auch hier gilt, dass weniger Aufrufe besser sind. Als letztes wird noch der verwendete Arbeitsspeicher
|
|
vom \textit{Glassfish}"=Server vor und nach dem Aufruf ermittelt und die Differenz gebildet, hierbei sollte im besten
|
|
Fall die Differenz bei 0 liegen. Dieser Aufbau gilt für alle weiteren Messungen. Zusätzlich werden noch die Laufzeiten
|
|
der \ac{JSF} ermittelt und die durchschnittlichen Zeiten mit in der Tabelle dargestellt, und auch hier ist es besser,
|
|
wenn die Zeiten kürzer sind.
|
|
|
|
Als Grundlage für die Vergleiche wurde eine Messung durchgeführt, bei welcher alle Caches deaktiviert wurden und keine
|
|
Änderung am Code vorgenommen wurde. Das Ergebnis dieser Messung ist in \autoref{tbl:measure-without-cache} zu finden.
|
|
Diese zeigen auch direkt ein zu erwartendes Ergebnis, dass der erste Aufruf bedeutend länger dauert als die Nachfolgenden.
|
|
Ebenfalls sieht man eindeutig, dass die Anzahl der Anfragen nach dem ersten Aufruf immer die gleiche Anzahl besitzen.
|
|
Der Speicherbedarf steigt relative gleichmässig, was nicht recht ins Bild passt, da hier keine Objekte im Cache
|
|
gehalten werden sollten.
|
|
|
|
\begin{table}[!h]
|
|
\centering
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & & \multicolumn{3}{c|}{RSS (MB)} \\
|
|
\hline
|
|
\# & min & avg & max & Queries & davor & danach & diff \\
|
|
\hline
|
|
1 & 395 & 578 & 1312 & 12237 & 747.15 & 924.88 & 177.73 \\
|
|
2 & 353 & 375 & 464 & 12080 & 924.51 & 1027.75 & 103,24 \\
|
|
3 & 286 & 345 & 535 & 12080 & 1018.21 & 1145.36 & 127.15 \\
|
|
4 & 291 & 307 & 340 & 12080 & 1129.91 & 1239.75 & 109,84 \\
|
|
\hline
|
|
\end{tabular}
|
|
\caption{Messung ohne Caches}
|
|
\label{tbl:measure-without-cache}
|
|
\end{table}
|
|
|
|
Vor jedem weiteren Test"=Lauf wird die Domain beendet und komplett neugestartet, um mit einer frischen Instanz zu
|
|
beginnen. Hierbei ist aufgefallen, dass fast immer 62 Abfragen zur Startup-Phase dazugehört haben, unabhängig von den
|
|
konfigurierten Cache Einstellungen. Einige dieser Abfragen sind durch das Erstellen der \textit{Materialized View}
|
|
\textit{searchreference} und \textit{searchfulltext} erklärbar. Zusätzlich ist noch ein zyklischer Dienst
|
|
\textit{SearchEntityService} vorhanden, der zum Start und alle sechs Stunden den Datenbestand für die Suche aufbereitet
|
|
und entsprechend einige Abfragen an die Datenbank absetzt. Da weder die Sichten noch der Dienst für die Dokumentenliste
|
|
benötigt werden, wurde der Dienst und das Erstellen im Code für die weiteren Tests deaktiviert.
|
|
|
|
Da die Abfragezeiten auf der Datenbank zu gering waren, um eine Verbesserung feststellen zu können, wurde für den
|
|
PostgreSQL und den Payara"=Server ein Docker"=Container erzeugt und diese limitiert. Die Konfiguration ist in
|
|
\autoref{ap:docker_config} beschrieben.
|
|
|
|
Mit dem neuen Aufbau ergeben sich nun eine neue Messung. Für den Speicherbedarf wird nun nicht mehr der benutzte
|
|
Speicher der Anwendung beobachtet, sondern die Speichernutzung des Docker-Containers für den Payara-Server. Auch hier
|
|
ist es besser, wenn es keine oder nur geringe Änderungen vor und nach dem Aufruf der Webseite gibt, ein steigender Wert
|
|
zeigt an, dass der verwendete Speicher nicht sauber freigegeben werden kann.
|
|
|
|
Für die Ausführungszeiten der \ac{SQL}"=Abfragen wurden nur die sechs Abfragen für die Darstellung der Tabelle beachtet.
|
|
Hierzu zählt die Hauptabfrage der Dokumenten"=-Tabelle, die Ermittlung des letzten und ersten Eintrags in der Tabelle,
|
|
die Ermittlung der Adressen des Autors, die Ermittlung der Koautoren, die Ermittlung der Faksimile, sowie die Ermittlung
|
|
der Anzahl aller vorhandenen Dokumente.
|
|
|
|
Zusätzlich wird die Zeit des Rendern der Sicht gemessen. Hierbei wird zum einen die komplette Zeit des Renderns
|
|
ermittelt. Innerhalb des Rendern wird dann noch die Zeit gemessen, wie lange es benötigt, die Daten aus der Datenbank
|
|
zu laden, und in die Java"=Objekte umzuformen.
|
|
|
|
\begin{table}[h]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 451 & 682 & 1931 & 1223.0 & 30.3 & 931.3 & 986.1 & 54.8 & 440 & 666 & 1859 & 290 & 399 & 710 \\ % 12230 - 303 ms (135+ 79+ 39+ 22+17+11) (#2-6,8)
|
|
2 & 341 & 389 & 478 & 1208.0 & 31.2 & 986.5 & 1159.0 & 172.5 & 331 & 378 & 468 & 235 & 282 & 367 \\ % 24310 - 615 ms (270+156+ 78+ 56+34+21) (#2-7)
|
|
3 & 299 & 407 & 682 & 1208.0 & 33.5 & 1163.0 & 1273.0 & 110.0 & 290 & 398 & 672 & 207 & 307 & 579 \\ % 36390 - 950 ms (406+256+118+ 79+55+36) (#2-7)
|
|
4 & 278 & 359 & 424 & 1208.0 & 33.7 & 1272.0 & 1465.0 & 193.0 & 270 & 351 & 415 & 198 & 269 & 342 \\ % 48470 - 1287 ms (564+334+167+105+72+45) (#2-7)
|
|
5 & 264 & 317 & 356 & 1208.0 & 32.9 & 1467.0 & 1574.0 & 107.0 & 256 & 309 & 348 & 184 & 235 & 276 \\ % 60560 - 1616 ms (699+428+210+128+92+59) (#2-7)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung ohne Caches im Docker}
|
|
\label{tbl:measure-without-cache-docker}
|
|
\end{table}
|
|
|
|
\section{Caching im OpenJPA}
|
|
\label{sec:performance-investigation-application:caching-openjpa}
|
|
|
|
Die Cache"=Einstellung von OpenJPA werden über die zwei Einstellungen \texttt{openjpa.DataCache} und
|
|
\texttt{openjpa.QueryCache} konfiguriert. Bei beiden Einstellungen kann zuerst einmal über ein einfaches Flag
|
|
\textit{true} und \textit{false} entschieden werden ob der Cache aktiv ist. Zusätzlich kann über das Schlüsselwort
|
|
\textit{CacheSize} die Anzahl der Elemente im Cache gesteuert werden. Wird diese Anzahl erreicht, dann werden zufällige
|
|
Objekte aus dem Cache entfernt und in eine SoftReferenceMap übertragen. Bei der Berechnung der Anzahl der Elemente werden
|
|
angeheftete Objekte nicht beachtet.
|
|
|
|
Die Anzahl der Soft References kann ebenfalls über eine Einstellung gesteuert werden. Hierfür wird die Anzahl der
|
|
Elemente über \textit{SoftReferenceSize} gesetzt, dessen Wert im Standard auf \textit{unbegrenzt} steht. Mit dem Wert
|
|
\textit{0} werden die Soft Referenzen komplett deaktiviert. Über die Attribute an den Entitätsklassen, können diese
|
|
Referenzen ebenfalls gesteuert werden, hierzu muss eine Überwachungszeit angegeben werden. Diese Zeit gibt in ms an,
|
|
wie lange ein Objekt gültig bleibt. Mit dem Wert \textit{-1} wird das Objekt nie ungültig, was ebenfalls der
|
|
Standardwert ist.
|
|
|
|
Zuerst wird mit aktivierten Cache mit einer Cache-Größe von 1000 Elemente getestet. Wie in
|
|
\autoref{tbl:measure-ojpa-active} zu sehen, dauert auch hier der erste Aufruf minimal länger als ohne aktiviertem
|
|
Cache. Alle Nachfolgenden Aufrufe wiederrum sind um 100ms schneller in der Verarbeitung. Auch bei der Anzahl der
|
|
Anfragen an die Datenbank kann der Rückgang der Anfragen sehr gut gesehen werden. Aktuell kann die Verringerung des
|
|
wachsenden Speicherbedarfs nur nicht erklärt werden.
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 291 & 611 & 2347 & 730.2 & 28.8 & 852.7 & 891.9 & 39.2 & 282 & 595 & 2286 & 172 & 284 & 770 \\ % 7302 - 288 ms (145+ 42+ 40+ 24+18+ 8+ 7+ 4) (#2-8,12)
|
|
2 & 278 & 319 & 422 & 667.3 & 25.8 & 892.7 & 1010.0 & 117.3 & 266 & 309 & 411 & 173 & 195 & 220 \\ % 13975 - 546 ms (282+ 81+ 70+ 47+33+14+11+ 8) (#2-9)
|
|
3 & 229 & 281 & 329 & 680.6 & 27.6 & 1011.0 & 1067.0 & 56.0 & 220 & 271 & 313 & 134 & 180 & 222 \\ % 20781 - 822 ms (430+120+ 99+ 77+49+20+16+11) (#2-9)
|
|
4 & 222 & 280 & 321 & 671.3 & 27.6 & 1067.0 & 1122.0 & 55.0 & 213 & 271 & 310 & 131 & 189 & 238 \\ % 27494 - 1098 ms (569+160+137+ 99+68+26+22+17) (#2-9)
|
|
5 & 206 & 272 & 388 & 683.6 & 27.6 & 1122.0 & 1219.0 & 97.0 & 199 & 264 & 380 & 122 & 175 & 291 \\ % 34330 - 1374 ms (704+202+171+128+86+34+27+22) (#2-9)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit OpenJPA-Cache und Größe auf 1000}
|
|
\label{tbl:measure-ojpa-active}
|
|
\end{table}
|
|
|
|
Bei einer erhöhten Cache"=Größe, von 1000 auf 10000, zeigt sich auf den ersten Blick ein noch besseres Bild ab, wie in
|
|
\autoref{tbl:measure-ojpa-active-bigger} ersichtlich ist. Der erste Aufruf entspricht der Laufzeit mit geringerer
|
|
Cache"=Größe, aber schon die Anfragen an die Datenbank gehen drastisch zurück. Bei den weiteren Aufrufen werden im
|
|
Schnitt nun nur noch 6 Anfragen pro Seitenaufruf an die Datenbank gestellt, wodurch die Laufzeit im Schnitt nochmal
|
|
um 100 ms beschleunigt werden konnte.
|
|
|
|
\begin{table}[!ht]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 151 & 368 & 1904 & 141.2 & 20.8 & 906.3 & 936.8 & 30.5 & 164 & 404 & 2232 & 39 & 124 & 847 \\ % 1412 - 208 ms (133+ 40+ 23+9+2+1) (#2,4-6,10,12)
|
|
2 & 133 & 143 & 159 & 6.0 & 20.5 & 935.7 & 939.3 & 3.6 & 121 & 136 & 146 & 32 & 36 & 44 \\ % 1472 - 413 ms (274+ 80+ 47+9+2+1) (#2-3,5,6,10,12)
|
|
3 & 120 & 126 & 132 & 6.0 & 19.9 & 939.4 & 942.7 & 3.3 & 116 & 136 & 256 & 32 & 47 & 167 \\ % 1532 - 612 ms (412+119+ 69+9+2+1) (#2,3,5,6,10,12)
|
|
4 & 120 & 124 & 128 & 6.0 & 21.4 & 944.3 & 945.4 & 1.1 & 105 & 113 & 125 & 30 & 32 & 39 \\ % 1592 - 826 ms (550+168+ 96+9+2+1) (#2-4,6,10,12)
|
|
5 & 109 & 114 & 131 & 6.0 & 19.7 & 945.5 & 946.7 & 1.2 & 101 & 107 & 112 & 30 & 32 & 35 \\ % 1652 - 1023 ms (683+209+119+9+2+1) (#2-4,6,10,12)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit OpenJPA-Cache und Größe auf 10000}
|
|
\label{tbl:measure-ojpa-active-bigger}
|
|
\end{table}
|
|
|
|
Bei dem deaktivieren der \textit{SoftReference} und dem kleineren Cache zeigt sich keine große Differenz, somit scheint
|
|
die \textit{SoftReference} nicht das Problem für den steigenden Arbeitsspeicher zu sein, wie in
|
|
\autoref{tbl:measure-ojpa-active-bigger-no-softref} ersichtlich.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 339 & 659 & 2435 & 880.8 & 33.2 & 909.6 & 960.2 & 50.6 & 330 & 644 & 2375 & 218 & 343 & 815 \\ % 8808 - 332 ms (168+ 63+ 44+ 32+21+ 4) (#1,3-6,10)
|
|
2 & 267 & 332 & 388 & 832.1 & 28.1 & 959.7 & 1000.0 & 40.3 & 259 & 323 & 377 & 178 & 229 & 280 \\ % 17129 - 613 ms (313+111+ 82+ 48+42+17) (#1-3,5-6,8)
|
|
3 & 265 & 397 & 350 & 830.3 & 27.3 & 1001.0 & 1107.0 & 106.0 & 256 & 288 & 241 & 172 & 204 & 252 \\ % 25432 - 886 ms (455+156+125+ 64+63+23) (#1-3,5-6,8)
|
|
4 & 249 & 311 & 401 & 727.8 & 27.1 & 1108.0 & 1234.0 & 126.0 & 240 & 303 & 392 & 165 & 225 & 317 \\ % 32710 - 1157 ms (594+205+163+ 85+80+30) (#1-5,8)
|
|
5 & 268 & 296 & 325 & 931.9 & 28.0 & 1236.0 & 1239.0 & 3.0 & 260 & 288 & 318 & 192 & 217 & 244 \\ % 42029 - 1437 ms (738+254+204+106+97+38) (#1-5,8)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit OpenJPA-Cache und Größe auf 1000 und 0 SoftReference}
|
|
\label{tbl:measure-ojpa-active-bigger-no-softref}
|
|
\end{table}
|
|
|
|
Der Vergleich zeigt, dass der Cache eine gute Optimierung bringt, aber dies nur dann gut funktioniert, wenn immer
|
|
wieder die gleichen Objekte ermittelt werden. Sobald die Anfragen im Wechsel gerufen werden oder einfach nur die Menge
|
|
der Objekte den Cache übersteigt, fällt die Verbesserung geringer aus.
|
|
|
|
\section{Cached Queries}
|
|
\label{sec:performance-investigation-application:cached-query}
|
|
|
|
Über die Einstellung \textit{openjpa.""jdbc.""QuerySQLCache} wird der Cache für Abfragen aktiviert. Hierbei können
|
|
Abfragen angeben werden, die aus dem Cache ausgeschlossen werden. Der QueryCache wiederrum beachtet aber nur Abfragen
|
|
die keine Parameter verwenden. Das sieht man auch entsprechend der Auswertung der Aufrufe in
|
|
\autoref{tbl:measure-cached-queries}, dass hier keine Veränderung der Aufrufzeiten stattgefunden hat, gleich ob
|
|
mit \ac{JPQL} oder Criteria API abfragt wird.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 409 & 771 & 2660 & 1222.4 & 32.8 & 850.4 & 982.8 & 132.4 & 366 & 633 & 2019 & 254 & 364 & 758 \\ % 12224 - 328 ms (140+ 90+ 43+ 22+20+13) (#1-6)
|
|
2 & 336 & 387 & 504 & 1208.0 & 31.2 & 982.9 & 1113.0 & 130.1 & 310 & 374 & 433 & 221 & 268 & 345 \\ % 24304 - 640 ms (280+174+ 80+ 41+39+26) (#1-6)
|
|
3 & 312 & 373 & 422 & 1208.0 & 31.1 & 1114.0 & 1221.0 & 107.0 & 295 & 401 & 658 & 216 & 320 & 570 \\ % 36384 - 951 ms (417+258+119+ 62+57+38) (#1-6)
|
|
4 & 288 & 363 & 471 & 1208.0 & 31.3 & 1239.0 & 1474.0 & 235.0 & 269 & 356 & 486 & 200 & 279 & 405 \\ % 48464 - 1264 ms (557+341+159+ 82+75+49) (#1-6)
|
|
5 & 325 & 398 & 535 & 1208.0 & 33.5 & 1474.0 & 1666.0 & 192.0 & 280 & 466 & 804 & 208 & 390 & 725 \\ % 60544 - 1599 ms (698+436+198+109+96+62) (#1-6)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit aktiviertem Cached Queries}
|
|
\label{tbl:measure-cached-queries}
|
|
\end{table}
|
|
|
|
\section{Caching mit Ehcache}
|
|
\label{sec:performance-investigation-application:caching-ehcache}
|
|
|
|
Der Ehcache ist ein L2"=Cache den man direkt in OpenJPA mit integrieren kann. Hierfür sind einige Punkte zu beachten.
|
|
Zum einen muss die Reference auf das \textit{ehcache} und das \textit{ehcache"=openjpa} Packet hinzugefügt werden.
|
|
Zusätzlich dazu sind die Konfiguration \textit{openjpa.""QueryCache}, \textit{openjpa.""DataCache} und
|
|
\textit{openjpa.""DataCacheManager} auf \textit{ehcache} anzupassen. Anhand der Annotation \texttt{@DataCache} kann
|
|
an jeder Klasse die Benennung des Caches sowie die Verwendung selbst gesteuert werden. Es wird für jede Klasse ein
|
|
eigener Cache angelegt und der Name auf den vollen Klassennamen gesetzt. Die Verwendung ist für alle Klassen
|
|
aktiviert und müssen explizit deaktiviert werden, wenn dies nicht gewünscht ist. Als letztes muss noch der
|
|
Cache"=Manager aktiviert werden, dieser kann entweder durch Code programmiert werden oder über eine Konfiguration
|
|
in der \textit{ehcache.xml}.
|
|
|
|
Anhand der Auswertung von \ref{tbl:measure-ehcache-active} sieht man, dass der Ehcache einen starke Performance
|
|
Verbesserung aufbringt. Über die Performance"=Statistik"=Webseite kann beobachtet werden, dass bei gleichen Aufrufen
|
|
der Webseite nur die Treffer in Cache steigen, aber die Misses nicht. Ebenfalls erhöht sich die Anzahl der Objekte
|
|
im Cache nicht. Zusätzlich steigt in diesem Fall der Speicherverbrauch nur gering bis gar nicht. Des Weiteren zeigt sich,
|
|
dass sich die Abfragezeiten in der Datenbank nur gering verkürzt wurden, aber die Laufzeit der Webseite sich stark
|
|
verbessert hat. Dies lässt auch hier den Schluss zu, dass die Erstellung der Objekte im OpenJPA die meiste Zeit
|
|
benötigt.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
%- & 151 & 368 & 1904 & 141.2 & 20.8 & 906.3 & 936.8 & 30.5 & 164 & 404 & 2232 & 39 & 124 & 847 \\ % 1412 - 208 ms (133+ 40+ 23+9+2+1) (#2,4-6,10,12)
|
|
1 & 156 & 488 & 2820 & 135.2 & 20.7 & 981.6 & 1006.0 & 24.4 & 147 & 490 & 2809 & 39 & 175 & 1186 \\ % 1352 - 207 ms (136+ 35+ 19+12+3+2) (#1-4,7-8)
|
|
2 & 135 & 144 & 166 & 6.0 & 20.1 & 1006.0 & 1007.0 & 1.0 & 124 & 136 & 157 & 33 & 38 & 47 \\ % 1412 - 408 ms (272+ 77+ 42+12+3+2) (#1-4,7-8)
|
|
3 & 121 & 129 & 136 & 6.0 & 19.4 & 1008.0 & 1009.0 & 1.0 & 113 & 121 & 126 & 32 & 34 & 33 \\ % 1472 - 602 ms (407+115+ 63+12+3+2) (#1-4,7-8)
|
|
4 & 116 & 123 & 133 & 6.0 & 19.7 & 1008.0 & 1016.0 & 8.0 & 108 & 116 & 125 & 31 & 33 & 34 \\ % 1532 - 799 ms (542+155+ 85+12+3+2) (#1-4,7-8)
|
|
5 & 111 & 118 & 127 & 6.0 & 12.7 & 1016.0 & 1012.0 & -4.0 & 104 & 111 & 119 & 32 & 34 & 38 \\ % 1592 - 926 ms (608+194+107+12+3+2) (#1-4,7-8)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit aktiviertem Ehcache}
|
|
\label{tbl:measure-ehcache-active}
|
|
\end{table}
|
|
|
|
\section{Caching in EJB}
|
|
\label{sec:performance-investigation-application:caching-ejb}
|
|
|
|
Die Cache"=Einstellungen des \ac{EJB} sind in der Admin-Oberfläche des Payara-Servers zu erreichen. Unter dem Punkt
|
|
Configurations $\Rightarrow$ server"=config $\Rightarrow$ EJB Container werden zum einem die minimalen und maximalen
|
|
Größen des Pools definiert werden. Zum anderen wird an dieser Stelle die maximale Größe des Caches und die Größe der
|
|
Erweiterung definiert.
|
|
|
|
Anhand der Auswertung der \autoref{tbl:measure-ejb-cache-active} ist ersichtlich, dass der \ac{EJB}"=Cache keine
|
|
Auswirkung auf die Performance hat. Ebenso ist es ersichtlich, dass die Anzahl der Datenbankabfragen nicht reduziert
|
|
wurden. Dies ist dadurch zu erklären, dass im \ac{EJB} die Provider gelagert werden, die über Dependency Injection
|
|
den Controller bereitgestellt werden. Die Objekt selbst werden nicht im \ac{EJB}"=Cache hinterlegt.
|
|
|
|
% document, documentaddresseeperson, first/last, documentcoauthorperson, count und documentfacsimile
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 364 & 741 & 2962 & 1222.1 & 29.4 & 880.6 & 991.7 & 111.1 & 353 & 725 & 2902 & 248 & 366 & 689 \\ % 12221 - 294 ms (135+ 73+ 41+ 20+16+ 9) (#1,2,4-7)
|
|
2 & 318 & 378 & 460 & 1208.0 & 31.0 & 992.4 & 1099.0 & 106.6 & 310 & 370 & 451 & 225 & 275 & 362 \\ % 24301 - 604 ms (274+154+ 80+ 42+34+20) (#1-3,5-7)
|
|
3 & 314 & 397 & 528 & 1208.0 & 32.5 & 1109.0 & 1308.0 & 199.0 & 306 & 388 & 519 & 227 & 307 & 434 \\ % 36381 - 929 ms (411+245+122+ 63+54+34) (#1-3,5-7)
|
|
4 & 334 & 371 & 420 & 1208.0 & 32.7 & 1308.0 & 1528.0 & 220.0 & 326 & 363 & 412 & 246 & 289 & 333 \\ % 48461 - 1256 ms (557+333+163+ 84+73+46) (#1-5,7)
|
|
5 & 304 & 392 & 562 & 1208.0 & 33.3 & 1518.0 & 1662.0 & 144.0 & 297 & 385 & 555 & 229 & 311 & 478 \\ % 60541 - 1589 ms (697+431+202+104+95+60) (#1-5,7)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit EJB"=Cache}
|
|
\label{tbl:measure-ejb-cache-active}
|
|
\end{table}
|
|
|
|
\section{Abfragen JPQL}
|
|
\label{sec:performance-investigation-application:query-jpql}
|
|
|
|
Für die \ac{JPQL} wird ein \ac{SQL} ähnlicher Syntax verwendet um die Abfragen an die Datenbank durchzuführen. Für die
|
|
Dokumentenliste wird der Code aus \autoref{lst:jpql-document-list-jpql} verwendet. Die Namen mit vorangestellten
|
|
Doppelpunkt sind Übergabevariablen.
|
|
|
|
\begin{lstlisting}[language=Java,caption={JPQL Dokumentenliste},label=lst:jpql-document-list-jpql]
|
|
SELECT DISTINCT d FROM Document d
|
|
LEFT JOIN FETCH d.authorPerson
|
|
LEFT JOIN FETCH d.coauthorPersonSet
|
|
LEFT JOIN FETCH d.addresseePersonSet
|
|
WHERE d.validUntil > :now
|
|
AND d.isPublishedInDb = :published
|
|
ORDER BY d.documentId ASC
|
|
\end{lstlisting}
|
|
|
|
In dem dazugehörigen Code am Server wird der JPQL-Code als NamedQuery hinterlegt und über den Name
|
|
\textit{Document.""findAll} referenziert. Eine Veränderung der Abfrage ist hier leider nicht möglich, wie man im Code
|
|
aus \autoref{lst:jpql-document-list} sehen kann.
|
|
|
|
\begin{lstlisting}[language=Java,caption={Java JPQL Dokumentenliste},label=lst:jpql-document-list]
|
|
List<Document> myResultList = createNamedTypedQuery("Document.findAll")
|
|
.setParameter("now", _IncludeDeleted ? new Date(0) : Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
|
|
.setParameter("published", true)
|
|
.setFirstResult(_Start)
|
|
.setMaxResults(_Size)
|
|
.setHint("javax.persistence.query.fetchSize", _Size)
|
|
.getResultList();
|
|
|
|
// Uebergabe der Ergebnisliste
|
|
if(myResultList != null && !myResultList.isEmpty()) {
|
|
myResult.addAll(myResultList);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Da dieser Code direkt so aus dem Projekt kommt, wird hierfür keine gesonderte Zeitmessung durchgeführt, da diese der
|
|
Messung aus \autoref{tbl:measure-without-cache} entspricht.
|
|
|
|
Für die Optimierung wurden noch zusätzlich die Hints \texttt{openjpa.""hint.""OptimizeResultCount},
|
|
\texttt{javax.""persistence.""query.""fetchSize} und \texttt{openjpa.""FetchPlan.""FetchBatchSize} gesetzt. Hierbei
|
|
konnten je nach gesetzten Wert, keine relevanten Unterschiede festgestellt werden. Hierbei wurde der Wert auf zwei
|
|
gesetzt, welcher viel zu gering ist. Als weiterer Test wurde der Wert auf angefragte Größte gestellt und auf den
|
|
20"=fachen Wert der angefragten Größe.
|
|
|
|
Ebenso bringt der Hint \texttt{openjpa.""FetchPlan.""ReadLockMode} auch keinen Unterschied bei der Geschwindigkeit.
|
|
Hierbei ist erklärbar, das im Standard bei einer reinen Selektion eine Lesesperre aktiv sein muss.
|
|
Bei \texttt{openjpa.""FetchPlan.""Isolation} wird gesteuert, auf welche Sperren beim Laden geachtet wird. Damit könnte
|
|
man lediglich Schreibsperren umgehen, und würde damit die Anfrage nicht mehr blockieren lassen, jedoch führt es unweigerlich
|
|
zu sogenannten >>Dirty"=Reads<<, wodurch die Ausgabe verfälscht werden könnte. Daher ist diese Einstellung
|
|
mit Vorsicht zu verwenden.
|
|
|
|
Mit dem Hint \texttt{openjpa.""FetchPlan.""EagerFetchMode} wird definiert, wie zusammengehörige Objekte abgefragt werden.
|
|
Bei dem Wert \textit{none} werden nur die Basis"=Daten abgefragt und jedes weitere Objekt wird in einem eigenen
|
|
Statement abgefragt. Mit \textit{join} wird definiert, dass abhängige Objekte die als >>to-one<<"=Relation
|
|
definiert sind, in der Abfrage über einen Join verknüpft und damit direkt mitgeladen werden. Bei reinen
|
|
>>to-one<<"=Relation funktioniert das Rekursiv und spart sich damit einige einzelne Abfragen.
|
|
Bei der Einstellung \textit{parallel} wird zwar für jede abhängige Objektdefinition eine Abfrage ausgeführt und
|
|
\texthl{diese werden direkt auf die Hauptobjekte gefiltert und die Verknüpfung im} OpenJPA"=Framework durchgeführt.
|
|
\todo{das wurde umgebaut}
|
|
Somit muss in unserem Beispiel nicht für jedes Dokument eine einzelne Abfrage für die Koautoren durchgeführt werden,
|
|
sondern es wird nur eine Abfrage abgesetzt für alle Dokumente die ermittelt wurden. Technisch gesehen wird die gleiche
|
|
WHERE"=Abfrage nochmal durchgeführt und um die JOINS ergänzt, um die Daten der Unterobjekte zu ermitteln.
|
|
Mit dem Hint \texttt{openjpa.""FetchPlan.""SubclassFetchMode} ist die Konfiguration für Unterklassen definiert. Die
|
|
Möglichkeiten entsprechen der vom \texttt{openjpa.""FetchPlan.""EagerFetchMode}.
|
|
|
|
Beim Umstellen der 2 Hints auf \textit{parallel} wird die Bearbeitungszeit fast halbiert und Anzahl der Datenbankaufrufe
|
|
wurde fast geviertelt. Dies zeigt, dass die einzelnen Aufrufe je Dokument aufwendiger sind, als eine komplette Abfrage
|
|
der abhängigen Daten und das zusammensetzen in der OpenJPA-Schicht.
|
|
|
|
Der letzte Hint \texttt{openjpa.""FetchPlan.""MaxFetchDepth} schränkt die rekursive Tiefe ein, für die abhängige
|
|
Objekte mitgeladen werden. Lediglich auf Grund fehlender Datenbestände wird die Abfrage beschleunigt.
|
|
|
|
\section{Abfragen Criteria API}
|
|
\label{sec:performance-investigation-application:query-criteria-api}
|
|
|
|
Für die Criteria API wird die Abfrage nicht in einem SQL-Dialekt beschreiben, hierbei werden über Attribute die
|
|
Verlinkung zur Datenbank durchgeführt. An der Klasse selbst wird der Tabellenname definiert und an den Attributen die
|
|
Spaltennamen. Um die Anfrage durchzuführen muss nun nur noch die Datenklasse angegeben und mit den Parametern
|
|
versorgt werden, wie es in \autoref{lst:criteria-api} gezeigt wird.
|
|
|
|
\begin{lstlisting}[language=Java,caption={Criteria API Dokumentenliste},label=lst:criteria-api]
|
|
CriteriaBuilder cb = getEntityManager().getCriteriaBuilder();
|
|
CriteriaQuery<Document> cq = cb.createQuery(Document.class);
|
|
Root<Document> from = cq.from(Document.class);
|
|
ParameterExpression<Boolean> includedPara = cb.parameter(Boolean.class, "published");
|
|
ParameterExpression<Date> validPart = cb.parameter(Date.class, "now");
|
|
|
|
CriteriaQuery<Document> select = cq.select(from)
|
|
.where(cb.and(
|
|
cb.equal(from.get("isPublishedInDb"), includedPara),
|
|
cb.greaterThan(from.get("validUntil"), validPart)
|
|
));
|
|
TypedQuery<Document> typedQuery = getEntityManager().createQuery(select)
|
|
.setParameter("now", _IncludeDeleted ? new Date(0) : Date.from(LocalDateTime.now().atZone(ZoneId.systemDefault()).toInstant()))
|
|
.setParameter("published", true)
|
|
.setFirstResult(_Start)
|
|
.setMaxResults(_Size)
|
|
.setHint("javax.persistence.query.fetchSize", _Size);
|
|
List<Document> myResultList = typedQuery.getResultList();
|
|
|
|
// Uebergabe der Ergebnisliste
|
|
if (myResultList != null && !myResultList.isEmpty()) {
|
|
myResult.addAll(myResultList);
|
|
}
|
|
\end{lstlisting}
|
|
|
|
Wie in der Messung in \autoref{tbl:measure-criteria-api} zu sehen, unterscheiden sich die Abfragezeiten nur marginal
|
|
von denen mit \ac{JPQL}. Wenn man sich den Code im Debugger betrachtet, \texthl{ist zu erkennen,} dass die zusammengesetzten
|
|
Abfragen in den Java-Objekten fast identisch sind. Und in der Datenbank sind die Anfragen identisch zu denen über JPQL.
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 429 & 704 & 2472 & 1224.4 & 27.0 & 848.5 & 928.2 & 79.7 & 419 & 687 & 2400 & 276 & 368 & 732 \\ % 12244 - 270 ms (120+ 66+ 41+ 20+12+11) (#1-6)
|
|
2 & 327 & 396 & 482 & 1208.0 & 30.1 & 929.3 & 1151.0 & 221.7 & 318 & 383 & 472 & 216 & 284 & 339 \\ % 24324 - 571 ms (257+138+ 82+ 44+26+24) (#1-6)
|
|
3 & 322 & 397 & 507 & 1208.0 & 28.6 & 1151.0 & 1304.0 & 153.0 & 312 & 389 & 498 & 232 & 308 & 420 \\ % 36404 - 857 ms (370+219+123+ 66+41+38) (#1-6)
|
|
4 & 306 & 351 & 416 & 1208.0 & 27.1 & 1303.0 & 1439.0 & 136.0 & 298 & 341 & 401 & 218 & 261 & 323 \\ % 48484 - 1128 ms (489+284+163+ 91+53+48) (#1-6)
|
|
5 & 288 & 357 & 448 & 1208.0 & 27.1 & 1440.0 & 1580.0 & 140.0 & 279 & 348 & 441 & 201 & 271 & 360 \\ % 60564 - 1399 ms (603+354+205+113+65+59) (#1-6)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit Criteria-API ohne Cache}
|
|
\label{tbl:measure-criteria-api}
|
|
\end{table}
|
|
|
|
Daher bringt die Criteria API keinen performance Vorteil gegenüber der JPQL"=Implementierung. Somit können beide
|
|
Implementierung ohne bedenken gegeneinander ausgetauscht werden, und die verwendet werden, die für den Anwendungsfall
|
|
einfacher umzusetzen ist.
|
|
|
|
Bei den Hints ist es das gleiche wie bei \ac{JPQL}. Auch hier haben die meisten Hints keinen merkbaren Einfluss. Die
|
|
Einstellung \texttt{openjpa.""FetchPlan.""EagerFetchMode} liefert auch hier Optimierungen, wenn der Wert auf
|
|
\textit{parallel} gestellt wird. Hier wird ebenfalls die Anzahl der Anfragen reduziert und damit auch die
|
|
Geschwindigkeit optimiert.
|
|
|
|
\section{Materialized Views}
|
|
\label{sec:performance-investigation-application:materialized-views}
|
|
|
|
\textit{Materialized Views} sind Sichten in der Datenbank, die beim Erstellen der Sicht den aktuellen Zustand ermitteln
|
|
und zwischenspeichern. Somit wird beim Zugriff auf diese Sichten, nicht die hinterlegte Abfrage ausgeführt, sondern auf
|
|
die gespeicherten Daten zugegriffen. Dies ist gerade bei vielen Joins von Vorteil. Zusätzlich können auf solchen
|
|
Sichten auch Indexe erstellt werden, um noch effektiver die Abfragen bearbeiten zu können.
|
|
|
|
Der größte Nachteil dieser Sichten ist, dass sie zyklisch oder bei Datenänderungen aktualisiert werden müssen, sonst
|
|
läuft der Datenbestand der Sicht und der zugrundeliegenden Abfrage auseinander. Da die Hauptarbeiten auf der Webseite
|
|
die Abfrage der Daten ist, und nicht das Editieren, kann dieser Nachteil bei entsprechender Optimierung ignoriert werden.
|
|
|
|
In diesem Test wurde die aktuelle Implementierung aus dem Wedekind"=Projekt der \textit{Materialized View} inklusive
|
|
der Trigger und der \textit{SearchDocument}"=Klasse übernommen \citep{Dokument53:online}. Wie in
|
|
\autoref{lst:sql-materialized-view} zu sehen, wurden zur Standard"=Abfrage, die sonst zusätzlichen Abfragen als
|
|
direkte Sub"=Selects mit integriert. Der Datenbestand dieser Sub"=Selects, wird im Json"=Format angegeben, damit bei
|
|
den Koautoren und den Adressen mehrere Datensätze in einer Zeile zurückgegeben werden können. Ohne diese Technik würde
|
|
sich die Anzahl der Dokumente vervielfachen.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={SQL Materialized View},label=lst:sql-materialized-view]
|
|
CREATE MATERIALIZED VIEW searchdocument AS
|
|
SELECT
|
|
d.id, d.documentid, d.datetype, d.startdatestatus, d.startyear,
|
|
d.startmonth, d.startday, d.enddatestatus, d.endyear, d.endmonth,
|
|
d.endday,
|
|
(
|
|
SELECT
|
|
jsonb_build_object(
|
|
'personId', hp.personid,
|
|
'surname', hp.surname,
|
|
'firstname', hp.firstname,
|
|
'dateBirth', json_build_object(
|
|
'year', hp.birthstartyear,
|
|
'month', hp.birthstartmonth,
|
|
'day', hp.birthstartday
|
|
),
|
|
'dateDeath', json_build_object(
|
|
'year', hp.deathstartyear,
|
|
'month', hp.deathstartmonth,
|
|
'day', hp.deathstartday
|
|
)
|
|
)
|
|
FROM historicalperson hp
|
|
WHERE hp.id = d.authorperson_id
|
|
AND hp.validuntil > NOW()
|
|
) as author,
|
|
(
|
|
SELECT
|
|
jsonb_agg(jsonb_build_object(
|
|
'personId', hcap.personid,
|
|
'surname', hcap.surname,
|
|
'firstname', hcap.firstname,
|
|
'dateBirth', json_build_object(
|
|
'year', hcap.birthstartyear,
|
|
'month', hcap.birthstartmonth,
|
|
'day', hcap.birthstartday
|
|
),
|
|
'dateDeath', json_build_object(
|
|
'year', hcap.deathstartyear,
|
|
'month', hcap.deathstartmonth,
|
|
'day', hcap.deathstartday
|
|
)
|
|
))
|
|
FROM documentcoauthorperson dcap
|
|
JOIN historicalperson hcap
|
|
ON hcap.id = dcap.authorperson_id
|
|
AND dcap.validuntil > NOW()
|
|
AND hcap.validuntil > NOW()
|
|
WHERE dcap.document_id = d.id
|
|
) AS coauthors,
|
|
(
|
|
SELECT
|
|
jsonb_agg(jsonb_build_object(
|
|
'personId', hap.personid,
|
|
'surname', hap.surname,
|
|
'firstname', hap.firstname,
|
|
'dateBirth', json_build_object(
|
|
'year', hap.birthstartyear,
|
|
'month', hap.birthstartmonth,
|
|
'day', hap.birthstartday
|
|
),
|
|
'dateDeath', json_build_object(
|
|
'year', hap.deathstartyear,
|
|
'month', hap.deathstartmonth,
|
|
'day', hap.deathstartday
|
|
)
|
|
))
|
|
FROM documentaddresseeperson dap
|
|
JOIN historicalperson hap
|
|
ON hap.id = dap.addresseeperson_id
|
|
AND dap.validuntil > NOW()
|
|
AND hap.validuntil > NOW()
|
|
WHERE dap.document_id = d.id
|
|
) AS addressees,
|
|
sc.city, d.documentcategory, d.ispublishedindb, d.createdat,
|
|
d.modifiedat, d.validuntil
|
|
FROM document d
|
|
LEFT JOIN sitecity sc ON sc.id = d.city_id;
|
|
\end{lstlisting}
|
|
|
|
Zusätzlich zur View, werden noch die Indexe aus \autoref{lst:sql-materialized-view-index} erstellt. Diese werden für
|
|
eine bessere Performance der Abfrage benötigt.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={SQL Materialized View},label=lst:sql-materialized-view-index]
|
|
CREATE INDEX idx_searchdocument_documentid
|
|
ON searchdocument (documentid);
|
|
|
|
CREATE INDEX idx_searchdocument_author_surname_firstname
|
|
ON searchdocument ((author->>'surname'), (author->>'firstname'));
|
|
|
|
CREATE INDEX idx_searchdocument_startdate
|
|
ON searchdocument (startyear, startmonth, startday);
|
|
|
|
CREATE INDEX idx_searchdocument_addressees_first_entry
|
|
ON searchdocument
|
|
( ((addressees->0->>'surname')::text)
|
|
, ((addressees->0->>'firstname')::text));
|
|
|
|
CREATE INDEX idx_searchdocument_city
|
|
ON searchdocument (city);
|
|
|
|
CREATE INDEX idx_searchdocument_documentcategory
|
|
ON searchdocument (documentcategory);
|
|
\end{lstlisting}
|
|
|
|
Für die Datenermittlung wurden die notwendigen Teile aus dem Wedekind"=Projekt kopiert. Bei der Darstellung wurden die
|
|
vorhanden Elemente, welche die Liste der Dokumente anzeigt, kopiert und auf die \texttt{SearchDocument}"=Klasse angepasst.
|
|
|
|
% document, first/last, documentaddresseeperson, documentcoauthorperson, documentfacsimile und count
|
|
% document, count, first/last
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 232 & 424 & 1486 & 14.3 & 1.4 & 828.2 & 929.3 & 101.1 & 222 & 408 & 1404 & 138 & 208 & 393 \\ % 143 - 14 ms (13+1+0) (#1,3,6)
|
|
2 & 154 & 182 & 219 & 7.0 & 1.2 & 939.9 & 941.2 & 1.3 & 145 & 174 & 209 & 81 & 103 & 132 \\ % 213 - 26 ms (25+1+0) (#1,3,5)
|
|
3 & 139 & 147 & 163 & 7.0 & 1.3 & 941.1 & 949.2 & 8.1 & 131 & 140 & 156 & 76 & 80 & 88 \\ % 283 - 39 ms (38+1+0) (#1,4,5)
|
|
4 & 128 & 134 & 141 & 7.0 & 1.3 & 946.0 & 946.6 & 0.6 & 121 & 127 & 133 & 72 & 75 & 78 \\ % 353 - 52 ms (51+1+0) (#1,4,5)
|
|
5 & 123 & 129 & 134 & 7.0 & 1.5 & 946.7 & 947.8 & 1.1 & 116 & 122 & 127 & 65 & 68 & 72 \\ % 423 - 67 ms (66+1+0) (#1,4,5)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit Materialized View}
|
|
\label{tbl:measure-materialized-view}
|
|
\end{table}
|
|
|
|
Wie in \autoref{tbl:measure-materialized-view} zu sehen, bringt die Verwendung der \textit{Materialized View} eine
|
|
Verbesserung in verschiedenen Punkten. Zum einen ist eine Verbesserung der Aufrufzeiten zu erkennen, zusätzlich fällt
|
|
der Speicheranstieg weniger stark aus. Die Verbesserung der Aufrufzeiten lässt sich zusätzlich erklären, dass hier nun
|
|
nur noch vier statt der sechs an die Datenbank gestellt werden, da die Einzelabfragen für die Adressen der
|
|
Personen und der Koautoren komplett entfallen.
|
|
|
|
Nach dem der Quellcode nochmal untersucht wurde, konnte man festellen, dass bei jeder Anfrage die gleiche Bedingung
|
|
benötigt wurden. Da die Sicht nun explizit für dies Anfrage geschaffen wurde, wurde die Bedingungen nun direkt in die
|
|
Sicht mit integriert. Dies bedeutet eine Erweiterung der Sicht aus \autoref{lst:sql-materialized-view} um
|
|
\autoref{lst:sql-materialized-view-ext} und das Entfernen der Parameter aus dem SQL"=Anfragen im Java"=Code.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={SQL Materialized View Erweiterung},label=lst:sql-materialized-view-ext]
|
|
WHERE d.validuntil > NOW()
|
|
AND d.ispublishedindb = true;
|
|
\end{lstlisting}
|
|
|
|
Nach dem Anpassungen haben sich dann die Werte aus \autoref{tbl:measure-materialized-view-ext} ergeben. Diese Werte
|
|
zeigen nur minimale Unterschiede in den Zeiten, diese sind auf Messtoleranzen zurückzuführen.
|
|
|
|
\begin{table}[h!]
|
|
\centering
|
|
\resizebox{\textwidth}{!}{
|
|
\begin{tabular}{|r|r|r|r|r|r|r|r|r|r|r|r|r|r|r|}
|
|
\hline
|
|
& \multicolumn{3}{c|}{Aufrufzeit (ms)} & \multicolumn{2}{c|}{Queries (ms)} & \multicolumn{3}{c|}{Memory (MB)} & \multicolumn{3}{c|}{Render (ms)} & \multicolumn{3}{c|}{DB-load (ms)} \\
|
|
\hline
|
|
\# & min & avg & max & \#"=avg & avg & start & stop & diff & min & avg & max & min & avg & max \\
|
|
\hline
|
|
1 & 241 & 348 & 859 & 16.8 & 2.5 & 896.0 & 932.4 & 36.4 & 232 & 331 & 803 & 132 & 174 & 334 \\ % 168 - 25 ms (14+11+0) (#1,2,8)
|
|
2 & 164 & 194 & 225 & 9.0 & 2.4 & 933.3 & 935.9 & 2.6 & 154 & 185 & 215 & 79 & 99 & 117 \\ % 258 - 49 ms (30+18+1) (#1,2,8)
|
|
3 & 147 & 161 & 179 & 9.0 & 2.4 & 935.8 & 938.8 & 3.0 & 139 & 152 & 167 & 68 & 77 & 86 \\ % 348 - 73 ms (45+27+1) (#1,2,7)
|
|
4 & 135 & 145 & 183 & 9.0 & 2.4 & 939.4 & 936.0 & -3.4 & 127 & 137 & 174 & 70 & 73 & 75 \\ % 438 - 97 ms (61+35+1) (#1,2,7)
|
|
5 & 126 & 137 & 154 & 9.0 & 2.4 & 936.1 & 939.1 & 3.0 & 118 & 129 & 143 & 66 & 72 & 79 \\ % 528 - 121 ms (76+44+1) (#1,2,7)
|
|
\hline
|
|
\end{tabular}
|
|
}
|
|
\caption{Messung mit erweiterter Materialized View}
|
|
\label{tbl:measure-materialized-view-ext}
|
|
\end{table}
|
|
|
|
Da bei der \textit{Materialized View} das Laden der Daten und das Wandeln in die Java"=Objekte getrennt programmiert
|
|
wurde, können hier eigene Zeitmessungen für die zwei Schritte eingebaut werden. Hierfür wird die Zeit vor dem
|
|
\texttt{map}"=Aufruf und der \texttt{map}"=Aufruf gemessen. Für den erste Messung, wurde vor der Datenbankabfrage ein
|
|
\texttt{SearchDocument} Objekt erzeugt und dieses in jedem \texttt{map}"=Aufruf zurückgegeben. Mit dieser Aufbau wurde
|
|
\texthl{die Zeit ermittelt, um die Daten aus der Datenbank zu laden und dieses Ergebnis einmalig zu durchlaufen} ohne ein \todo{verena}
|
|
Objekt zu erstellen. Hierbei lagen die Zeiten bei circa 1 ms für das reine Laden der Daten und 3 ms
|
|
für den Aufruf der \texttt{map}"=Funktion. Sobald innerhalb der \texttt{map}"=Funktion pro Eintrag ein Objekt
|
|
erzeugt wird, ohne die Konvertierung der ermittelten Daten in das Objekt, steigt die Laufzeit schon auf 54 ms.
|
|
Wenn man nun noch die Konvertierung der Daten mit einbaut, erhöht sich die Laufzeit nochmals, auf nun 82 ms.
|
|
Alleine für das Erzeugen der Objekte und der Json"=Parse Aufrufe wird die meiste Zeit aufgewendet.
|
|
|
|
Bei der Verwendung des Hints \texttt{openjpa.""FetchPlan.""FetchBatchSize} kann die Abfrage enorm verschlechtern. Wenn
|
|
dieser Wert zu klein oder groß definiert ist, wird die Laufzeit verschlechtert. Bei einem zu großen Wert wird die
|
|
Laufzeit der Datenbankanfrage auf circa 20 ms verlängert. Wenn der Wert zu gering gewählt ist, dann wird zwar die
|
|
Laufzeit der Datenbankanfrage minimal verkürzt, aber die \texttt{map}"=Funktion wird dadurch verlängert.
|
|
|
|
Das Aktivieren der Cache"=Optionen wie in \autoref{sec:performance-investigation-application:caching-openjpa} oder in
|
|
\autoref{sec:performance-investigation-application:cached-query} dargestellt, haben keine Auswirkung auf die Performance.
|
|
Dies ist dadurch erklärbar, das keine Objekte durch das OpenJPA"=Framework erstellt werden, sondern erst in der
|
|
\texttt{map}"=Funktion des eigenen Codes und daher wird der Cache nicht genutzt.
|
|
|
|
Wie schon ermittelt, benötigt das Erstellen der Objekte den Großteil der Zeit für die Datenermittlung. Auf Grund dessen
|
|
wurde die übernommene \texttt{SearchDocument}"=Klasse abermals genauer betrachtet. Beim Erstellen werden die
|
|
\textit{Json}"=Daten direkt in Java"=Objekte gewandelt. Im ersten Schritt wird die Parse"=Funktion entfernt und die
|
|
Seite nochmals aufgerufen. Durch diese Umstellung fällt die Laufzeit der Datenermittlung auf circa 4 ms ab. Nun muss
|
|
noch überprüft werden, welche Zeit nun der Client zum parsen der \textit{Json}"=Daten benötigt. Hierfür werden die
|
|
Daten in einem versteckten \texttt{span}"=Element hinterlegt, wie es im \autoref{lst:jsf-datatable-json} zu sehen ist.
|
|
Die hinterlegte \textit{CSS}"=Klasse ist zum auffinden der Elemente für den späteren Javascript. Das
|
|
\textit{ajax}"=Element im Beispiel ist notwendig, damit bei einem Seitenwechsel die gleiche Interpreter"=Funktion für
|
|
die \textit{Json}"=Daten aufgerufen wird, wie beim laden der Webseite.
|
|
|
|
\begin{lstlisting}[language=xml,caption={DataTable mit Json},label=lst:jsf-datatable-json]
|
|
<p:ajax event="page" oncomplete="convertJsonData()"/>
|
|
<p:column id="author" headerText="#{lang.List_Docs_Author}" sortable="true" sortBy="#{myObj.surname}">
|
|
<h:outputText styleClass="json-convert" style="display: none;" value="#{myObj.authorJson}"/>
|
|
</p:column>
|
|
<p:column id="Addressee"
|
|
headerText="#{lang.List_Docs_Addressee}"
|
|
sortable="true"
|
|
sortBy="#{(myObj.addresseePersonSet!=null and myObj.addresseePersonSet.size() > 0)?myObj.addresseePersonSet[0].addresseePerson:''}">
|
|
<h:outputText styleClass="json-convert" style="display: none;" value="#{myObj.adresseeJson}" data="abc"/>
|
|
</p:column>
|
|
\end{lstlisting}
|
|
|
|
Die Interpreter"=Funktion, welche in JavaScript geschrieben ist, wird benötigt um die übertragenen
|
|
\textit{Json}"=Daten in eine darstelle Form zu bringen. Die Funktion aus dem \autoref{lst:jsf-datatable-json-convert}
|
|
ermittelt erst alle versteckten Elemente, deserialisiert den Inhalt und erstellt neue \textit{HTML}"=Elemente mit dem
|
|
darzustellenden Inhalt. Zusätzlich wird noch eine Zeitmessung eingebaut, um die Laufzeit am Client für das Rendern
|
|
in der Konsole anzuzeigen. Die Funktion wird nun direkt nach dem die Webseite fertig geladen wurde aufgerufen.
|
|
|
|
\begin{lstlisting}[language=javascript,caption={Wandeln von Json nach Html},label=lst:jsf-datatable-json-convert]
|
|
function isEmpty(str) {
|
|
return (str === null) || (str === undefined) || (typeof str === "string" && str.length === 0);
|
|
}
|
|
function convertJsonData() {
|
|
let $jsonObj = $(".json-convert")
|
|
, start = new Date()
|
|
;
|
|
|
|
$.each($jsonObj, function() {
|
|
let json = this.innerHTML
|
|
, strEmpty = (json === null) || (typeof json === "string" && json.length === 0)
|
|
, jsonDat = strEmpty ? null : JSON.parse(json)
|
|
;
|
|
if(!strEmpty) {
|
|
let res = ""
|
|
, $that = $(this)
|
|
, $par = $that.parent()
|
|
;
|
|
$.each(jsonDat, function() {
|
|
let hasOnlyOne = isEmpty(this.surname) || isEmpty(this.firstname)
|
|
, pseudonymExists = !isEmpty(this.pseudonym)
|
|
, namePart = "<span>" + (hasOnlyOne ? this.surname + this.firstname : this.surname + ", " + this.firstname) + "</span><br/>"
|
|
, pseudoPart = pseudonymExists ? "<span class='w3-small w3-text-dark-gray'>" + this.pseudonym + "</span><br/>" : ""
|
|
;
|
|
res += namePart + pseudoPart;
|
|
});
|
|
$par.append(res);
|
|
}
|
|
});
|
|
let end = new Date()
|
|
, diff = (end - start)
|
|
;
|
|
console.log(Math.round(diff) + " ms");
|
|
}
|
|
|
|
$(document).ready(function() {
|
|
convertJsonData();
|
|
});
|
|
\end{lstlisting}
|
|
|
|
Da nun am Client der Code ausgeführt wird, nachdem die Daten übertragen wurden, kann nicht mehr alles über das Script
|
|
durchgeführt werden. Daher werden nun die Laufzeiten am Server und am Client zusammenaddiert. Im Schnitt benötigt der
|
|
Aufruf auf der Serverseite nun 70 ms und am Client sind es circa 13 ms. Die summierte Laufzeit von Client und Server
|
|
ist geringer als die reine Serverlösung und erzeugt gleichzeit weniger Last am Server. \mytodos{verena}
|
|
|
|
\section{Optimierung der Abfrage}
|
|
\label{sec:performance-investigation-application:optimizing-query}
|
|
|
|
Für die Optimierung der Abfragen werden diese zuerst mit \texttt{EXPLAIN}, wie in \autoref{lst:explain-diagnostic}
|
|
dargestellt, untersucht. Für die einfachere Diagnose, wird der erstellte Plan mit Hilfe von
|
|
\textit{Postgres Explain Visualizer 2} (\url{https://github.com/dalibo/pev2}) visualisiert.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={Explain für Diagnose},label=lst:explain-diagnostic]
|
|
explain (analyze, verbose, buffers, summary, format json)
|
|
select <Spalten>
|
|
from
|
|
public.document t0
|
|
left outer join public.historicalperson t1 on t0.authorperson_id = t1.id
|
|
left outer join public.sitecity t5 on t0.city_id = t5.id
|
|
left outer join public.appuser t6 on t0.editor_id = t6.id
|
|
left outer join public.extendedbiography t2 on t1.extendedbiography_id = t2.id
|
|
left outer join public.sitecity t3 on t1.sitecity_birth_id = t3.id
|
|
left outer join public.sitecity t4 on t1.sitecity_death_id = t4.id
|
|
left outer join public.appuserrole t7 on t6.appuserrole_id = t7.id
|
|
where (t0.validuntil > NOW()
|
|
and t0.ispublishedindb = true)
|
|
order by startyear DESC, startmonth DESC, startday DESC
|
|
limit 400;
|
|
\end{lstlisting}
|
|
|
|
Die erstellte Visualisierung der Abfrage ist in \autoref{fig:explain-visualize} zu sehen. In der Visualisierung wurde
|
|
die Darstellung der Kosten gewählt, da ein Vergleich auf Basis der Zeit sehr schwierig ist und von äußeren Faktoren
|
|
abhängt, wie zum Beispiel dem Cache. Die Kosten sind stabiler und hängen in erster Linie von der Art des Datenbestandes ab.
|
|
|
|
\begin{figure}[h!]
|
|
\includegraphics[width=\linewidth]{gfx/chapter05_ExplainVisualize.png}
|
|
\caption{Visualisierung EXPLAIN}
|
|
\label{fig:explain-visualize}
|
|
\end{figure}
|
|
|
|
In der Graphik ist zu sehen, dass zum einen die Hauptkosten im untersten Knoten \textit{Seq Scan} und einen der
|
|
obersten Knoten dem \textit{HashAggregate} liegen. Zusätzlich sieht man anhand der Stärke von den Verbindungslinien der
|
|
Knoten, dass die Menge der Datensätze enorm hoch ist und diese sich bis zum obersten Knoten durchziehen. Dies
|
|
bedeutet, dass die Einschränkung des Datenbestandes erst am Ende der Abfrage durchgeführt wird und diesbezüglich die
|
|
Dauer der Abfrage linear mit den Inhalt der \textit{document}"=Tabelle zusammenhängt. Des Weiteren wird für keine
|
|
Tabelle ein \textit{Index Scan} verwendet, sondern immer mit einem \textit{Seq Scan} gearbeitet, da durch das Ermitteln
|
|
des kompletten Datenbestandes der Optimizer entscheidet, ob der komplette Scan der Tabelle kostengünstiger ist, als
|
|
die Verwendung eines der vorhandenen Indexe. Dies kann durch den Befehl \lstinline[language=SQL]|SET enable_seqscan = off|
|
|
sehr einfach verifiziert werden. Damit wird die Verwendung von \textit{Seq Scan} deaktiviert und es wird anschließend ein
|
|
\textit{Index Scan} verwendet. Wenn man nun beide Pläne vergleicht sieht man die Erhöhung der Kosten bei der Verwendung
|
|
von \textit{Index Scan}.
|
|
|
|
Die beste Optimierung hierbei ist, die Menge der Datensätze so früh wie möglich einzuschränken. Da die Verwendung von
|
|
\texttt{order by} innerhalb eines Sub"=Selects nicht erlaubt ist, verwenden wir hierfür eine \textit{Common Table
|
|
Expression}, wie es in \autoref{lst:explain-optimize-cte} zu sehen ist. Zusätzlich wurde noch ein Index auf der
|
|
\textit{document}"=Tabelle für die Spalten der Bedingung und der Sortierung gesetzt, wie in
|
|
\autoref{lst:explain-optimize-cte-idx} zur sehen, damit in der \textit{Common Table Expression} nur der Index verwendet
|
|
werden kann und kein zusätzlicher Zugriff in die Tabelle notwendig ist.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={Optimierung mit Common Table Expression},label=lst:explain-optimize-cte]
|
|
with doc_limit as (
|
|
select id
|
|
from public.document
|
|
where validuntil > now()
|
|
and ispublishedindb = true
|
|
order by startyear desc, startmonth desc, startday desc
|
|
limit 400
|
|
)
|
|
select *
|
|
from doc_limit t
|
|
join public.document t0 on t0.id = t.id
|
|
order by t0.startyear desc, t0.startmonth desc, t0.startday desc
|
|
\end{lstlisting}
|
|
|
|
\begin{lstlisting}[language=SQL,caption={Index für Common Table Expression},label=lst:explain-optimize-cte-idx]
|
|
create index idx_document_with_stmt on document using btree
|
|
( ispublishedindb, validuntil, startyear desc, startmonth desc
|
|
, startday desc, id );
|
|
\end{lstlisting}
|
|
|
|
Mit diesen Umstellungen erkennt man, dass die Kosten entsprechend gefallen sind. Ebenfalls konnten die Laufzeit um
|
|
mehr als den Faktor drei reduziert werden. Die Optimierung ist in \autoref{fig:explain-visualize-with} sehr deutlich
|
|
an den dünneren Verbindungslinien zwischen den Knoten und der Umstellung von einigen \textit{Seq Scan} zu
|
|
\textit{Index Scan} ersichtlich. Zeitgleich ist auch der teure \textit{HashAggregate} nicht mehr im Abfrageplan
|
|
vorhanden.
|
|
|
|
\begin{figure}[h!]
|
|
\includegraphics[width=\linewidth]{gfx/chapter05_ExplainVisualize_with.png}
|
|
\caption{Visualisierung EXPLAIN with}
|
|
\label{fig:explain-visualize-with}
|
|
\end{figure}
|
|
|
|
Bei der Untersuchung der Abfrage zur \textit{Materialized View} ist direkt herausgekommen, dass hier keine Optimierung
|
|
mehr möglich ist, da durch die definierten Indexe bei den aktuell möglichen Sortierkriterien direkt ein
|
|
\textit{Index Scan} verwendet wird. Dies ist durch eine Überprüfung der Abfragepläne beweisbar, für diesen Fall wird
|
|
die Abfrage aus \autoref{lst:explain-search-document} verwendet.
|
|
|
|
\begin{lstlisting}[language=SQL,caption={xxxl},label=lst:explain-search-document]
|
|
explain (analyze)
|
|
select sd.id, documentid, datetype, startyear, startmonth, startday
|
|
, startdatestatus , endyear, endmonth, endday, enddatestatus
|
|
, author, coauthors, addressees, city, documentcategory
|
|
, ispublishedindb, createdat, modifiedat, validuntil
|
|
from searchdocument sd
|
|
order by startyear desc, startmonth desc, startday desc
|
|
limit 400;
|
|
\end{lstlisting}
|
|
|
|
Der dazugehörige Abfrageplan ist in \autoref{lst:explain-search-document-output} zu sehen, hierbei ist die erste
|
|
Ausgabe mit dem erstellten Index. Vor der zweiten Ausgabe wurde dieser Index deaktiviert. Anhand der Ausgabe ist zu
|
|
sehen, dass bei der Verwendung des Index weniger Operation notwendig sind und damit auch die teure Sortierung
|
|
eingespart werden konnte. Dies liegt daran, dass der Index entsprechend des Sortierkriterien definiert wurde und
|
|
somit ist es möglich, direkt in dem Index die Elemente in der richtigen Reihenfolge zu ermitteln.
|
|
Somit ist durch den Index der schnellstmögliche Zugriff gegeben.
|
|
|
|
\begin{lstlisting}[basicstyle=\scriptsize,caption={aa},label=lst:explain-search-document-output]
|
|
Limit (cost=0.28..144.92 rows=400 width=948) (actual time=0.035..0.660 rows=400 loops=1)
|
|
-> Index Scan Backward using idx_searchdocument_startdate on searchdocument sd (cost=0.28..1911.30 rows=5285 width=948) (actual time=0.033..0.593 rows=400 loops=1)
|
|
Planning Time: 0.199 ms
|
|
Execution Time: 0.732 ms
|
|
|
|
Limit (cost=747.69..748.69 rows=400 width=948) (actual time=2.128..2.146 rows=400 loops=1)
|
|
-> Sort (cost=747.69..760.90 rows=5285 width=948) (actual time=2.127..2.135 rows=400 loops=1)
|
|
Sort Key: startyear DESC, startmonth DESC, startday DESC
|
|
Sort Method: top-N heapsort Memory: 703kB
|
|
-> Seq Scan on searchdocument sd (cost=0.00..492.85 rows=5285 width=948) (actual time=0.006..0.943 rows=5285 loops=1)
|
|
Planning Time: 0.056 ms
|
|
Execution Time: 2.164 ms
|
|
\end{lstlisting}
|