Matthias: Performance bei Textzerlegung

Hallo,

ich habe eine Klasse gebaut, die eine BatchInput-Datei verarbeitet, bevor diese ins SAP eingespielt wird.

Die Datei wird zeilenweise gelesen, die einzelnen Bereiche aus der Zeile herausgeschnitten und in eine Tabelle eingetragen.

Danach findet ein Abgleich zwischen der gerade befüllten Tabelle und einer Stammtabelle statt, bei der verschiedene Kriterien abgeglichen werden.

Der Bearbeiter bekommt Abweichungen angezeigt, kann diese korrigieren bzw. die neuer Werte in die Stammtabelle übernehmen, und erhält am Ende wieder eine valide formatierte BatchInput-Datei zurück.

Funktioinert auch alles wunderbar.

Bei einer Datei mit 2.459 Zeilen dauert das Zerlegen und in die Tabelle schreiben zwischen 6 und 7 Sekunden. Und das ist noch eine der kleineren Dateien, 'normale' liegen bei um die 5.000 Zeilen.

Meine Frage ist, ob jemand einen Tip hat, wie ich die Verarbeitung performanter gestalten kann.

Hier mein Code:

  
$query  = "TRUNCATE TABLE  "  
         ."  table_name ";  
$result = sqlsrv_query( $this->DB_Connector, $query );  
echo $this->db_error( $result , $query );  
  
$datei_daten = file( $this->datei_path.$this->datei_name );  
  
foreach ( $datei_daten as $nr => $zeile )  
{  
  $A =               substr( $zeile ,   '0' ,  '8' );  
  $B =        ltrim( substr( $zeile ,   '8' , '10' ) , '0' );  
  $C =        ltrim( substr( $zeile ,  '18' , '12' ) , '0' );  
  $D =        ltrim( substr( $zeile ,  '30' , '10' ) , '0' );  
  $E =        ltrim( substr( $zeile ,  '40' , '12' ) , '0' );  
  $F =        ltrim( substr( $zeile ,  '52' , '10' ) , '0' );  
  $G = strtr( ltrim( substr( $zeile ,  '62' , '15' ) , '0' ) , ',' , '.' );  
  $H =         trim( substr( $zeile ,  '77' , ' 1' )       );  
  $I =        rtrim( substr( $zeile ,  '79' ,  '9' ) , ' ' );  
  $J =         trim( substr( $zeile ,  '88' , '10' )       );  
  $K =         trim( substr( $zeile ,  '99' , '20' )       );  
  $L =         trim( substr( $zeile , '120' ,  '8' )       );  
  $M =         trim( substr( $zeile , '128' , '20' )       );  
  
  $query  = "INSERT INTO  "  
           ."  table_name "  
           ."  (          "  
           ."    id,      "  
           ."    FieldA,  "  
           ."    FieldB,  "  
           [...]  
           ."    FieldM   "  
           ."  )          "  
           ."VALUES       "  
           ."  (          "  
           ."    '$nr',   "  
           ."    '$A',    "  
           ."    '$B',    "  
           [...]  
           ."    '$M'     "  
           ."  )          ";  
  $result = sqlsrv_query( $this->DB_Connector, $query );  
  echo $this->db_error( $result , $query );  
}  

Bin für jeden Tip dankbar.

Gruß

  1. Bin für jeden Tip dankbar.

    Wie siehen die Rohdaten aus? Ggf. hilft ein simples explode() :) alternativ wäre noch ein Regulärer Ausdruck denkbar und eleganter.

    Ebenfalls könntest du das File gleich Zeilenweise lesen und nicht mit foreach durchackern - dürfte ebenfalls performanter sein.

  2. Bin für jeden Tip dankbar.

    In bytecode umwandeln

    lg

  3. Hallo,

    Bei einer Datei mit 2.459 Zeilen dauert das Zerlegen und in die Tabelle schreiben zwischen 6 und 7 Sekunden. Und das ist noch eine der kleineren Dateien, 'normale' liegen bei um die 5.000 Zeilen.

    das dürfte vermutlich vor allem an den ca. 2.500 INSERT-Anweisungen liegen.

    Meine Frage ist, ob jemand einen Tip hat, wie ich die Verarbeitung performanter gestalten kann.

    a) Prepared Statements (serverseitige) verwenden.
    b) die Doku des DBMS befragen, ob das DBMS eine geeignetere Schnittstelle für
       Massen-Insert-Operationen hat - und wenn ja, diese bedienen.

    Freundliche Grüße

    Vinzenz

    1. Hallo,

      schon mal Danke für die Denkanstoesse.

      Hier als Beispiel eine Zeile aus der Datei. Mit explode oder regex wüsste ich nicht wie ich da rangehen soll.

      1002201000000111110000000000  0000000815            73020100  000000000051,10 WARENSEND KLEINTEIL   Herr Müller         Nr.12345LiefSch231

      Die Sache mit mit den substrings mit bestimmten Start- und Endpunkten ist hier die nach wie vor die einzige Lösung die mir einfällt.

      Ich habe

        
      $datei_daten = file( $this->datei_path.$this->datei_name );  
        
              foreach ( $datei_daten as $nr => $zeile )  
      
      

      ausgetauscht mit

        
      $handle = fopen ( $this->datei_path.$this->datei_name , "r" );  
              $nr     = '0';  
              while ( ! feof( $handle ) )  
              {  
        
                $nr++;  
                $zeile = fgets( $handle, '4096' );  
      
      

      , das brachte schonmal eine Ersparnis von 2 Sekunden.

      Dann habe ich mir die Geschichte mit Prepared-Statements angeschaut, das hatte dann aber länger gedauert. Muss mich da nochmal ein bisschen einlesen...

      Ich habe, aufgrund des Hinweises, dass die lange Zeit mit durch die vielen Insert-Statements bedingt ist, meine Logik wie folgt geändert:

        
      $handle = fopen ( $this->datei_path.$this->datei_name , "r" );  
      $nr     = '0';  
      $i      = '0';  
      $query  = '';  
      while ( ! feof( $handle ) )  
      {  
        $nr++;  
        $i++;  
        $zeile = fgets( $handle, '4096' );  
        
        //Leerzeilen abfangen  
        if ( strlen ( trim( $zeile ) ) > '0' )  
        {  
          $A =               substr( $zeile ,   '0' ,  '8' );  
          $B =        ltrim( substr( $zeile ,   '8' , '10' ) , '0' );  
          $C =        ltrim( substr( $zeile ,  '18' , '12' ) , '0' );  
          $D =        ltrim( substr( $zeile ,  '30' , '10' ) , '0' );  
          $E =        ltrim( substr( $zeile ,  '40' , '12' ) , '0' );  
          $F =        ltrim( substr( $zeile ,  '52' , '10' ) , '0' );  
          $G = strtr( ltrim( substr( $zeile ,  '62' , '15' ) , '0' ) , ',' , '.' );  
          $H =         trim( substr( $zeile ,  '77' , ' 1' )       );  
          $I =        rtrim( substr( $zeile ,  '79' ,  '9' ) , ' ' );  
          $J =         trim( substr( $zeile ,  '88' , '10' )       );  
          $K =         trim( substr( $zeile ,  '99' , '20' )       );  
          $L =         trim( substr( $zeile , '120' ,  '8' )       );  
          $M =         trim( substr( $zeile , '128' , '20' )       );  
        
          $query .= "INSERT INTO  "  
                   ."  table_name "  
                   ."  (          "  
                   ."    id,      "  
                   ."    FieldA,  "  
                   ."    FieldB,  "  
                   [...]  
                   ."    FieldM   "  
                   ."  )          "  
                   ."VALUES       "  
                   ."  (          "  
                   ."    '$nr',   "  
                   ."    '$A',    "  
                   ."    '$B',    "  
                   [...]  
                   ."    '$M'     "  
                   ."  )          ";  
        
          if ( $i == '300' )  
          {  
            $query  = rtrim( $query , ';' );  
            $result = sqlsrv_query( $this->DB_Connector, $query );  
            $query  = '';  
            $i      = '0';  
            echo "<br><br>$nr/$i -> [$query]";  
          }  
        }  
      }  
      fclose ($handle);  
      if ( strlen( $query ) > '0' )  
      {  
        $query = rtrim( $query , ';' );  
        $result = sqlsrv_query( $this->DB_Connector, $query );  
        echo $this->db_error( $result , $query );  
      }  
      
      

      Erst hatte ich $query befüllt, und dann in einem Mal am Ende an SQL übergeben. Dabei waren aber jeweils nur ~250 Datensätze in der Tabelle, deswegen habe ich noch den Schritt eingebaut, dass nach 300 Sätzen ein Insert erfolgt, und $query wieder geleert wird.

      1. Hallo,

        Hier als Beispiel eine Zeile aus der Datei. Mit explode oder regex wüsste ich nicht wie ich da rangehen soll.

        1002201000000111110000000000  0000000815            73020100  000000000051,10 WARENSEND KLEINTEIL   Herr Müller         Nr.12345LiefSch231

        Die Sache mit mit den substrings mit bestimmten Start- und Endpunkten ist hier die nach wie vor die einzige Lösung die mir einfällt.

        und vermutlich sowieso die performanteste :-)
        Schneller als die direkte Angabe der "Schnittpunkte" kann das Findenlassen der Schnittpunkte kaum sein. Trimmen sollte auch nicht zuviel Zeit kosten.
        Das solltest Du allerdings auch messen können.

        Miss bitte, wieviel Zeit Du gewinnst, wenn Du Deine Datei auf einen Rutsch schreibst. Ermittle den Flaschenhals. Ich gehe davon aus, dass Du dem Skript genügend Speicher zur Verfügung stellen kannst, dass es praktisch alle Operationen im RAM durchführen kann. Ständige Lese- oder Schreiboperationen auf der Festplatte bremsen ein Skript genauso aus wie unnötig viele DB-Operationen.

        Dann habe ich mir die Geschichte mit Prepared-Statements angeschaut, das hatte dann aber länger gedauert. Muss mich da nochmal ein bisschen einlesen...

        Dabei waren aber jeweils nur ~250 Datensätze in der Tabelle, deswegen habe ich noch den Schritt eingebaut, dass nach 300 Sätzen ein Insert erfolgt, und $query wieder geleert wird.

        Möglicherweise erreichtest Du mit Deinen Multiinserts hier die maximale Paketgröße :-)

        Haben die Multi-Insert-Anweisungen etwas gebracht? Wenn ja, wieviel?
        Prepared Statements haben bei der Ausführung in einer Schleife den Vorteil, dass das SQL-Statement nur ein einziges Mal "übersetzt" werden muss. Dadurch sollte sich bei genau solchen Anwendungsbeispielen wie Deinem ein Performancegewinn erzielen lassen.

        Welches DBMS setzt Du ein?

        Freundliche Grüße

        Vinzenz

        1. Miss bitte, wieviel Zeit Du gewinnst, wenn Du Deine Datei auf einen Rutsch schreibst. Ermittle den Flaschenhals. Ich gehe davon aus, dass Du dem Skript genügend Speicher zur Verfügung stellen kannst, dass es praktisch alle Operationen im RAM durchführen kann. Ständige Lese- oder Schreiboperationen auf der Festplatte bremsen ein Skript genauso aus wie unnötig viele DB-Operationen.

          Mmmh.. wäre das nicht ein Schritt zurück? Ich hatte zuerst die Option, dass die Datei mit file() und dann einer foreach-Schleife abgearbeitet wurde. Dabei findet der Lesevorgang von der Platte einmal statt, dann arbeite ich mit den gelesenen Daten.
          Das war aber im Vergleich zu fopen > while !feof > $zeile = fgets (was, wenn ich nichts falsch verstehe, wie von Dir geschildert konstant die Platte bemüht) 2 Sekunden schneller.

          Haben die Multi-Insert-Anweisungen etwas gebracht? Wenn ja, wieviel?

          Ja.

          Damit dauert die Datei nun (bei 2.460 Zeilen) statt 6 Sekunden (jede Zeile ein eigenes Insert) 5 Sekunden (300er Insert-Packen).
          Damit dauert die Datei nun (bei 6.844 Zeilen) statt 25 Sekunden (jede Zeile ein eigenes Insert) 11 Sekunden (300er Insert-Packen).

          Prepared Statements haben bei der Ausführung in einer Schleife den Vorteil, dass das SQL-Statement nur ein einziges Mal "übersetzt" werden muss. Dadurch sollte sich bei genau solchen Anwendungsbeispielen wie Deinem ein Performancegewinn erzielen lassen.

          Mit den prepared-Statements habe ich mich heute morgen kurz beschäftigt, habe damit aber glaube ich ein prinzipielles Verständnisproblem.

          Ich habe folgenden Code gebaut, der auch funktioniert:

            
          /* Set up the Transact-SQL query. */  
          $tsql     =  "INSERT INTO  "  
                      ."  table_name "  
                      ."  (          "  
                      ."    id,      "  
                      ."    FieldA,  "  
                      ."    FieldB,  "  
                      [...]  
                      ."    FieldM   "  
                      ."  )          "  
                      ."VALUES       "  
                      ."  (          "  
                      ."    ( ?),    "  
                      ."    ( ?),    "  
                      ."    ( ?),    "  
                      [...]  
                      ."    ( ?),    "  
                      ."  )          ";  
            
          $handle = fopen ( $this->datei_path.$this->datei_name , "r" );  
          $nr     = '0';  
          while ( ! feof( $handle ) )  
          {  
            $nr++;  
            $zeile = fgets( $handle, '4096' );  
            //Leerzeilen abfangen  
            if ( strlen ( trim( $zeile ) ) > '0' )  
            {  
              $A =               substr( $zeile ,   '0' ,  '8' );  
              $B =        ltrim( substr( $zeile ,   '8' , '10' ) , '0' );  
              $C =        ltrim( substr( $zeile ,  '18' , '12' ) , '0' );  
              $D =        ltrim( substr( $zeile ,  '30' , '10' ) , '0' );  
              $E =        ltrim( substr( $zeile ,  '40' , '12' ) , '0' );  
              $F =        ltrim( substr( $zeile ,  '52' , '10' ) , '0' );  
              $G = strtr( ltrim( substr( $zeile ,  '62' , '15' ) , '0' ) , ',' , '.' );  
              $H =         trim( substr( $zeile ,  '77' , ' 1' )       );  
              $I =        rtrim( substr( $zeile ,  '79' ,  '9' ) , ' ' );  
              $J =         trim( substr( $zeile ,  '88' , '10' )       );  
              $K =         trim( substr( $zeile ,  '99' , '20' )       );  
              $L =         trim( substr( $zeile , '120' ,  '8' )       );  
              $M =         trim( substr( $zeile , '128' , '20' )       );  
            
              /* Set up the parameters array. Parameters correspond, in order, to  
                 question marks in $tsql. */  
              $params = array( $nr,$BLDAT,$SKOST,$SAUFN,$EKOST,$EAUFN,$KSTAR,$IKPSU,$minus,$anwen,$kennz,$fatxt,$hitxt,$abrbr );  
            
              /* Create the statement. */  
              $stmt = sqlsrv_prepare( $this->DB_Connector, $tsql, $params);  
              if( $stmt ){}  
              else  
              {  
                echo "<br><br>Error in preparing statement.\n";  
                die( print_r( sqlsrv_errors(), true));  
              }  
            
              /* Execute the statement. Display any errors that occur. */  
              if( sqlsrv_execute( $stmt)){}  
              else  
              {  
                echo "<br><br>Error in executing statement.\n";  
                die( print_r( sqlsrv_errors(), true));  
              }  
            }  
          }  
          fclose ($handle);  
          
          

          Das hat länger gedauert, und macht, wenn ich nicht irgendwas falsh verstehe, keinen Sinn.
          Mit dieser Version erzeuge ich statt einem Insert-Befehl pro Zeile nun einen PrepareStatement-Befehl und einen ExecuteStatement Befehl.
          Mehr Sinn macht es in meinen Augen, wenn ich den sqlsrv_prepare-Befehl vor die Schleife setze. Dann ist nur das Problem, dass er den $params-array noch nicht kennt.
          Also an dem Zusammenspiel von Schleife und prepare-Insert knabbere ich noch...

          Welches DBMS setzt Du ein?

          Ich greife per SQLManager Lite auf eine MsSql-Db zu. Die DB selbst wird aber von unserer IS verwaltet, was die nutzen müsste ich nachfragen.

          Freundliche Grüße

          Matthias

          1. Hallo,

            Mehr Sinn macht es in meinen Augen, wenn ich den sqlsrv_prepare-Befehl vor die Schleife setze.

            ja klar. Sonst ist der ganze Vorteil zum Teufel.

            Dann ist nur das Problem, dass er den $params-array noch nicht kennt.

            Welches DBMS setzt Du ein?
            Ich greife per SQLManager Lite auf eine MsSql-Db zu. Die DB selbst wird aber von unserer IS verwaltet, was die nutzen müsste ich nachfragen.

            Ich kenne Deine DB-Klasse nicht. Typischerweise werden innerhalb der Schleife die Werte an die Parameter gebunden. Ob Deine Klasse hier einen Arrayzugriff erlaubt oder nicht, das kann ich von außen nicht sehen. Da Deine Parameter sowieso als Einzelvariablen vorliegen, sehe ich hier kein prinzipielles Problem. In Pseudocode

            Setze Prepare-Anweisung mit dem Prepared Statement und Platzhaltern ab
            Solange es Datensätze gibt
                Binde die Daten an die Platzhalter
                Führe das Prepared Statement mit den gebundenen Daten aus
            Ende Solange

            Für den Zugriff auf MSSQL verwende ich bevorzugt .NET (typischerweise in Nicht-Web-Umgebungen) ;-)

            Freundliche Grüße

            Vinzenz

            1. In Pseudocode

              Setze Prepare-Anweisung mit dem Prepared Statement und Platzhaltern ab
              Solange es Datensätze gibt
                  Binde die Daten an die Platzhalter
                  Führe das Prepared Statement mit den gebundenen Daten aus
              Ende Solange

              Habe es nun hinbekommen. Ein paar strategisch platzierte & helfen :-)

                
              $query  = "INSERT INTO  "  
                         ."  table_name "  
                         ."  (          "  
                         ."    id,      "  
                         ."    FieldA,  "  
                         ."    FieldB,  "  
                         [...]  
                         ."    FieldM   "  
                         ."  )          "  
                         ."VALUES       "  
                         ."  (          "  
                         ."    '( ?)'   "  
                         ."    '( ?)'   "  
                         ."    '( ?)'   "  
                         [...]  
                         ."    '( ?)'   "  
                         ."  )          ";  
              $params = array( &$A , &$B , &$C , [...] , &$M );  
              $stmt   = sqlsrv_prepare( $this->DB_Connector , $query , $params );  
              $handle = fopen ( $this->datei_path.$this->datei_name , "r" );  
              $nr     = '0';  
              while ( ! feof( $handle ) )  
              {  
                $nr++;  
                $zeile = fgets( $handle, '4096' );  
                //Leerzeilen abfangen  
                if ( strlen ( trim( $zeile ) ) > '0' )  
                {  
                  $A =               substr( $zeile ,   '0' ,  '8' );  
                  $B =        ltrim( substr( $zeile ,   '8' , '10' ) , '0' );  
                  $C =        ltrim( substr( $zeile ,  '18' , '12' ) , '0' );  
                  $D =        ltrim( substr( $zeile ,  '30' , '10' ) , '0' );  
                  $E =        ltrim( substr( $zeile ,  '40' , '12' ) , '0' );  
                  $F =        ltrim( substr( $zeile ,  '52' , '10' ) , '0' );  
                  $G = strtr( ltrim( substr( $zeile ,  '62' , '15' ) , '0' ) , ',' , '.' );  
                  $H =         trim( substr( $zeile ,  '77' , ' 1' )       );  
                  $I =        rtrim( substr( $zeile ,  '79' ,  '9' ) , ' ' );  
                  $J =         trim( substr( $zeile ,  '88' , '10' )       );  
                  $K =         trim( substr( $zeile ,  '99' , '20' )       );  
                  $L =         trim( substr( $zeile , '120' ,  '8' )       );  
                  $M =         trim( substr( $zeile , '128' , '20' )       );  
                  sqlsrv_execute( $stmt);  
                }  
              }  
              fclose ($handle);  
              
              
              • A -
                Mit 2.460 Zeilen  7 Sekunden, fgets(), prepared statement
                Mit 6.844 Zeilen 19 Sekunden, fgets(), prepared statement

              • B -
                Mit 2.460 Zeilen  3 Sekunden, fgets(), 300erInserts
                Mit 6.844 Zeilen  7 Sekunden, fgets(), 300erInserts

              • C -
                Mit 2.460 Zeilen  2 Sekunden, file(), 300erInserts
                Mit 6.844 Zeilen  7 Sekunden, file(), 300erInserts

              Werde dann wohl Variatne C wählen.

              Vielen Dank an alle für die Hilfe und Vorschläge, speziell Vinzenz.

              Gruß

              Matthias