{"id":204,"date":"2012-11-18T22:45:11","date_gmt":"2012-11-18T21:45:11","guid":{"rendered":"http:\/\/blog.systemconnect.dk\/?p=204"},"modified":"2015-04-25T14:10:23","modified_gmt":"2015-04-25T12:10:23","slug":"sql-kode-pa-speed","status":"publish","type":"post","link":"https:\/\/scblog.lynge.org\/?p=204","title":{"rendered":"SQL-kode p\u00e5 speed?"},"content":{"rendered":"<p>Nogle C5-k\u00f8rsler tager bare for\u00a0lang tid. Generelt kan man selvf\u00f8lgelig pr\u00f8ve at optimere koden. Justere eller skifte algoritme. Lave simplere genneml\u00f8b. Samle flere genneml\u00f8b til f\u00e5 v.hj.a. tempor\u00e6re tabeller &#8211; eller m\u00e5ske undlade at bruge tempor\u00e6re tabeller og i stedet benytte arrays.<\/p>\n<p>Naturligvis kan det ogs\u00e5 betale sig at t\u00e6nke i om andre eller nye indexer kan speede tingene op &#8211; eller om der kan laves &#8220;mellemregning&#8221; i nye felter i tabellerne.<\/p>\n<p>P\u00e5 SQL har man dog flere &#8220;skud i b\u00f8ssen&#8221; som der dog er rigtigt mange konsulenter og programm\u00f8rer, der helt undlader at benytte. Det er en skam. De er faktisk indrettet s\u00e5dan at de kan anvendes uanset om man k\u00f8rer Native eller SQL &#8211; s\u00e5 vender man sig til at bruge dem hver gang (og det\u00a0ER faktisk\u00a0en vane-sag), s\u00e5 er det blot ligegyldigt p\u00e5 Native, men kan optimere tingene rigtigt meget p\u00e5 SQL.<\/p>\n<p>Bem\u00e6rk ogs\u00e5 at selvom tempor\u00e6re tabeller faktisk oprettes p\u00e5 SQL-serveren, s\u00e5 lagres data lokalt i en tempor\u00e6r native datafil (der kommer aldrig data i tabellerne p\u00e5 serveren). SQL-optimeringerne ignoreres derfor ogs\u00e5 p\u00e5 tempor\u00e6re tabeller.<\/p>\n<p><strong>SELECT &#8230; #USING(&lt;index&gt;) i stedet for SELECT &#8230; USING &lt;index&gt;<\/strong><br \/>\nP\u00e5 en SQL-server angiver man typisk ikke hvilket index, der skal bruges i et genneml\u00f8b. Det er Optimizerens (del af SQL-serveren) problem (bem\u00e6rk dog at man nogle gange kan f\u00e5 lov til at komme med index-forslag til optimizeren, men det kommer vi ikke ind p\u00e5 i dette indl\u00e6g).\u00a0Det betyder ogs\u00e5 at den r\u00e6kkef\u00f8lge man f\u00e5r poster tilbage i fra SQL, ikke er garanteret og alts\u00e5 skal betragtes som tilf\u00e6ldig.<\/p>\n<p>Det kr\u00e6ver en anden m\u00e5de at t\u00e6nke p\u00e5 end\u00a0med Native databasen, hvor man altid angiver et index, b\u00e5de for at hj\u00e6lpe databasen med at s\u00f8ge posterne hurtigere frem &#8211; men ogs\u00e5 for at fastl\u00e6gge deres r\u00e6kkef\u00f8lge.<\/p>\n<p>Bruger man alligevel SEARCH\u00a0&#8230; USING &lt;index&gt; &#8230; varianten\u00a0med C5 p\u00e5 SQL, s\u00e5 sker der faktisk det at C5 sender en SQL-foresp\u00f8rgsel uden index-hint til SQL-serveren. Denne returnerer s\u00e5 et tilf\u00e6ldigt ordnet resultat, som C5-kernen s\u00e5 sorterer jf. index&#8217;et inden applikationen ser dataene. Kernen g\u00f8r det for at din kode stadig fungerer, men det er naturligvis\u00a0frygteligt\u00a0dyrt performancem\u00e6ssigt\u00a0og i rigtig mange tilf\u00e6lde helt un\u00f8dvendigt.<\/p>\n<p>Benyttes i stedet SEARCH &#8230;\u00a0#USING(&lt;index&gt;) &#8230; signalerer applikationen overfor kernen at det er OK at posterne returneres i vilk\u00e5rlig r\u00e6kkef\u00f8lge &#8211; og dermed spares hele sorteringen i kernen, n\u00e5r koden afvikles p\u00e5 SQL. Afvikles der p\u00e5 Native, g\u00f8r de to varianter det samme &#8211; dvs. hj\u00e6lper Native databasen med at finde data hurtigt\u00a0med det angivne index og\u00a0returnerer posterne sorteret i indexr\u00e6kkef\u00f8lgen.<\/p>\n<p>Bem\u00e6rk at man b\u00e5de p\u00e5 SQL og Native naturligvis kan bede kerne sorterer i en helt ande r\u00e6kkef\u00f8lge v.hj.a. SEARCH &#8230;.. ORDER BY &lt;feltnavne&gt; &#8230;<\/p>\n<p>Eksempel 1 &#8211; Genneml\u00f8b alle ordrelinjer for\u00a0at s\u00e6tte SupplyNow til Qty-Delivered (r\u00e6kkef\u00f8lge ligegyldigt)<\/p>\n<pre>SEARCH SalesLine #USING(NumTransLineIdx) WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0 SET SupplyNow = Qty-Delivered\r\n\u00a0\u00a0\u00a0 UPDATE SalesLine\r\nEND\r\n\r\n<\/pre>\n<p>Eksempel\u00a02 &#8211; Klassisk, uoptimeret genneml\u00f8b alle ordrelinjer for\u00a0at finde det h\u00f8jeste linjenummer<\/p>\n<pre>SET &amp;MaxLineNo = #MinReal\r\nSEARCH SalesLine USING NumTransLineIdx WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0 SET &amp;MaxLineNo = #Max(&amp;MaxLineNo,LineNumber)\r\nEND\r\n\r\n<\/pre>\n<p>Eksempel\u00a03 &#8211; Genneml\u00f8b alle ordrelinjer for at finde det h\u00f8jeste bel\u00f8b (bem\u00e6rk &#8211; der findes en endnu mere effektiv metode til netop dette eksempel p\u00e5 SQL &#8211; se #SQLMaxList senere)<\/p>\n<pre>SEARCH SalesLine #USING(NumTransLineIdx) ORDER BY\u00a0Amount DESCEND\u00a0WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0 SET &amp;MaxAmount = Amount\r\n\u00a0\u00a0\u00a0 BREAK\r\nEND\r\n\r\n<\/pre>\n<p><strong>SQL-serveren finder og retunerer jo alle felter p\u00e5 posten til C5-kernen og dermed til applikationen. Er det ikke spild af tid hvis man kun skal bruge et enkelt felt?<br \/>\n<\/strong>Jo, det er det. med kommandoenden #SQLFieldList(&lt;tabelnavn&gt;,&lt;feltnavn 1&gt;,&lt;feltnavn 2&#8230;.) kan man fort\u00e6lle kernen og SQL-serveren at man kun \u00f8nsker at f\u00e5 returneret udvalgte felter&#8230; P\u00e5 et langsomt netv\u00e6rk og en stor tabel, kan det spare en del tid og netv\u00e6rksbelastning, og dermed forbedre den generelle hastighed b\u00e5de for den aktuelle bruger og alle andre.<\/p>\n<p>Kommandoen s\u00e6ttes lige inden en SEARCH eller INTRODUCE med direkte opslag og findes i Makrobiblioteket SQL (s\u00e5 det skal du lige huske at loade). Native-kernen ignorerer kommandoen fuldst\u00e6ndig &#8211; s\u00e5 du kan roligt benytte den p\u00e5 Native for at g\u00f8re din kode SQL-parat, men v\u00e6r OBS p\u00e5 at du s\u00e5 naturigvis ikke f\u00e5r testet rigtigheden af din kode hvis du ikke tester p\u00e5 SQL.<\/p>\n<p><strong>MEN PAS P\u00c5<\/strong>: Hvis du i din kode k\u00f8rer INSERT\/UPDATE af de fremfundne data og du har en C5 tabeltrigger, der skal bruge felter som du IKKE har med i din #SQLFieldList, s\u00e5 f\u00e5r du problemer. Det\u00a0samme g\u00e6lder naturligvis hvis du overf\u00f8rer hele posten til et andet element, der bruger felter du har fravalgt.<\/p>\n<p>P\u00e5 nyere netv\u00e6rk er der typisk kapacitet nok, s\u00e5 pga. ovenst\u00e5ende fejlmuligheder er det typisk ikke denne optimering du skal bruge mest tid p\u00e5. Sandsynligheden for at du glemmer et felt &#8211; eller senere \u00e6ndrer koden s\u00e5 du skal bruge felter du ikke har med (og glemmer at rette kommandoen) er relativ h\u00f8j og kan medf\u00f8re irriterende fejls\u00f8gning.<\/p>\n<p>Eksempel &#8211; Genneml\u00f8b alle ordrelinjer for at finde det h\u00f8jeste bel\u00f8b:<\/p>\n<pre>#SQLFieldList(SalesLine,Amount)\r\nSEARCH SalesLine #USING(NumTransLineIdx) ORDER BY\u00a0Amount DESCEND WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0 SET &amp;MaxAmount = Amount\r\n\u00a0\u00a0\u00a0\u00a0BREAK\r\nEND\r\n\r\n<\/pre>\n<p><strong>Hvad nu hvis jeg blot skal finde st\u00f8rte\/mindste v\u00e6rdi eller sum af v\u00e6rdierne i en tabel?<br \/>\n<\/strong>Ja, disse simple ting er der naturligvis ogs\u00e5 l\u00f8sninger p\u00e5 &#8211; l\u00f8sninger der vel at m\u00e6rke flytter al arbejdet til SQL-serveren. Og er det 1.000.000 poster der skal findes st\u00f8rste-\/mindste-v\u00e6rdi eller sum p\u00e5, ja s\u00e5 sparer man alts\u00e5 en del flytning af data over netv\u00e6rket og ikke mindst arbejde p\u00e5 klienten\u00a0for C5 kernen.<\/p>\n<p>Kommandoerne fungerer stort set ligesom #SQLFieldList ovenfor &#8211; dog med et enkelt tvist omkring SQL-kommandoen GROUP BY &#8211; mere om det senere. Kommandoerne hedder #SQLMinList, #SQLMaxList og #SQLSumList &#8211; og de ignoreres ligesom #SQLFieldList helt p\u00e5 Native databasen.<\/p>\n<p>Kunsten her er at lave en stump kode der virker uanset om den afvikles p\u00e5 Native (og alts\u00e5 arbejder p\u00e5 alle poster der skal h\u00e5ndteres), eller n\u00e6sten f\u00e5r resultatet for\u00e6ret af SQL-serveren.<\/p>\n<p>Eksempel &#8211; Genneml\u00f8b alle ordrelinjer for at finde det h\u00f8jeste bel\u00f8b:<\/p>\n<pre>#SQLMaxList(SalesLine,Amount)\r\nSEARCH SalesLine #USING(NumTransLineIdx) ORDER BY Amount DESCEND WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0 SET &amp;MaxAmount = Amount\r\n\u00a0\u00a0\u00a0 BREAK\r\nEND\r\n\r\n<\/pre>\n<p>Bem\u00e6rk: P\u00e5 SQL retuneres kun en enkelt post, nemlig posten med summen direkte i Amount-feltet. Den post kan C5-kernen hurtig sorterer (ORDER BY) og det betyder heller ikke noget at SEARCH afbrydes med BREAK efter at den eneste post er l\u00e6st.<br \/>\nP\u00e5 Native fremfindes alle poster og sorteres faldende (ORDER BY). Dermed er f\u00f8rste post den h\u00f8jeste og SEARCH afbrydes med BREAK s\u00e5 snart den er l\u00e6st.<br \/>\nUmiddelbart kan ovenst\u00e5ende virke som det samme, men det er vigtigt at forst\u00e5 at p\u00e5 SQL ville SQL-serveren skulle finde ALLE poster frem og sende dem over netv\u00e6rket til C5, som s\u00e5 skulle sortere dem efter Amount &#8211; blot for at applikationen kunne bruge den h\u00f8jeste af v\u00e6rdierne. Dermed sparer SQL-serveren og\u00a0kernen rigtigt meget arbejde p\u00e5 SQL s\u00e5fremt koden udnytter #SQLMaxList.<\/p>\n<p><strong>PAS P\u00c5<\/strong>: Det er en rigtig d\u00e5rlig ide at lave en insert eller update p\u00e5 det resultat du f\u00e5r retur. Det giver i hvert fald sj\u00e6ldent mening&#8230;<\/p>\n<p><strong>Jamen, jeg skal bruge mindstev\u00e6rdi \/ st\u00f8rstev\u00e6rdi \/ sum af bel\u00f8b pr. leveringsdato (blot et eksempel):<br \/>\n<\/strong>Du kan naturligvis v\u00e6lge blot at k\u00f8re flere SEARCH-l\u00f8kker som ovenfor for at f\u00e5 de forskellige tal. Men det er jo ikke ligefrem p\u00e6nt. Du kunne ogs\u00e5 v\u00e6lge at behandle samtlige poster og s\u00e5 indbygge logik i koden til at beregne separate v\u00e6rdier&#8230; Eller du kan bruge en teknik der p\u00e5 SQL-servere kaldes for GROUP BY&#8230;<\/p>\n<p>Eksempel:<\/p>\n<pre>\/\/ Bem\u00e6rk: ,-, er adskiller til GROUP BY-felter\r\n#SQLMaxList(SalesLine,Amount,-,DeliveryDate)\r\nSEARCH SalesLine #USING(NumTransLineIdx) ORDER BY DeliveryDate,Amount DESCEND WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0\u00a0IF &amp;i==0\u00a0OR &amp;DeliveryDate[&amp;i] &lt;&gt;\u00a0DeliveryDate THEN\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 #Add(&amp;i,1)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 SET &amp;DeliveryDate[&amp;i] = DeliveryDate\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 SET &amp;MaxAmount[&amp;i] = Amount\r\n\u00a0\u00a0\u00a0 ENDIF\r\nEND\r\n\r\n<\/pre>\n<p>Eksemplet opbygger to arrays med hhv. leveringsdatoer og det tilsvarende st\u00f8rste bel\u00f8b. P\u00e5 Native sker dette ved at l\u00f8be alle linjer igennem, sorteret efter leveringsdato og faldende bel\u00f8b. Der gemmes s\u00e5 en ny post hver gang leveringsdato skifter.<br \/>\nP\u00e5 SQL benyttes GROUP BY teknologien og SQL-serveren overlader en\u00a0max pr. DeliveryDate. Disse poster h\u00e5ndteres s\u00e5 af applikationskoden (dvs. i praksis ignoreres ingen poster &#8211; modsat Native afviklingen).<\/p>\n<p><strong>Hvad nu hvis jeg blot skal checke antallet af poster?<br \/>\n<\/strong>S\u00e5 skal du bruge den sidste variant af disse kommandoer, nemlig #SQLCountList. #SQLCountList virker ligesom #SQLMinList, #SQLMaxList og #SQLSumList, dog med den undtagelse at resultaterne (antal poster)\u00a0ikke returneres i de udpegede felter &#8211; men altid i RowNumber-feltet. #SQLCountList virker naturligvis ogs\u00e5 med GROUP BY varianten.<\/p>\n<p>Eksempel:<\/p>\n<pre>\/\/ Bem\u00e6rk: ,-, er adskiller til GROUP BY-felter\r\n#SQLCountList(SalesLine,Amount,-,DeliveryDate)\r\nSEARCH SalesLine #USING(NumTransLineIdx) ORDER BY DeliveryDate WHERE Number = &amp;Number\r\n\u00a0\u00a0\u00a0 IF &amp;i==0 OR &amp;DeliveryDate[&amp;i] &lt;&gt; DeliveryDate THEN\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 #Add(&amp;i,1)\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 SET &amp;DeliveryDate[&amp;i] = DeliveryDate\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ Hvis vi k\u00f8rer SQL, s\u00e5 indeholder RowNumber antallet af poster.\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 \/\/ Ellers skal hver enkelt post\u00a0t\u00e6lles\u00a0som 1.\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 SET &amp;Count[&amp;i] =\u00a0(#DBDIsSql(SalesLine) ? RowNumber : 1)\r\n\u00a0\u00a0\u00a0\u00a0ELSE\r\n\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0\u00a0 #Add(&amp;Count[&amp;i], 1)\r\n\u00a0\u00a0\u00a0 ENDIF\r\nEND\r\n\r\n<\/pre>\n<p>Eksemplet opbygger to arrays med hhv. leveringsdato og det tilsvarende antal ordrelinjer.<br \/>\nP\u00e5 Native sker det ved at alle linjer fremfindes i\u00a0leveringsdato orden. N\u00e5r leveringsdatoen skifter, skiftes index p\u00e5 arrays og\u00a0antal s\u00e6ttes\u00a0til 1. Er leveringsdatoen u\u00e6ndret t\u00e6lles antallet blot en op.<br \/>\nP\u00e5 SQL returneres kun et antal poster i RowNumber. S\u00e5 der returneres kun en post pr. leveringsdato (ELSE-forgr\u00e6ningen bliver aldrig aktiv). Det returnerede antal gemmes direkte i Count-Arrayet.<\/p>\n<p>Der findes enkelte andre optimeringsmuligheder p\u00e5 SQL end ovenst\u00e5ende, men disse er efter min mening de vigtigste. En komplet gennemgang finder du i Microsofts dokument \u00a0&#8211; disse bliver gennemg\u00e5et i dokumentet &#8220;Microsoft Dynamics C5 SQL Best Practices&#8221;, som du finder p\u00e5 PartnerSource.<\/p>\n<p><strong>PAS P\u00c5<\/strong>: Hvis du programmerer meget i\u00a0C5, s\u00e5 b\u00f8r du v\u00e6re opm\u00e6rksom p\u00e5 et enkelt alvorligt punkt, hvor SQL opf\u00f8rer sig helt anderledes en Native. L\u00e6s mere om det i\u00a0dette indl\u00e6g: <a title=\"SQL vs Native \u2013 med et tvist af en indbygget SQL-bug?\" href=\"http:\/\/blog.systemconnect.dk\/?p=194\">SQL vs Native \u2013 med et tvist af en indbygget SQL-bug?<\/a><\/p>\n","protected":false},"excerpt":{"rendered":"<p>Nogle C5-k\u00f8rsler tager bare for\u00a0lang tid. Generelt kan man selvf\u00f8lgelig pr\u00f8ve at optimere koden. Justere eller skifte algoritme. Lave simplere genneml\u00f8b. Samle flere genneml\u00f8b til f\u00e5 v.hj.a. tempor\u00e6re tabeller &#8211; eller m\u00e5ske undlade at bruge tempor\u00e6re tabeller og i stedet &hellip; <a href=\"https:\/\/scblog.lynge.org\/?p=204\">L\u00e6s resten <span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[37,4],"tags":[15,43,13,22,11,53,14,21,28],"class_list":["post-204","post","type-post","status-publish","format-standard","hentry","category-klassisk-c5","category-klassisk-c5-teknik","tag-business-solutions","tag-c5","tag-damgaard-data","tag-database","tag-dynamics","tag-klassisk-c5","tag-microsoft","tag-native","tag-sql"],"_links":{"self":[{"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=\/wp\/v2\/posts\/204","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=204"}],"version-history":[{"count":17,"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=\/wp\/v2\/posts\/204\/revisions"}],"predecessor-version":[{"id":735,"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=\/wp\/v2\/posts\/204\/revisions\/735"}],"wp:attachment":[{"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=204"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=204"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/scblog.lynge.org\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=204"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}