02. oktober 2006 - 13:57Der er
48 kommentarer og 2 løsninger
Optimering af kode.
Hej eksperter.
Et lille spørgsmål som går ud på optimering af kode.
Jeg har lavet en service som holder øje med en folder om en fil er oprettet. Når det sker, indlæser den filen og behander den. Jeg har en access db med 3 felter. 2 TEXT og 1 MEMO. Indhold fil, som er en txt fil, bliver "smidt" ind i DATALOG/MEMO. Og ejer af filen bliver lagt ind i NAME/TEXT og navnet på filen bliver lagt ind i FILE/TEXT. Mit problem er i starten når jeg oprettet records i db gik det bare hurtigt, nu efter af antalet af records er kommet op på 12000 går det meget langsomt. Er det fordi, at jeg har lavet noget "dårligt" kode. :) Og kan man optimere det jeg har lavet eller er det bare access der er sådan. Jeg har ca 7000 filer endnu, som jeg skal have oprettet i access. Kan nogen sige mig om det kan optimeres?
Nedenstående kode er koden for når en fil er oprettet:
begin if pos('CDATABUP',Filename) <> 0 then begin if pos('Log',Filename) <> 0 then begin if pos('DKSOLT',Filename) or pos('DKSODT',Filename) <> 0 then begin repeat Sleep(100); until not isFileInUse(Filename); try StringList := TStringList.Create; StringList.LoadFromFile(FileName); except on EFopenError do begin LogMessage('Cannot open file: ' + filename,EVENTLOG_INFORMATION_TYPE); end; end; Tmp := StringList.Strings[2]; if (POS('ROBOCOPY', Tmp) <> 0) and (POS('Version XP010', Tmp) <> 0) then begin beep; TmpUSERNAME := StringList.Strings[8]; Delete(TmpUSERNAME,1,pos('\',TmpUSERNAME)); Delete(TmpUSERNAME,1,pos('\',TmpUSERNAME)); Delete(TmpUSERNAME,1,pos('\',TmpUSERNAME)); Delete(TmpUSERNAME,1,pos('\',TmpUSERNAME)); TmpUSERNAME := copy(TmpUSERNAME,1,pos('\',TmpUSERNAME)-1); TmpFILE := FILENAME; Delete(TmpFILE,1,pos('\',TmpFILE)); Delete(TmpFILE,1,pos('\',TmpFILE)); Delete(TmpFILE,1,pos('\',TmpFILE)); Delete(TmpFILE,1,pos('\',TmpFILE)); Delete(TmpFILE,1,pos('\',TmpFILE)); ADOTable1.Open; if not (ADOTable1.State in [dsInsert, dsEdit]) then ADOTable1.Insert; ADOTable1Name.AsString := TmpUSERNAME; ADOTable1FILE.AsString := TmpFILE; ADOTable1DataLog.LoadFromFile(Filename); ADOTable1.Post; ADOTable1.Close; DeleteFile(Filename); end; end; StringList.Free; end; end; end;
I dette særtema om aspekter af AI ser vi på skiftet fra sprogmodeller til AI-agenter, og hvordan virksomheder kan navigere i spændet mellem teknologisk hastighed og behovet for menneskelig kontrol.
GLEM ALT OM ACCESS, den kører langsomt og bliver KUN langsomere jo mere du "putter" i din DB, også selv om du sletter igen og fylder nyt i.
Vil foreslå dig at bruge Interbase/Firebird ( www.firebirdsql.org ) Firebird er gratis og kører rigtig godt, du kan fint bruge Delphi's Interbase DB Componenter til den, og den er ca. en faktor 10-50 gange hurtigere.
Nu har jeg installeret SQL 2005 EE det hjalp lidt, men hvis jeg skal gøre det du har nævnt, hvad er det jeg præcis skal putte ind som indsætter 12000 records i min tabel?
Er det sådan her: ADOTable1.Connection.BeginTrans; try ADOTable1.Open; if not (ADOTable1.State in [dsInsert, dsEdit]) then ADOTable1.Insert; ADOTable1Name.AsString := TmpUSERNAME; ADOTable1FILE.AsString := TmpFILE; ADOTable1DataLog.LoadFromFile(Filename); ADOTable1.Post; ADOTable1.Close; ADOTable1.Connection.CommitTrans; except ADOTable1.Connection.RollbackTrans; raise; end;
Den fejler ihvertfald ikke, men det er ikke fordi at der er nogen foreskel om jeg bruger den metode eller ikke. Men det går hurtigere nu efter at jeg bruger SQL 2005 EE.
HRC læg lige et svar og jeg deler point.
Er ADOQuery bedere end ADOTabel? Kan man det hele med ADOQuery som man kan med ADOTable og lidt til?
Det med at commit'e uden om din løkke det kan spare tid hvis du indsætter "store klumper" / mange records på en gang, ellers tager det bare den tid det tager og er noget du skal gøre, hvis du bruger en trans. SQL db, som du jo gør nu :), men jeg vil foreslå dig at skrive din kode om til et "rigtig" insert into statement, en table har aldrig være hurtig når vi snakker sql servere, de er som navnet siger beregnet til SQL :)
Nu er det også et spørgsmål om religion om man bruger en TTable eller en TQuery. En TTable er generelt tungere i kommunikationen frem og tilbage til databasen. Desuden så er min "religion" at bruge SQL. Tror i øvrigt nok at det er hvad der sker i baggrunden når man bruger en TTable-afart.
I øvrigt. Du behøver ikke lukke/åbne tabellen efter du har indsat. Det sløver i alt fald også. Hold den åben mens du arbejder.
åbne/lukke er ofte nødvendigt for at få opdateret sin table. Det er ikke et spm. hvis man vil ha' hurtig kode, så skal du kun bruge TTable til dine DB-grids, ellers KUN TQuery :)
Det er nu ret simpelt. Kommandoen for at indsætte records i en tabel er:
INSERT INTO <tabel> (<feltnavn>,<feltnavn>,,,) VALUES(<:param>,<:param>,,,)
For en postnummertabel:
INSERT INTO post (postnr, postby, postgade, postfirma, postland) VALUES(:postnr, :postby, :postgade, :postfirma, :postland)
Smæk en TADOQuery på din form / datamodul og forbind den til databasen. I din uses-sektion tilføjer du også lige "Variants"-unitten. Åbn sciptvinduet under komponentens SQL-property og indtast dit script. De forskellige parametre i VALUES() skal derefter sættes op. Klik på params og sæt datatypen på hver enkelt parameter. Sæt evt en default værdi ind.
I din kode skal du indsætte data i parametrene før du sender den afsted:
Nedenstående eksempel sætter de danske postnumre ind
// Start transaktionen så alle ændringer gemmes i en buffer på DB-serveren Query.Connection.BeginTrans; try for i := 0 to Liste.Count do begin PostData := Liste[i]; // Type: TPostnrData Query.Parameters.ParamByName('postnr').Value := PostData.Postnr; Query.Parameters.ParamByName('postby').Value := PostData.Postby; Query.Parameters.ParamByName('postgade').Value := Null; Query.Parameters.ParamByName('postfirma').Value := Null; Query.Parameters.ParamByName('postland').Value := PostData.Postland; // Danmark Query.ExecSQL; end; // OK, Det hele er på serveren. Gem hele skidtet så andre brugere kan se det Query.Connection.CommitTrans; except // Ikke OK! Fang fejlen og rul operationen tilbage igen Query.Connection.RollbackTrans;
// Send fejlen videre til programmet da brugeren skal informeres. raise; end;
// Hvis du er i tvivl om hvad "Liste" er, så har jeg ridset den og dens dataklasse op nedenfor:
NEJ, hvis du vil køre på en rigtig SQL-DB så skal du også lave rigtig SQL for at indsætte osv., men det er ikke så slemt som det ser ud til, du lærer det ret hurtigt så du kan komme vidre....
Ja, Det skulle kunne virke. Ville nok ændre dataobjektet så den tog en tekststreng som argument i constructoren. Den skulle så sørge for at fiske de enkelte elementer ud af linien og placere dem i de rette properties som efterfølgende kunne skrives. Laver endnu et eksempel - og det kommer som skidt fra en spædekalv...
TFileData = class public constructor Create(const aLine : string); property ... procedure SaveRecord(aQuery : TADOQuery); end;
constructor TFileData.Create(aLine : string); var FieldNo, p : integer; begin inherited; FieldNo := 0;
// Initialiser felter før man parser strengen fUsername := ''; fFilename := ''; repeat p := pos(';',aLine); // Antager her at felter adskilt af semikolon if p > 0 then begin inc(FieldNo); // Felttæller case FieldNo of 1 : fNavn := copy(aLine,1,p-1); 2 : .... end; System.Delete(aLine,1,p); end; until p = 0; fSidsteFelt := aLine; // Skal huske det sidste felt end;
procedure TFileData.SaveRecord(aQuery : TADOQuery); var LocalTransaction : boolean; begin // Hvis den ikke kaldes i en insert-løkke så lav transaktionen selv LocalTransaction := not aQuery.InTransaction;
if LocalTransaction then Query.Connection.BeginTrans; try aQuery.Parameters.ParamByName('').Value := fNavn; aQuery.Parameters.ParamByName('').Value := .. aQuery.Parameters.ParamByName('').Value := .. aQuery.ExeSql;
if LocalTransaction then Query.Connection.CommitTrans; except if LocalTransaction Query.Connection.RollbackTrans; raise; end; end;
{ TFileList }
constructor TFileList.Create(const aFilename : string); var i : integer; Lines : TStringList; begin // En eller andet kontrol om filen er lukket eller i brug
Lines := TStringList.Create; try Lines.LoadFromFile(aFilename); for i := 0 to Lines.Count - 1 do Add(TFileData(Lines[i])); finally Lines.Free; end; end;
procedure TFileList.SaveRecords; var i : integer; Query : TADOQuery; begin Query := TADOQuery.Create(nil); try Query.Connection := Din_ADOConnection; Query.SQL.Text := 'INSERT INTO ... '; Query.Connection.BeginTrans; try for i := 0 to Count - 1 do Items[i].SaveRecord(Query); Query.Connection.CommitTrans; except Query.Connection.RollbackTrans; raise end; finally Query.Free; end; end;
HRC: Du hjulpet mig med opgaven tidligere i denne tråd: http://www.eksperten.dk/spm/721758 Og det med at smide hele txtfil i en memo i en access fungeret skide godt, men kan man ikke bruge det samme hvis man skulle smide 2 felter mere ind? Jeg her prøvet dette, men det spiller ikke:
ADOQuery1.SQL.Text := 'Insert Into DATALOG (NAME, FILENAME) Values(:TmpUSERNAME,:TmpFILE)'; ADOQuery1.Connection.BeginTrans; try { ADOTable1.Open; if not (ADOTable1.State in [dsInsert, dsEdit]) then ADOTable1.Insert; ADOTable1Name.AsString := TmpUSERNAME; ADOTable1FILENAME.AsString := TmpFILE; ADOTable1DataLog.LoadFromFile(Filename); ADOTable1.Post; ADOTable1.Close;} ADOQuery1.Parameters.ParamByName('NAME').Value := TmpUSERNAME; ADOQuery1.Parameters.ParamByName('FILENAME').Value := TmpFILE; MemoryStream.LoadFromFile(FILENAME); MemorySTream.Seek(0,soFromBeginning); ADOQuery1.Parameters.ParamByName('DATALOG').LoadFromStream(MemoryStream,ftFmtMemo); ADOQuery1.ExecSQL; ADOQuery1.Connection.CommitTrans; except ADOQuery1.Connection.RollbackTrans; Raise; end;
Du havde mistofstået brugen af insert-kommandoen. Det første parantessæt er de felter du vil indsætte noget i. Det andet er der hvor værdierne placeres - og her har vi smidt nogle parametre som vi efterfølgende fylder noget i.
Jeg har ikke testet nedenstående, men det er i alt fald skridt i den rigtige retning.
Næ, det virker ikke. :) Jeg får denne fejl i min eventlog: The following information is part of the event: Access violation at address 0048D555 in module 'BackupCheck.exe'
Nu lige for at få Kong Hans her til bare at forstå lidt af det hele. Det du lige har give et eksempel på, erstatter det hele den smøre du lavet før? Går det hurtigere med hele "smøren" eller er det, det samme?
'Insert Into <TabelNavn> (<FeltNavne [felt1,felt2,felt3] >) Values(<Feltværdi> [felt1,felt2,felt3] )'
Så det vi kan se, er at den SQL-sætning indsætter en række i din tabel :)
ADOQuery1.Connection.BeginTrans; try ADOQuery1.Parameters.ParamByName('Felt1').Value := felt1-værdi; ADOQuery1.Parameters.ParamByName('felt2').Value := felt2-værdi; MemoryStream.LoadFromFile('MyFile.txt'); MemorySTream.Seek(0,soFromBeginning); // felt3-værdi er her en filestream ADOQuery1.Parameters.ParamByName('felt3').LoadFromStream(MemoryStream,ftFmtMemo); ADOQuery1.ExecSQL; // udfør sql-sætning ADOQuery1.Connection.CommitTrans; except ADOQuery1.Connection.RollbackTrans; Raise; end;
Lettede det forståelsen ??
Access violation at address 0048D555 in module 'BackupCheck.exe' >> Betyder som regl at du bruger et Object/Classe du ikke har oprettet ( MyObj := TMyObj.Create )
procedure TALBackupCheck.DirWatch1NewFileCreated(Sender: TObject; const FileName: string); var tmp : string; MemoryStream : TMemoryStream; begin MemoryStream := TMemoryStream.Create; LogMessage('File created: ' + FileName,EVENTLOG_AUDIT_SUCCESS); if pos('CDATABUP',Filename) <> 0 then begin if pos('Log',Filename) <> 0 then begin if pos('DKSOLT',Filename) or pos('DKSODT',Filename) <> 0 then begin repeat Sleep(100); until not FileInUse(Filename); try StringList := TStringList.Create; StringList.LoadFromFile(FileName); except on EFopenError do begin LogMessage('Cannot open file: ' + filename,EVENTLOG_INFORMATION_TYPE); end; end; // Det kunne være her den fejler for du glemmer at checke om du har loadet en fil // og det fortsætter du med længere nede Tmp := StringList.Strings[2]; if (POS('ROBOCOPY', Tmp) <> 0) and (POS('Version XP010', Tmp) <> 0) then begin beep; Navn := StringList.Strings[8]; Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Navn := copy(Navn,1,pos('\',Navn)-1); FilNavn := FILENAME; Delete(FilNavn,1,pos('\',FilNavn)); Delete(FilNavn,1,pos('\',FilNavn)); Delete(FilNavn,1,pos('\',FilNavn)); Delete(FilNavn,1,pos('\',FilNavn)); Delete(FilNavn,1,pos('\',FilNavn)); ADOQuery1.SQL.Text := 'Insert Into DATALOG (NAME, FILENAME, DATALOG) Values(:ParamNAME,:ParamFILE,:ParamDATALOG)'; ADOQuery1.Connection.BeginTrans; try ADOQuery1.Parameters.ParamByName('ParamNAME').Value := Navn; ADOQuery1.Parameters.ParamByName('ParamFILE').Value := Filnavn; MemoryStream.LoadFromFile(FILENAME); MemorySTream.Seek(0,soFromBeginning); ADOQuery1.Parameters.ParamByName('ParamDATALOG').LoadFromStream(MemoryStream,ftFmtMemo); ADOQuery1.ExecSQL; ADOQuery1.Connection.CommitTrans; except ADOQuery1.Connection.RollbackTrans; Raise; end; DeleteFile(Filename); end; end; MemoryStream.Free; StringList.Free; end; end; end;
Tak for tippet angående ExtractFilename() og ExtraFilePath(). Det kunne jeg bruge. Men det andet forstår jeg ikke helt. Jeg går helt bestemt udfra at hvis der var en fejl i load af fil at jeg får en besked i min eventlog.
Hvis jeg bruger denne løsning har jeg ingen problemer.
procedure TALBackupCheck.DirWatch1NewFileCreated(Sender: TObject; const FileName: string); var tmp : string; MemoryStream : TMemoryStream; begin MemoryStream := TMemoryStream.Create; LogMessage('File created: ' + Filename,EVENTLOG_AUDIT_SUCCESS); if pos('CDATABUP',Filename) <> 0 then begin if pos('Log',Filename) <> 0 then begin if pos('DKSOLT',Filename) or pos('DKSODT',Filename) <> 0 then begin repeat Sleep(100); until not FileInUse(Filename); try StringList := TStringList.Create; StringList.LoadFromFile(FileName); except on EFopenError do begin LogMessage('Cannot open file: ' + filename,EVENTLOG_INFORMATION_TYPE); end; end; Tmp := StringList.Strings[2]; if (POS('ROBOCOPY', Tmp) <> 0) and (POS('Version XP010', Tmp) <> 0) then begin beep; Navn := StringList.Strings[8]; Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Navn := copy(Navn,1,pos('\',Navn)-1); Filnavn := ExtractFileName(Filename); ADOTable1.Open; if not (ADOTable1.State in [dsInsert, dsEdit]) then ADOTable1.Insert; try ADOTable1Name.AsString := Navn; ADOTable1FILENAME.AsString := FilNavn; ADOTable1DataLog.LoadFromFile(Filename); finally ADOTable1.Post; ADOTable1.Close; end; DeleteFile(Filename); end; end; MemoryStream.Free; StringList.Free; end; end; end;
Det gør du også, men fordi du har except så får du behandlet færdig der, og prgrammet kører vidre, der efter prøver du at læse line 2 i din ( ikke indlæste fil ) og det samme gør du i line 8, det kunne godt være en årsag til at du får din "Access Violation"
Tmp := StringList.Strings[2]; <---- hvis der ikke var en fil eller en openerror så læser du bare uden at checke !!
Men jeg ved jo at filen på det tidspunkt findes og hvis ikke der er nogen problemer i at åbne den, så jeg kan da vel godt lave denne: Tmp := StringList.Strings[2];
Men jeg har set på dit forslag og spørger ganske forsigtig om det skal gøres på denne måde. Jeg er lidt usikker på om begge metoder skal bruges, men jeg prøvet dette: try StringList := TStringList.Create; try StringList.LoadFromFile(FileName); except on EFopenError do begin LogMessage('Cannot open file: ' + filename,EVENTLOG_INFORMATION_TYPE); end; end; finally Tmp := StringList.Strings[2]; end;
Hvis jeg skal bruge: try finally
kan jeg ikke sådan lige se hvor exception kommer ind i billedet.
det der er problemmet er at du "kører" vidre i din kode fordi du har en try except, så du bør nok tænke din struktur om, for hvis filen ikke findes er StringList.count = 0 og så vil du få fejl ved at prøve en Strings[2]
Du kunne overveje ot checke med if FileExists() then .lav din db indsæt .... else ..lav ikke noget ( vent )..
Ok, men selve proceduren hedder "NewFileCreated" så burde den ikke findes på det tidpunkt? procedure TALBackupCheck.DirWatch1NewFileCreated(Sender: TObject; const FileName: string);
Og i det at jeg tjekker for om den rigtig sti er der, med: if pos('CDATABUP',Filename) <> 0 then begin if pos('Log',Filename) <> 0 then begin if pos('DKSOLT',Filename) or pos('DKSODT',Filename) <> 0 then
så vil jeg da gå udfra filen er der, for ellers ville jeg ikke komme så "langt" i koden?
Og som jeg nævnte, at når jeg bruger ADOTable har jeg ingen problemer. :) Problemet opstår først når jeg bruger ADOQuery.
Er du i tvivl om mere, så smut ned på bib. og læs i nogle pacal/delphi bøger, det er lidt af det grundlægende du ikke helt har på plads lyder det som om
Nu kommer der jo også mange N00B spørgsmål her, så vær nu lige venlig at bære over med mig. :)
Jeg har prøvet dette: procedure TALBackupCheck.DirWatch1NewFileCreated(Sender: TObject; const FileName: string); var tmp : string; MemoryStream : TMemoryStream; begin MemoryStream := TMemoryStream.Create; LogMessage('File created: ' + Filename,EVENTLOG_AUDIT_SUCCESS); if pos('CDATABUP',Filename) <> 0 then begin if pos('Log',Filename) <> 0 then begin if pos('DKSOLT',Filename) or pos('DKSODT',Filename) <> 0 then begin repeat Sleep(100); until not FileInUse(Filename); if FileExists(FileName) then begin StringList := TStringList.Create; try StringList.LoadFromFile(Filename); if (POS('ROBOCOPY', StringList.Strings[2]) <> 0) and (POS('Version XP010', StringList.Strings[2]) <> 0) then begin beep; Navn := StringList.Strings[8]; Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Delete(Navn,1,pos('\',Navn)); Navn := copy(Navn,1,pos('\',Navn)-1); LogMessage(Navn,EVENTLOG_INFORMATION_TYPE); Filnavn := ExtractFileName(Filename); LogMessage(FilNavn,EVENTLOG_INFORMATION_TYPE); ADOQuery1.SQL.Text := 'Insert Into DATALOG (NAME, FILENAME, DATALOG) Values(:ParamNAME,:ParamFILE,:ParamDATALOG)'; ADOQuery1.Connection.BeginTrans; try ADOQuery1.Parameters.ParamByName('ParamNAME').Value := Navn; ADOQuery1.Parameters.ParamByName('ParamFILE').Value := Filnavn; MemoryStream.LoadFromFile(FILENAME); MemorySTream.Seek(0,soFromBeginning); ADOQuery1.Parameters.ParamByName('ParamDATALOG').LoadFromStream(MemoryStream,ftFmtMemo); ADOQuery1.ExecSQL; ADOQuery1.Connection.CommitTrans; except ADOQuery1.Connection.RollbackTrans; Raise; end; end; finally MemoryStream.Free; StringList.Free; end; end; end; end; end; end;
Men får nnu denne fejl i min eventlog:
Operand type clash: image is incompatible with nvarchar(max).
Meeen det med delphi børgene, mon ikke du stadig kan låne dem på bib. det tror jeg.
Ellers så prøv at læs lidt i delphi online hjælp omkring Try finally og try except
det du skal tænke på er at hvis du "selv fanger" din fejl med en try except så kører programet vidre, som om der ikke har været en fejl, for den har du jo lige fanget :)
nvarchar er begrænset til 8kb i MS-SQL - og sikkert også i Access. De skal jo ligne hinanden, også på de dumme ting. Mon ikke der er en datatype der hedder "image". Prøv den.
Jeg er nok bange for at I må hjælpe mig hvad jeg kan gøre her. Hvorfor virker det med ADOTable og ikke ADOQuery?
Synes godt om
Ny brugerNybegynder
Din løsning...
Tilladte BB-code-tags: [b]fed[/b] [i]kursiv[/i] [u]understreget[/u] Web- og emailadresser omdannes automatisk til links. Der sættes "nofollow" på alle links.