ORACLE – DATENBANKOPTIMIERUNG (BASICS) INHALT 1 Motivation ........................................................................................................................................... 1 2 Automatische, regelmäßige DB-Optimierung ..................................................................................... 2 2.1 Index-Rebuild ................................................................................................................................ 2 2.2 Tabellen-Reorganisation ............................................................................................................... 2 2.3 Statistiken ermitteln ..................................................................................................................... 3 2.4 Alles automatisiert ........................................................................................................................ 3 3 Performance-Monitoring..................................................................................................................... 4 3.1 V$SESSION_LONGOPS................................................................................................................... 4 3.2 V$SQL, V$SQL_AREA ..................................................................................................................... 5 4 Performance-Tuning durch DB-Indizes................................................................................................ 6 4.1 Fremdschlüssel-Indizes ................................................................................................................. 6 4.2 Identifizierung anhand hoher Kosten bei SQL-Statements .......................................................... 6 5 Zusammenfassung ............................................................................................................................... 9 1 MOTIVATION In einem Kundenprojekt standen wir vor dem Problem, dass die Anwendung – und scheinbar auch die Oracle-Datenbank-Instanz selber - über die Jahre immer langsamer geworden ist. Wir haben das Problem lange Zeit auf die ständig anwachsenden Datenmengen und mangelnde Optimierung in der Persistenzschicht der Applikation geschoben. Als ein externer Oracle-Berater dann einen Blick auf die Produktiv-Datenbank geworfen hat, wurden uns einige Nachlässigkeiten im Umgang mit der Datenbank aufgezeigt. Im Folgenden möchte ich die wesentlichen Punkte der Performance-Analyse und -Tunings kurz skizzieren. Dies ist sicherlich kein Oracle-Optimierungs-Workshop für Fortgeschrittene ist, sondern vielmehr wird das Rüstzeug eines Anwendungsentwicklers beschrieben, der z.B. per JDBC-Treiber auf Oracle-Datenbanken zugreift. Desweiteren sollte man (ggf. mit Hilfe eines Oracle-Experten) ausfindig machen, ob der Datenbankserver selber richtig dimensioniert ist. Man kann mithilfe geeigneter SQL-Statements und Tools leicht herausfinden, ob es z.B. einen File-IO-Engpass auf dem DB-Server gibt oder einen Hauptspeichermangel. Wenn solche „Mängel“ vorliegen, sollten natürlich auf der Hardware-Seite entsprechende Aufrüstungsmaßnahmen ergriffen werden. Die folgenden Aufgaben liegen teilweise in der Verantwortung des Datenbankadministrators und teilweise in der des Softwareentwicklers. 2 AUTOMATISCHE, REGELMÄßIGE DB-OPTIMIERUNG Der DB-Administrator sollte dafür sorgen, dass folgende Aufgaben regelmäßig ausgeführt werden: (Regelmäßig ist ein sehr relativer Begriff – aber das hängt stark von den einzelnen Datenbanken, der Verfügbarkeit, der Anzahl Schreibzugriffe, etc. ab. Es schadet aber nicht, mindestens jede Woche, z.B. am Wochenende, oder auch jede Nacht, wenn weniger oder keine Last auf der DB herrscht, die Optimierungen automatisch durchführen zu lassen (z.B. per SQL mittels Cron-Job – wahrscheinlich geht das auch mit Oracle-Administrationstools). 2.1 INDEX-REBUILD Durch Analyse der Systemtabelle „USER_INDEXES“ und geeigneten SQL-Befehlen kann man herausfinden, welche Indices degeneriert sind und neu angelegt werden sollten – aber auch, welche Indizes überflüssig sind. Index-Rebuild mittels: ALTER INDEX <IndexName> REBUILD; Es ist sinnvoll, alle Indizes in einem eigenen Tablespace zu speichern. Gleichzeitig mit einem IndexRebuild kann man ggf. das Verschieben in den Tablespace „INDX“ folgendermaßen durchführen: ALTER INDEX <IndexName> REBUILD TABLESPACE INDX; 2.2 TABELLEN-REORGANISATION Eine Defragmentierung von Tabellen durch deren Neuaufbau(„Rebuild“) ist von Zeit zu Zeit durchzuführen: ALTER TABLE <TableName> MOVE TABLESPACE USERS; Verschiebt die Tabelle in einen anderen Tablespace und erstellt sie neu. Wenn der Tablespace derselbe ist wie zuvor, so wird die Tabelle lediglich reorganisiert (defragmentiert). Eine Reorganisation einer Tabelle bzw. eines DB-Index wird empfohlen, wenn sich die Datenmenge durch viele INSERT oder DELETE-Statements oder Importe signifikant verändert hat! 2.3 STATISTIKEN ERMITTELN Oracle benötigt u.a. zur Optimierung von SQL-Abfragen aktuelle Statistiken über alle Tabellen und Indizes. Diese müssen regelmäßig ermittelt werden. Je nach Konfiguration der Datenbank werden die Statistiken automatisch neu erzeugt. Bei einem Datenbankimport geschieht das typischerweise auch automatisch. Ermittlung, wann zuletzt Statistiken über DB-Entitäten erhoben worden sind: SELECT owner, table_name, last_analyzed FROM owner='APPLICATION_XYZ'; dba_tab_statistics WHERE SELECT table_name, last_analyzed from dba_tables where Owner = 'APPLICATION_XYZ'; SELECT index_name, last_analyzed from dba_indexes where Owner = 'APPLICATION_XYZ'; EXIT; Manuelle Ermittlung der Statistiken: // internal DataDictionary-Stats: call dbms_stats.gather_fixed_objects_stats(); // System-Stats: call dbms_stats.gather_dictionary_stats(); // Perform object statistics for tables and indices CALL dbms_stats.gather_index_stats('<User>', '<IndexName>'); CALL dbms_stats.gather_Table_stats('<User>', '<TableName>'); 2.4 ALLES AUTOMATISIERT Folgendes SQL-Skript erzeugt alle Statements, um sämtliche Indizes und Tabellen zu reorganisieren und alle Statistiken neu zu berechnen. Diese Statements können dann regelmäßig – zum Beispiel jedes Wochenende - ausgeführt werden. SET PAGESIZE 0; SET LINESIZE 200; call dbms_stats.gather_dictionary_stats(); SPOOL tmp_OracleRebuildIndices.sql SELECT 'ALTER INDEX '||owner||'.'||index_name||' REBUILD TABLESPACE INDX;' FROM dba_indexes WHERE Owner = 'APPLICATION_XYZ'; SPOOL OFF SPOOL tmp_OracleGatherIndexStats.sql SELECT 'CALL dbms_stats.gather_index_stats(''COMPIERE'', '''|| index_name ||''');' FROM dba_indexes WHERE Owner = 'APPLICATION_XYZ'; SPOOL OFF SPOOL tmp_OracleAnalyzeIndices.sql SELECT 'ANALYZE INDEX '||owner||'.'||index_name||' COMPUTE STATISTICS;' FROM dba_indexes WHERE Owner = 'APPLICATION_XYZ'; SPOOL OFF SPOOL tmp_OracleGatherTableStats.sql SELECT 'CALL dbms_stats.gather_table_stats(''COMPIERE'', '''|| table_name ||''');' FROM dba_tables WHERE Owner = 'APPLICATION_XYZ'; SPOOL OFF exit; 3 PERFORMANCE-MONITORING Oracle besitzt mit den V$_-Views sogenannte Dynamic Performance Views. Sie enthalten dynamische Informationen zu laufenden und vergangenen SQL-Anfragen, DB-Sessions, und vielem mehr, die vom DB-Server automatisch ermittelt und bei Abfrage durch den SYSTEM-User aktualisiert werden. Wichtige Views sind z.B. V$SQL, V$SQLAREA, V$SESSION, V$SESSION_LONGOPS. 3.1 V$SESSION_LONGOPS Auflisten teurer, langlaufender Aktionen mit ihrem verursachten „Aufwand“ und Laufzeit in der DB: select OPName, Target, Message, TOTALWORK, ELAPSED_SECONDS, START_TIME, Last_Update_Time from V$Session_LongOps where username='COMPIERE' order by TOTALWORK desc Ergebnis z.B.: OPNAME TARGET MESSAGE TOTALWORK ELAPSED_SECONDS ------------------------------------------------------------------------------Table Scan COMPIERE.OCP_CRM_CORRESPONDENCE Table Scan: 88381 Table Scan 11 COMPIERE.PWORKITEMS Table Scan: 82432 Table Scan COMPIERE.PWORKITEMS: 82432 out of 82432 Blocks done 43 COMPIERE.PBPEPROCESSINSTS Table Scan: 23552 Table Scan COMPIERE.OCP_CRM_CORRESPONDENCE: 88381 out of 88381 Blocks done COMPIERE.PBPEPROCESSINSTS: 23552 out of 23552 Blocks done 66 COMPIERE.M_TRANSACTION Table Scan: 19277 COMPIERE.M_TRANSACTION: 19277 out of 19277 Blocks done 18 Massnahmen: Langlaufende Operationen oder teure “Full Table Scans“ könnten auf fehlende oder nicht optimale DB-Indizes hindeuten. Eventuell kann man auch DB-Zugriffe in der Anwendung optimieren (weniger Daten holen, Cachings einbauen, etc.). 3.2 V$SQL, V$SQL_AREA 3.2.1 Ermittlung der Ausführungsdauer von SQL-Abfragen (Top 10): select * from ( select sql_text,executions,cpu_time from v$sql where executions>0 order by cpu_time/executions desc )where rownum<10; 3.2.2 Teure SQL-Statements auflisten: SELECT buffer_gets/(executions+1), executions, buffer_gets, sql_text FROM v$SQLAREA WHERE buffer_gets/(executions+1) >= 20 ORDER BY 1 DESC 3.2.3 Aufspüren unperformanter SQL-Befehle: Der folgende Befehl zeigt für die SQL-Befehle, die sich im SQL-Cache befinden, wie oft sie ausgeführt wurden und wie viele Blockzugriffe sie zur Abarbeitung benötigten. (BUFFER_GET = Blockzugriff = Lesen eines Speicherbereiches von der Harddisk in den Speicher relativ teure Operation) Außerdem wird die Trefferquote des Befehls im Datencache angezeigt. Damit lassen sich sehr schnell "schlecht optimierte" SQL-Befehle herausfinden. Trefferquoten kleiner 70% deuten regelmäßig darauf hin, dass eine Tabelle vollständig ohne Index-Zugriffe gelesen wird. Da die Statistik nur die Befehle anzeigt, die sich gerade im SQL-Cache befinden, muss die folgende Abfrage ggf. mehrfach täglich zu unterschiedlichen Zeitpunkten aufgerufen werden. SELECT TO_CHAR(EXECUTIONS, '999G999G990') "EXECUTIONS", TO_CHAR(BUFFER_GETS, '999G999G990') "GETS", TO_CHAR(BUFFER_GETS/GREATEST(NVL(EXECUTIONS,1),1), '999G999G990') "GETS_PER_EXEC", TO_CHAR (ROUND(100*(1-(DISK_READS/GREATEST(NVL(BUFFER_GETS,1),1))),2), '990D00') TREFFERQUOTE, SQL_TEXT FROM V$SQL WHERE BUFFER_GETS > 1000 ORDER BY BUFFER_GETS DESC Diese Query sollte man nach verschiedenen Spalten filtern/sortieren, um unterschiedliche Aussagen zu erhalten. Man kann sich z.B. SQL-Abfragen anzeigen lassen mit geringer Trefferquote („WHERE TrefferQuote < 80“), vielen IO-Zugriffen („WHERE GETS_PER_EXEC > 10000“) , vielen Ausführungen („WHERE EXECUTIONS > 100“), etc. Beispielhafte Ergebnisse: EXECUTIONS GETS GETS_PER_EXEC TREFFERQUOTE SQL_TEXT -------------------------------------------------------------------------------------------- 276 10,696,208 38,754 99.91 SELECT AD_User.AD_User_ID,NULL... 115 1,872,464 16,282 99.70 DELETE FROM OCE_ProdInvMovement WHERE... 115 1,859,686 16,171 99.84 DELETE FROM OCE_ProductionOrderLine WHERE 29 1,853,698 63,921 91.94 SELECT SUM(ol.LineNetAmt * (CASE WHEN... TREFFERQUOTE SQL_TEXT …Where where Trefferquote < 80…: EXECUTIONS GETS GETS_PER_EXEC -------------------------------------------------------------------------------------------36 653,400 18,150 78.72 SELECT OCP_DAMDocument_ID, Name FROM... 23 443,666 19,290 48.79 select MovementType, MovementQty from... 10 203,048 20,305 68.44 SELECT l.OCE_ProductionOrder_ID, ... 10 182,580 18,258 72.65 SELECT COUNT(ROWNUM) FROM OCP_DAMDocument 7 127,050 18,150 77.04 SELECT OCP_DAMDocument_ID FROM ... 4 PERFORMANCE-TUNING DURCH DB-INDIZES Bei der Implementierung von mehrschichtigen Java-Anwendungen und Verwendung von PersistenzFrameworks (wie z.B. Hibernate) vergisst man leicht, dass datenbanknahe Optimierungen, wie die Einführung geeigneter Datenbank-Indizes, ein enormes Potential an Performanceverbesserung bieten. 4.1 FREMDSCHLÜSSEL-INDIZES Generell sollten eigentlich alle Fremdschlüssel einer Tabelle einen eigenen Index besitzen. Beispiel: CREATE TABLE department ( deptno , dname NUMBER(2) CONSTRAINT PK_DEPT PRIMARY KEY VARCHAR2(14) ); CREATE TABLE employee ( empno NUMBER(4) CONSTRAINT PK_EMP PRIMARY KEY , ename VARCHAR2(10) , deptno NUMBER(2) ); Anlegen eines Fremdschlüssel-Indizes: CREATE INDEX emp_deptno ON employee(deptno) TABLESPACE indx; 4.2 IDENTIFIZIERUNG ANHAND HOHER KOSTEN BEI SQL-STATEMENTS Ziel: Full Table Scans auf größere Tabellen sind möglichst zu vermeiden – beispielsweise durch Einführen eines einen DB-Index. Vorgehen: 1) Man identifiziert anhand der obigen Monitoring-Möglichkeiten ein SQL-Statement, welches teuer in der Ausführung ist und/oder sehr häufig ausgeführt wird. 2) Für dieses SQL-Statement lässt man einen Ausführungsplan erstellen (in vielen SQLTools und im Oracle SQL Developer möglich ; auf Kommandozeile mittels "explain plan ..." oder „Autotrace On“) Bsp.: Auf Kommandozeile: SQL> set autotrace on SQL> set linesize 200 SQL> SELECT <ColumnNames> FROM <TableName> JOIN <joins> WHERE <WhereClause>; 3) Für dieses SQL-Statement lässt man einen Ausführungsplan erstellen. Im Ausführungsplan kann man an den einzelnen Knoten die Kosten für die Teilstatements ablesen. Dort wo hohe Kosten sind, muss man optimieren. 4) Wenn dort "Table Access (Full)" steht, heißt das, dass kein (geeigneter) DB-Index zur Verfügung steht und ein „Full Table Scan“ durchgeführt wird, was bei großen Tabellen immer sehr schlecht ist, da alle Zeilen der Tabelle durchsucht werden müssen! Diese Full Table Scans sollte man weitestgehend eliminieren, indem man passende Indices einführt. 5) Nach der Index-Erstellung sollte erneut ein Ausführungsplan erstellt werden, um zu überprüfen, dass der Index genutzt wird und ob er kostenmäßig etwas bringt. 6) Beispiele: Bsp. 1) Fremdschlüssel-Index Zu optimierendes SQL-Statement: SELECT SUM(HandlingTime) FROM M_InOut WHERE OCE_RP_DAY_ID=1013025 AND IsActive='Y'; --> Der Execution Plan zeigt einen Full Table Scan bei der M_InOut-Tabelle an. Index erstellt: create index M_InOut_Day on M_InOut(OCE_RP_DAY_ID); -- Index Statistics generieren: CALL dbms_stats.gather_index_stats('COMPIERE', 'M_InOut_Day'); Anschließend lief der Befehl deutlich schneller und mit deutlich geringeren "Kosten" ab. Bsp. 2) Index für Spalten aus Where-Clause: Zu optimierendes SQL-Statement: SELECT SUM(OL.QTYORDERED) FROM C_ORDER O JOIN C_ORDERLINE OL ON O.C_ORDER_ID=OL.C_ORDER_ID LEFT OUTER JOIN M_INOUTLINE ML ON ML.C_ORDERLINE_ID=OL.C_ORDERLINE_ID LEFT OUTER JOIN M_INOUT M ON M.M_INOUT_ID=ML.M_INOUT_ID WHERE OL.M_PRODUCT_ID=415 AND o.AD_Org_ID=1000000 AND (o.DocStatus='DR' OR o.DOCSTATUS='IP') AND (m.DocStatus<>'CO' OR m.DocStatus IS NULL) AND o.ISSOTRX='N' AND o.PROCESSED='N' AND o.C_DocTypeTarget_ID NOT IN (SELECT C_DocType_ID FROM C_DocType WHERE Name='Supplier Claim') AND ((ol.DatePromised IS NULL AND ol.DateDesired <= SysDate+20) OR (ol.DatePromised IS NOT NULL AND ol.DatePromised <= SysDate+20)) Execution Plan (vor der Optimierung): ----------------------------------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 1 | 114 | | 1 | | | 1 | 114 | | | |* 2 | | | | | | | | 3 | | | 1 | 114 | 1263 (6)| 00:00:05 | | 4 | | | 25 | 2625 | 1239 (6)| 00:00:05 | |* 5 | HASH JOIN ANTI | | 25 | 2325 | 1194 (6)| 00:00:04 | | 6 | NESTED LOOPS | | 36 | 2160 | 1190 (6)| 00:00:04 | |* 7 | TABLE ACCESS FULL | C_ORDERLINE | 85 | 3060 | 1105 (6)| 00:00:04 | |* 8 | TABLE ACCESS BY INDEX ROWID| C_ORDER |* 9 | |* 10 | | 11 | |* 12 | | 13 | |* 14 | SORT AGGREGATE FILTER NESTED LOOPS OUTER NESTED LOOPS OUTER INDEX UNIQUE SCAN TABLE ACCESS FULL TABLE ACCESS BY INDEX ROWID INDEX RANGE SCAN TABLE ACCESS BY INDEX ROWID INDEX UNIQUE SCAN 1263 (6)| 00:00:05 | | 1 | 24 | 1 (0)| 00:00:01 | | C_ORDER_KEY | 1 | | 0 (0)| 00:00:01 | | C_DOCTYPE | 6 | 198 | 3 (0)| 00:00:01 | | M_INOUTLINE | 1 | 12 | 2 (0)| 00:00:01 | | M_INOUTLINE_ORDERLINE | 1 | | 1 (0)| 00:00:01 | | M_INOUT | 1 | 9 | 1 (0)| 00:00:01 | | M_INOUT_KEY | 1 | | 0 (0)| 00:00:01 | ----------------------------------------------------------------------------------------------------------- Hier sieht man, dass die meisten Kosten (1105 von 1263) des SQL-Statements durch den „Table Access Full“ auf die Tabelle C_ORDERLINE in Zeile 7 erzeugt werden. Dieser Zugriff sollte durch Einführen eines DB-Index optimiert werden. In diesem Bsp. Werden wir alle Spalten der gejointen Tabelle C_ORDERLINE in den Index aufnehmen, die in der Where-Bedingung abgefragt werden. Der zweite „Table Access Full“ in Zeile 10 kann ignoriert werden, weil die Tabelle C_DOCTYPE sehr klein ist (wenige Zeilen enthält) und ein „Full Table Scan“ kein Problem darstellt. Neuer DB-Index: create index C_ORDERLINE_QtyOrd on C_ORDERLINE(M_Product_ID, DatePromised, DateDesired); CALL dbms_stats.gather_index_stats('COMPIERE', 'C_ORDERLINE_QtyOrd'); Execution Plan (nach erfolgreicher Optimierung): ----------------------------------------------------------------------------------------------------------| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------------------------------------| 0 | SELECT STATEMENT | | 1 | 114 | | 1 | | | 1 | 114 | | | |* 2 | | | | | | | | 3 | | | 1 | 114 | | 4 | | | 25 | 2625 | 234 (1)| 00:00:01 | |* 5 | HASH JOIN ANTI | | 25 | 2325 | 189 (1)| 00:00:01 | | 6 | NESTED LOOPS | | 36 | 2160 | 185 (0)| 00:00:01 | |* 7 | | 85 | 3060 | 100 (0)| 00:00:01 | |* 8 | | 101 | | 3 (0)| 00:00:01 | |* 9 | |* 10 | |* 11 | | 12 | SORT AGGREGATE FILTER NESTED LOOPS OUTER NESTED LOOPS OUTER TABLE ACCESS BY INDEX ROWID| C_ORDERLINE INDEX RANGE SCAN | C_ORDERLINE_QTYORD TABLE ACCESS BY INDEX ROWID| C_ORDER INDEX UNIQUE SCAN TABLE ACCESS FULL TABLE ACCESS BY INDEX ROWID 258 258 (1)| 00:00:01 | (1)| 00:00:01 | | 1 | 24 | 1 (0)| 00:00:01 | | C_ORDER_KEY | 1 | | 0 (0)| 00:00:01 | | C_DOCTYPE | 6 | 198 | 3 (0)| 00:00:01 | | M_INOUTLINE | 1 | 12 | 2 (0)| 00:00:01 | |* 13 | | 14 | |* 15 | INDEX RANGE SCAN TABLE ACCESS BY INDEX ROWID INDEX UNIQUE SCAN | M_INOUTLINE_ORDERLINE | 1 | | 1 | M_INOUT | 1 | 9 | 1 (0)| 00:00:01 | (0)| 00:00:01 | | M_INOUT_KEY | 1 | | 0 (0)| 00:00:01 | ----------------------------------------------------------------------------------------------------------- 7) Weitere Hinweise: Indices auf Spalten mit geringer Diversität (schwach selektiv) machen nicht so viel Sinn! (Bsp.: IsActive (Y|N), State(a|b|c|d), AD_Client_ID (1000000, 1000001, ...)) Bei Tabellen, auf denen viel geschrieben wird, sind Indices teuer, da diese permanent aktualisiert werden müssen. 5 ZUSAMMENFASSUNG Man sollte daran denken, Oracle‘s Statistiken regelmäßig zu aktualisieren– sowohl interne Statistiken über die Systemtabellen als auch Statistiken über benutzerdefinierte Tabellen und Indizes. Mit veralteten Statistiken läuft man Gefahr, dass Oracle falsche SQL-Optimierungen vornimmt. Außerdem sollte man von Zeit zu Zeit Indizes rebuilden und Tabellen reorganisieren. Die meisten Performancegewinne erzielt man mit guten DB-Indizes. Diese erstellt man, indem man den Ausführungsplan (execution plan) von komplexen und/oder häufig auszuführenden SQLStatements analysiert und nach Verursachern von hohen Kosten durchsucht. Außerdem sollten DBIndizes in der Regel auf alle Fremdschlüssel angewendet werden.