Waldo's Blog
Microsoft Dynamics NAV

February 2008 - Posts

The Belgian Dynamics Community

Since a few months now, we have been working on a new initiative in Belgium: the Belgian Dynamics Community.

Now, who is "we", and why would we need another "community" while there is mibuso, dynamicsuser, numerous blogs (sustained engineering, waldo Smile, Team Blog, ....), ... .

We

We are called the "board members", but I would just like to call it "the people who started off" Wink. Here is an overview:

Patrick Dalle

Former marketing manager at Navision Belgium
10 years of experience with Microsoft Dynamics
Managing director of Lemarco

Kurt Juvyns

12 years of experience with Microsoft Dynamics
Microsoft employee 

Ludo Van den Ende

Former product manager at Navision Belgium
12 years of experience with Microsoft Dynamics
Helios-IT employee 

Luc Van Dyck

Founder and administrator of the online community Mibuso
11 years of experience with Microsoft Dynamics
GMI Group employee 

Eric Wauters

Administrator of www.waldo.be
7 years of experience with Microsoft Dynamics
Partner at iFacto Business Solutions 

Rodrigue Dion

Project leader of MATEXI
8 years of experience with Microsoft Dynamics
Working for end customer

As you can see, all very experience people ... In fact, I've got less experience than any one of them Indifferent. Hm ... I should think this over ... Wink. (And the word you're looking for is ...) ... ANYWAY ... These are not only experienced people, but also one by one "community animals". And that's only positive for what we're trying to do, isn't it? So...

Why another community?

Very simple. We're not trying to compete with other online communities. In fact, we're trying to make a difference with the other communities:

  • We're trying to reach not only technical people, but also more functional consultants and end users;
  • The online community is actually a small part af the initiative: we will organise evenings for information sharing, networking, ... as well;
  • Futhermore we'll try to bring microsoft closer to the end user and vica versa, by organising round tables and such;
  • ...

That's all Smile.

To conclude, I would like to take this opportunity to announce our first event :the Belgian Dynamics Community Launch event on the 24th of April 2008.

The agenda on this event is not completely final but we can already announce that the Microsoft Dynamics NAV development team from Denmark decided not to miss this unique opportunity.

They confirmed to be present and show us upcoming technology in some sneak preview from the Microsoft Dynamics product family.

Platform updates overview - 3.70.B - 5.0 SP1

Here is the next I-think-up-to-date version of the Service Packs/Updates/hotfixes/Knowledge Base Articles/... Post. Enjoy!

Released Version

Update

Build No.

KB number

Prerequisites

3.70.B

  

19516

  

  

  

Update 1

19868

890640

  

4.0

  

19365

  

  

  

Update 1

20942

890551

  

  

Update 2

21871

908918

Update 1

4.0 SP1

  

21666

  

  

  

Update 1 

21990

913523

  

  

Update 2 – NODBC 

22373

912791

  

  

Update 3 

22363

915455

  

4.0 SP2

  

22100

  

  

  

Update 1 

22611

919407

  

  

Update 2 – NODBC

22851

921893

  

  

Update 3 

23099

922695

  

  

Update 4 – NODBC 

23460

927113

Update 3

4.0 SP3

  

23305

  

  

  

Update 1 

24080

931841

  

  

Update 2 – NODBC

  

Not released

  

  

Update 3 

24219

933727

Update 1

  

Update 4 

24449

936602

Update 1

  

Update 5 

24734

938138

Update 1 and 4

  

Update 6 

25143

940718

  

  

Update 6.1

25307

943227

Update 6

  

Update 6.2

25638

945349

Update 6

  

Update 6.3

25709

946247

Update 6

5.0

  

24199

  

  

  

Update 0.1

24632

936885

  

  

Update 1

25359

943858

  

  

Update 1.1

25581

944919

Update 1

  

Update 1.2

25344

943581

Update 1

  

Update 1.3

25653

945349

Update 1

5.0 SP1

  

  

  

  

Here is a list of knowledge base articles that were added in januari 2008 for Microsoft Dynamics NAV.

Microsoft Dynamics NAV 5.0 SP1

I know there is no download on the 5.0 SP1 yet today, but the URL already exists, so you can check once in a while. This release is really going to be quite "innovative" because of the more than 300 improvements in the application and the platform. Amongst them:

  • Introduction of Bulk Inserts and SIFT with Indexed Views.
  • Dynamics Mobile will be part of 5.0 SP1.
  • Also, NAV 5.0 SP1 will be available in all localisations.

A few remarks:

  • Because of the amount of hotfixes that comes out every month, It is quite difficult to keep above table up-to-date. That's why I will try to put only the client updates (new fin(sql).exe, nas(sql).exe, ...) above with the corresponding knowledge base articles. Furthermore, I will always add the links I get to the KB articles that were added in the current month (see link above). Natalie tends to help me on this Smile.
  • I changed the numbering. A major update (like update 1, update 2, ...) is actually a platform update. Usually these are cumulative. A minor update (like update 1.1, update 6.3, ...) are just KB articles which contain a new fin.exe, but is not cumulative. Usually the platform update is still needed to install.
  • The updates in italic are the ones I added since last time.

Any comments are always appreciated Smile.

   

SQL Server: sa locked out automatically

I had this issue at a customer of ours. The error message "Login failed for user 'sa' because the account is currently locked out" surprised me a bit. I didn't know this was possible. Actually, it is even possible to not have any admin anymore in the database, by locking out sa.

Luckily there are solutions that I would like to share with you.

You have this behaviour when you set the setting on your sa account "Enforce password policy" (Security/Logins/sa/Properties). If you uncheck this property, the sa account won't be able to lock anymore.

Now what do you do when the sa account is locked? You should run the following T-SQL Statement:

ALTER LOGIN sa WITH PASSWORD = 'yourpassword' UNLOCK

Like I said, this way, it is perfectly possible to not have a user with admin rights in your database: if you only activated SQL Server authentication. With a simple hack, you can change this as well. Just change the registery key "HKEY_LOCAL_MACHINE\Software\Microsoft\MSSqlserver\MSSqlServer\LoginMode" to 2 for Mixed mode. Then restart the SQL Server service. Now, you should be able to log in with a local administrator account and unlock the sa account like mentioned above.

Hope this was useful Smile.

Sources: MSDN / Urig

   

What impact does my C/AL have on SQL?

This is a very challenging blogpost in my opinion. I see many stories going around on what would be the best way to use the new FIND-instructions, what C/AL to write in what case. I have my own thoughts on this which I would like to share. I don't want to say the following is the best way to go, let's just say it's a good way to go Smile.

First question (and I think the most important question that is on people's mind), how to use the FIND statements. This is not easy, because using the FIND statement really depends on the type of loop you're using.

First of all the basics. I think the table from stryk explains it very well:

C/AL

SQL (combined)

Customer.SETRANGE("Country Code", 'US'); Customer.FIND('-');

SELECT *,DATALENGTH("Picture")
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))
ORDER BY "No_"

Customer.SETRANGE("Country Code", 'US');
Customer.FINDFIRST;

SELECT TOP 1 *,DATALENGTH("Picture")
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))
ORDER BY "No_"

Customer.SETRANGE("Country Code", 'US');
Customer.FIND('+');

SELECT *,DATALENGTH("Picture")
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))
ORDER BY "No_" DESC

Customer.SETRANGE("Country Code", 'US');
Customer.FINDLAST;

SELECT TOP 1 *,DATALENGTH("Picture")
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))
ORDER BY "No_" DESC

Customer.SETRANGE("Country Code", 'US');
Customer.FINDSET;

SELECT TOP 500 *,
DATALENGTH("Picture")
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))
ORDER BY "No_"

Customer.SETRANGE("Country Code", 'US');
Customer.FINDSET(TRUE);

SET TRANSACTION ISOLATION LEVEL
SERIALIZABLE
SELECT *,DATALENGTH("Picture")
FROM "dbo"."Cronus$Customer"
WITH (UPDLOCK)
WHERE (("Country Code"='US'))
ORDER BY "No_"

Customer.SETRANGE("Country Code", 'US');
IF Customer.ISEMPTY THEN;

SELECT TOP 1 NULL
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))

Customer.SETRANGE("Country Code", 'US');
IF Customer.COUNT <> 0 THEN;

SELECT COUNT(*)
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))

Customer.SETRANGE("Country Code", 'US');
IF Customer.COUNTAPPROX <> 0 THEN;

SET SHOWPLAN_ALL ON
SELECT *
FROM "dbo"."Cronus$Customer"
WITH (READUNCOMMITTED)
WHERE (("Country Code"='US'))

GLEntry.LOCKTABLE;
GLEntry.FIND('+');

SET TRANSACTION ISOLATION LEVEL
SERIALIZABLE
SELECT *
FROM "dbo"."Cronus$G_L Entry"
WITH (UPDLOCK)
ORDER BY "Entry No_" DESC

GLEntry.LOCKTABLE;
GLEntry.FINDLAST;

SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
SELECT TOP 1 *
FROM "dbo"."Cronus$G_L Entry"
WITH (UPDLOCK)
ORDER BY "Entry No_" DESC

A ground rule might be: try to fetch as less data as needed - but all data you need - in one server call. E.g. Don't fetch all records if you're not going to loop those records / Don't fetch data if you just want to know if a record is present for that range or not, ... .

The table above is nice, but it still doesn't make me think about the things I have to take in count when writing code. Let me try to help you with this by asking a few basic questions:

WHEN NOT LOOPING DATA

  • Am I going to change data?

    Suppose you write this code:

    recCust.GET('10000');
    recCust.VALIDATE(Name, 'Van Terp Kantoorinrichting');
    recCust.MODIFY(TRUE);

    Not really exciting. In fact, I see code like this going around quite often. Well, the problem with this is that you do an extra server call, because you didn't apply that you wanted to change data. In fact, you'll see this in the client monitor:

C/AL

SQL Code

Explanation

recCust.GET('10000');

SELECT *,DATALENGTH("Picture")
FROM "CRONUS BELGIË NV$Customer"
WITH (READUNCOMMITTED)
WHERE "No_"='10000'

Read the data without setting a lock (you didn't specify this)

recCust.VALIDATE(...);
recCust.MODIFY(TRUE);

SELECT *,DATALENGTH("Picture") FROM "CRONUS BELGIË NV$Customer" WITH (UPDLOCK, REPEATABLEREAD) WHERE "No_"='10000'

UPDATE "CRONUS BELGIË NV$Customer" WITH (REPEATABLEREAD) SET ...

Read the data again and place a lock.
 

Update the fields.

Two times the SELECT is overkill, and must be avoided. And avoiding it is simple. Specifying that you want to change the data can be done by "LOCKTABLE". This makes our small piece of code like:

recCust.LOCKTABLE;
recCust.GET('10000');
recCust.VALIDATE(Name, 'Van Terp Kantoorinrichting');
recCust.MODIFY(TRUE);

To make the comparison complete, here is what happens behind the scenes:

C/AL

SQL Code

Explanation

recCust.LOCKTABLE;
recCust.GET('10000');

SELECT *,DATALENGTH("Picture") FROM "CRONUS BELGIË NV$Customer" WITH (UPDLOCK, REPEATABLEREAD) WHERE "No_"='10000'

Read the data with setting a lock

recCust.VALIDATE(...);
recCust.MODIFY(TRUE);

UPDATE "CRONUS BELGIË NV$Customer" WITH (REPEATABLEREAD) SET ...

  

This being said, take a look at following example:

IF ICHandledInboxTransaction.FIND('-') THEN BEGIN
  ICHandledInboxTransaction.LOCKTABLE;
  ICHandledInboxTransaction.Status := ICHandledInboxTransaction.Status::Posted;
  ICHandledInboxTransaction.MODIFY;
END;

This is code from default NAV (earlier version) which could be replaced by (to not change the behaviour on Native):

IF RECORDLEVELLOCKING THEN
 
ICHandledInboxTransaction.LOCKTABLE;
IF ICHandledInboxTransaction.FINDFIRST THEN BEGIN
 
ICHandledInboxTransaction.LOCKTABLE;
 
ICHandledInboxTransaction.Status := ICHandledInboxTransaction.Status::Posted;
 
ICHandledInboxTransaction.MODIFY;
END;

The statement "RECORDLEVELLOCKING" is the way for checking in code whether you're working on SQL Server (TRUE) or Native (FALSE). Furthermore, I added these two lines of code because it's necessary to lock before we read (to send the UPDLOCK in SQL).

  • Does a record exist?

    There are two (good) ways to check whether a record is present for a certain range:

    • Using FINDFIRST: Use this statement when you need the data of the first record, and the first record only.
    • Using ISEMPTY: this is a somewhat forgotten statement, but it's lighter then the FINDFIRST. If you only want to check if a record exists in the filtered range, but you don't need any value of a field of any of the records, use this statement.

    See table above for what SQL Statement it produces (TOP 1 * or TOP 1 NULL).

    For example:

    IF SalesLine.FIND('-') THEN
      Cust.CheckBlockedCustOnDocs(Cust,"Document Type",TRUE,TRUE);

    Could be replaced by

    IF NOT SalesLine.ISEMPTY THEN
     
    Cust.CheckBlockedCustOnDocs(Cust,"Document Type",TRUE,TRUE);

WHEN LOOPING DATA

That wasn't that exciting, was it? But what if we want to loop data. That's a whole different story Indifferent.

First thing that I have to state is that NAV uses cursors to simulate native behaviour. The new FIND statements (from 4.0) are the first step to go around this native behaviour.

Looping without changing data

In Stryk's tabel above, you can see the difference on SQL level between FIND('-'), FINDFIRST and FINDSET. I'll try to make the comparison of what happens on SQL Server:

IF recCust.FINDFIRST THEN
REPEAT

UNTIL recCust.NEXT = 0;

SELECT TOP 1 *,DATALENGTH("Picture") FROM "CRONUS BELGIË NV$Customer" WITH (READUNCOMMITTED) ORDER BY "No_"

SELECT *,DATALENGTH("Picture") FROM "CRONUS BELGIË NV$Customer" WITH (READUNCOMMITTED) WHERE "No_">'01121212' ORDER BY "No_"

FETCH 5
FETCH 20
FETCH 40
FETCH 40

IF recCust.FINDSET THEN
REPEAT

UNTIL recCust.NEXT = 0;

SELECT TOP 501 *,DATALENGTH("Picture") FROM "CRONUS BELGIË NV$Customer" WITH (READUNCOMMITTED) ORDER BY "No_"

  

IF recCust.FIND('-') THEN
REPEAT

UNTIL recCust.NEXT = 0;

SELECT *,DATALENGTH("Picture") FROM "CRONUS BELGIË NV$Customer" WITH (READUNCOMMITTED) ORDER BY "No_"

FETCH 5
FETCH 20
FETCH 40
FETCH 40

What do we learn from this:

  • The best way to go is using the FINDSET. Why? It's fetching a SET of records and will be working with this set all the way. You see that there is no cursor activity (no FETCH statements). If you read further, you'll see that this is not always the case!
  • FINDFIRST is worse then FIND('-') when looping records because of the extra call (TOP 1 *). NAV does not expect a loop after a FINDFIRST. If it sees that you are looping, it's doing a new SELECT * (same as FIND('-') statement) to be ready for the loop and working with cursors to loop through the data (therefore the FETCH statements). Therefore, please do not just replace FIND('-') with FINDFIRST.
  • Never blindly replace FIND('-') by FINDFIRST. Think of what you're doing.

Now, what about the number of records? Does that count?

Well, NAV behaves like this: if the set is bigger then 500 records (or the size you specified as "Record Set" value in the "Alter Database" window), it will not use the recordset, but is going to "cursor" it's way through the rest of the loop, like "old school". I'll try to show it using tho examples. In both of them, the code runs 2840 times through the loop (or in other words, the messages shows "2840").

i := 0;

IF recGL.FIND('-') THEN
REPEAT
 
i := i + 1;
UNTIL recGL.NEXT = 0;

 MESSAGE('%1',i);

SELECT * FROM "CRONUS BELGIË NV$G_L Entry" WITH (READUNCOMMITTED) ORDER BY "Entry No_"

FETCH 5
FETCH 20
FETCH 60
FETCH 60
FETCH 60

.
.
.

A total of 2845 "FETCHes"

i := 0;
IF recGL.FINDSET THEN
REPEAT
 
i := i + 1;
UNTIL recGL.NEXT = 0;

 MESSAGE('%1',i);

SELECT TOP 501 * FROM "CRONUS BELGIË NV$G_L Entry" WITH (READUNCOMMITTED) ORDER BY "Entry No_"

SELECT * FROM "CRONUS BELGIË NV$G_L Entry" WITH (READUNCOMMITTED) WHERE "Entry No_">501 ORDER BY "Entry No_"

FETCH 5
FETCH 20
FETCH 60
FETCH 60
FETCH 60
.
.
.

A total of 2365 "FETCHes"

In case of the FINDSET: A first impression shows you the when the resultset is bigger than 500 records, it just throws an extra database call without the "TOP 500" and is going to cursor it's way through the records. When you examine deeper, you conclude that it's actually looping through the recordset of 500 records, and after that, NAV does not call the next 500 records, but just trows a "SELECT *" and is going to cursor it's way through the rest of the records.

That's why with FINDSET, we have about 500 FETCHes less than FIND('-'). BUT, we have an extra database call, which is quite "heavy" (a "SELECT *" is heavier than a "FETCH 60").

It's hard to discuss what's best. Further benchmarking should point that out. I would bet my money on using FINDSET in all cases. Why?

  • You avoid about 10 times a FETCH x call
  • A "SELECT TOP 500" as extra database call doesn't seem that heavy for me.

If anyone is interested in doing a thorough investigation on this ... Please count me in or let me know the resultsWink.

What do we learn from this:

  • Using FINDSET in front of every loop, is a pretty sure bet.
  • Using FINDSET only gets the first 500 records as a set, but is going to cursor it's way through the rest of the records like FIND('-').

Looping with changing data

The same rules apply like above, only see if you're locking the data before you're reading it. You can do this by either using LOCKTABLE, like:

recGL.LOCKTABLE;
IF recGL.FINDSET THEN
REPEAT
...
UNTIL recGL.NEXT = 0;

Or, if using the FINDSET statement, you can use its parameter like:

IF recGL.FINDSET(TRUE) THEN
REPEAT
...
UNTIL recGL.NEXT = 0;

There is also an option FINDSET(TRUE, TRUE). Only use this if you are going to modify any field value within the current key. Now, I really don't recommend to use this. You have to be aware for the "NEXT FROM HELL" (like Hynek likes to call it).

The "Next from hell" can happen when you filter on a value, and in a loop, you're changing that value. The loop jumps to places that are hard to predict. Also, when you change a key value in a loop (even without modify), a "NEXT" can give unexpected results. Try to avoid these situations by just declaring a new variable, and modify in that variable. For example, this is looking for trouble:

recCust.SETRANGE("Country/Region Code", 'BE');
IF recCust.FINDSET(TRUE) THEN
REPEAT
 
recCust.VALIDATE("Country/Region Code", 'US');
 
recCust.MODIFY;
UNTIL recCust.NEXT = 0;

You're modifying the Country Code while you're filtered on that field. Try to use a seperate record variable, and leave your looping variable intact, like:

recCust.SETRANGE("Country/Region Code", 'BE');
IF recCust.FINDSET(TRUE) THEN
REPEAT
 
recCust2 := recCust;
 
recCust2.VALIDATE("Country/Region Code", 'US');
 
recCust2.MODIFY;
UNTIL recCust.NEXT = 0;

Conclusion

When I started writing this article, I didn't expect that it was so hard to put this subject into words. I hope the above is clear to you, but to be sure, I just wanted to give you an overview of the things mentioned above.

  • Not looping data
    • Modifying data
      • Use LOCKTABLE with GET or FINDFIRST
    • Not modifying data (usually to check if a record exists or not)
      • Use FINDFIRST if you need the data of the record further down the road
      • Use ISEMPTY if you do not need the values of the keys
  • Looping data
    • Not modifying data
      • Use loop like: IF FINDSET THEN REPEAT UNTIL NEXT=0;
    • Modifying data: see that you lock the records that you read.
      • Use loop like: IF FINDSET(TRUE) THEN REPEAT UNTIL NEXT=0;
    • Mind the "NEXT FROM HELL" !

So, always think what the code is doing, then make up your mind what you will need for most optimal performance on SQL Server.    

Writing this article, I used the SQL Profiler and Client monitor to monitor what my code is doing. I noticed very few people know what this client monitor can do, so who knows, there might be an upcoming blog about monitoring your code Wink.

Any comments are appreciated!