Foxhound 1.2 is now available...
Easier management of up to 100 Monitor sessions.
Faster scrolling through performance history.
Better support for adhoc reporting.

Wednesday, February 15, 2012

Example: ISDATE Is Smarter Than It Looks

Question: If SQL Anywhere has ISNUMERIC() and ISDATE() functions, how come it doesn't have ISTIME() and ISTIMESTAMP()?

Answer: It does, they're both called ISDATE()!

Here's proof... Four tests involving strings containing good and bad DATE, TIME, TIMESTAMP and UTC TIMESTAMP literals:

BEGIN 
   DECLARE @good_date            VARCHAR ( 100 );
   DECLARE @good_time            VARCHAR ( 100 );
   DECLARE @good_timestamp       VARCHAR ( 100 );
   DECLARE @good_utc_timestamp   VARCHAR ( 100 );
   DECLARE @good_time2           VARCHAR ( 100 );
   DECLARE @bad_date             VARCHAR ( 100 );
   DECLARE @bad_time             VARCHAR ( 100 );
   DECLARE @bad_timestamp        VARCHAR ( 100 );
   DECLARE @bad_utc_timestamp    VARCHAR ( 100 );
   DECLARE @bad_time2            VARCHAR ( 100 );

   SET @good_date          = CAST ( CURRENT DATE AS VARCHAR ( 100 ) );
   SET @good_time          = CAST ( CURRENT TIME AS VARCHAR ( 100 ) );
   SET @good_timestamp     = CAST ( CURRENT TIMESTAMP AS VARCHAR ( 100 ) );
   SET @good_utc_timestamp = CAST ( CURRENT UTC TIMESTAMP AS VARCHAR ( 100 ) );
   SET @good_time2         = '1:1';

   SET @bad_date           = STRING ( @good_date, 'x' );
   SET @bad_time           = STRING ( @good_time, 'x' );
   SET @bad_timestamp      = STRING ( @good_timestamp, 'x' );
   SET @bad_utc_timestamp  = STRING ( @good_utc_timestamp, 'x' );
   SET @bad_time2          = STRING ( @good_time2, 'x' );

   SELECT '(1a)' AS "Test", @good_date AS "Value", ISDATE ( @good_date ) AS "ISDATE"  
   UNION
   SELECT '(1b)', @bad_date, ISDATE ( @bad_date ) 
   UNION
   SELECT '(2a)', @good_time, ISDATE ( @good_time ) 
   UNION
   SELECT '(2b)', @bad_time, ISDATE ( @bad_time ) 
   UNION
   SELECT '(3a)', @good_timestamp, ISDATE ( @good_timestamp ) 
   UNION
   SELECT '(3b)', @bad_timestamp, ISDATE ( @bad_timestamp ) 
   UNION
   SELECT '(4a)', @good_utc_timestamp, ISDATE ( @good_utc_timestamp ) 
   UNION
   SELECT '(4b)', @bad_utc_timestamp, ISDATE ( @bad_utc_timestamp ) 
   UNION
   SELECT '(5a)', @good_time2, ISDATE ( @good_time2 ) 
   UNION
   SELECT '(5b)', @bad_time2, ISDATE ( @bad_time2 ) 
   ORDER BY 1;

END;

Test     Value                           ISDATE
(1a)    '2012-02-14'                        1
(1b)    '2012-02-14x'                       0
(2a)    '15:26:48.592'                      1
(2b)    '15:26:48.592x'                     0
(3a)    '2012-02-14 15:26:48.592'           1
(3b)    '2012-02-14 15:26:48.592x'          0
(4a)    '2012-02-14 20:26:48.592+00:0'      1
(4b)    '2012-02-14 20:26:48.592+00:00x'    0
(5a)    '1:1'                               1
(5b)    '1:1x'                              0

Test 5(a) is especially interesting: the string '1:1' passes the ISVALID test... that's because it's not just a valid TIME but it's also a valid DATE, TIMESTAMP and UTC TIMESTAMP!

How can that be?


It's because in SQL Anywhere a valid DATE is also a valid TIME, and vice versa: a valid TIME is also valid DATE.
SELECT '3-2-1' AS "DATE", 
       CAST ( "DATE" AS DATE )      AS "AS DATE",
       CAST ( "DATE" AS TIME )      AS "AS TIME",
       CAST ( "DATE" AS TIMESTAMP ) AS "AS TIMESTAMP";

SELECT '2:1' AS "TIME", 
       CAST ( "TIME" AS DATE )      AS "AS DATE",
       CAST ( "TIME" AS TIME )      AS "AS TIME",
       CAST ( "TIME" AS TIMESTAMP ) AS "AS TIMESTAMP";

DATE,AS DATE,AS TIME,AS TIMESTAMP
'3-2-1','2003-02-01','00:00:00.000','2003-02-01 00:00:00.000'

TIME,AS DATE,AS TIME,AS TIMESTAMP
'2:1','2012-02-14','02:01:00.000','2012-02-14 02:01:00.000'

Curious, isn't it?


Not the part about a-valid-THIS-is-a-valid-THAT, but the part about "today" being used to fill in a missing date part, but not "now" for a missing time part: '00:00:00.000' is used instead.

Dilbert.com


Tuesday, February 14, 2012

Latest SQL Anywhere EBFs - 11.0.1 For (Almost) Everyone (Again)!

The three asterisks "***" show what's new since the previous list.

  • Only EBFs for the latest fully-supported versions of SQL Anywhere are shown here: 10.0.1, 11.0.1 and 12.0.1.

  • Note: Some earlier versions are also fully supported but they are not shown here; e.g., the 12.0.0.2758 EBF for Windows was recently released. For more information see SQL Anywhere Supported Platforms and Engineering Support Status.

  • Also: Just because an older version isn't "fully supported" doesn't mean you can't download files (or ask questions, or get help), it just means there won't be any more new EBFs released. For a complete list of everything available for download see Downloads - EBFs / Maintenance.

Current builds for the most active platforms...

HP-UX Itanium    12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2753 EBF ***   13 Feb 2012 ***
                 10.0.1.3777 EBF       14 Oct 2008

IBM AIX          12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2753 EBF ***   13 Feb 2012 ***

Linux x86/x64    12.0.1.3519 EBF       20 Dec 2011
                 11.0.1.2753 EBF ***   13 Feb 2012 ***
                 10.0.1.4310 EBF       17 Jan 2012

Mac OS           12.0.1.3352 EBF       26 May 2011
                 11.0.1.2449 EBF       29 Jun 2010
                 10.0.1.4042 EBF       01 Oct 2010

Solaris SPARC    12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2753 EBF ***   13 Feb 2012 ***
                 10.0.1.4310 EBF       17 Jan 2012

Solaris x64      12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2753 EBF ***   13 Feb 2012 ***
                 10.0.1.3870 EBF       01 Apr 2009

Windows x86/x64  12.0.1.3554 EBF       31 Jan 2012
                 11.0.1.2755 EBF       10 Feb 2012 ***
                 10.0.1.4310 EBF       12 Jan 2012
[Download EBFs here] [Register for the Fuji Beta here]

...and for the other platforms:

HP-UX PA-RISC    10.0.1.3778 EBF                 16 Oct 2008

Linux Itanium    10.0.1.3777 EBF                 14 Oct 2008

NetWare          10.0.1.4036 EBF                 10 Mar 2010

Windows CE       10.0.1.4036 EBF                 09 Mar 2010

Windows Itanium  10.0.1 GA Upgrade from 10.0.0   26 Apr 2007
[Download EBFs here]


Friday, February 10, 2012

Why SQL Server Sucks: ISNULL

Question: Why does SQL Anywhere have both ISNULL() and COALESCE()?

They both work exactly the same way, they both perform a useful function (return the first non-null argument), and they both have stupid names, so why the duplication?

Why not have just one function, with a decent name, like FIRST_NON_NULL()?

Answer: Here's a guess: It might have something to do with "Transact SQL compatibility" which is a historical requirement that by its very nature breaks the fundamental rule "WATCOM does things the way they should be done."

In this case, however, "compatibility" is not absolute: In Microsoft SQL Server the two functions do not work exactly the same way. For one thing, Transact SQL ISNULL() accepts only two arguments whereas Transact SQL COALESCE() accepts two or more.

But it gets worse...


When calling ISNULL() in Microsoft SQL Server the second argument "must be of a type that is implicitly convertible to the type" of the first argument.

That sounds OK, until you think about it.

Even after you think about it, it probably still sounds OK... ISNULL() could return either argument so they have to have compatible types, right?

It sounds OK until you actually run a test using Microsoft SQL Server 2008:
1> BEGIN
2>    DECLARE @hello VARCHAR ( 10 );
3>    SET @hello = NULL;
4>    SELECT ISNULL ( @hello, 'Hello, World!' ) AS Result;
5> END
6> GO
 Result
 ----------
 Hello, Wor

(1 row affected)

Go ahead, justify that behavior... the inventors of SQL Anywhere clearly couldn't, which is probably why they didn't try.

In SQL Anywhere "the way things should be done" trumps "Transact SQL compatibility" and here's a test using SQL Anywhere 12 to prove it:
BEGIN
   DECLARE @hello VARCHAR ( 10 );
   SET @hello = NULL;
   SELECT ISNULL ( @hello, 'Hello, World!' ) AS Result;
END
GO

Result
'Hello, World!'

For the record, SQL Server and SQL Anywhere both return 'Hello, World!' when you call COALESCE() instead of ISNULL().

...and for a longer discussion of ISNULL's strange behavior in SQL Server see Twist in ISNULL function.


Wednesday, February 8, 2012

Example: UNLOAD INTO VARIABLE and EXECUTE IMMEDIATE

Way back in January I made a New Year's Resolution to "Post More Examples Here", so here we go (and I'm set for another year, right?)...



Question: How do I write a SQL Anywhere function that accepts a SELECT statement in a string parameter and returns a string containing the result set with each row delimited by [square brackets] and separated by commas, and adjacent columns separated by tilde '^' characters?

Here's a demonstration of how it should work:
CREATE TABLE person (
   first_name    VARCHAR ( 100 ) PRIMARY KEY,
   member_since  DATE,
   last_seen     DATE,
   visits        INTEGER );

INSERT person VALUES ( 'Joe',  '2009-11-10', '2012-02-01', 1  );
INSERT person VALUES ( 'Mary', '2009-11-08', '2012-01-31', 45 );
INSERT person VALUES ( 'Tom',  '2009-11-11', '2012-01-30', 12 );
COMMIT;

SELECT to_string ( 'SELECT * FROM Person ORDER BY first_name' ) AS result;

result
[Joe^2009-11-10^2012-02-01^1],[Mary^2009-11-08^2012-01-31^45],[Tom^2009-11-11^2012-01-30^12]

Answer: Phrases like "row delimited by" and "columns separated by" should make every SQL Anywhere developer think of the UNLOAD and OUTPUT statements... in fact, if you look up DELIMITED BY in the Help that's what you'll find.

In this case, OUTPUT won't work because it's unique to the ISQL utility and can't be embedded inside a function, so UNLOAD it will have to be.

Let's tackle the problem step-by-step:

Step 1: Build A Prototype UNLOAD SELECT


Here's a standalone UNLOAD built with a few clauses described in the Help:
UNLOAD
   SELECT * FROM Person ORDER BY first_name
   TO 'c:/temp/result.txt'
   ROW DELIMITED BY '],['
   DELIMITED BY '^'
   ESCAPES OFF
   HEXADECIMAL OFF
   QUOTES OFF;

The UNLOAD and TO clauses on lines 1 and 3 are wrapped around the SELECT to tell SQL Anywhere to send the entire result set to a text file.

The ROW DELIMITED BY clause on line 4 really should be called "ROW TERMINATED BY" because it tells SQL Anywhere to place the character string '],[' at the right end of each output row including the last. This isn't exactly what we want, but it's the only row-separator-or-terminator clause that UNLOAD gives us... so we'll take it for now and deal with it later.

The DELIMITED BY clause on line 5 really should be called "COLUMNS SEPARATED BY" because it tells SQL Anywhere to put the tilde '^' between adjacent columns... in this case, that's exactly what we want.

The remaining three clauses on lines 6 through 8 tell SQL Anywhere to write all the column values as simple strings with no extra formatting: no backslash-escape sequences, no \0x hexadecimal formatting and no 'single quotes' around string values.

Here's what the output file looks like... close, but no cigar:
Joe^2009-11-10^2012-02-01^1],[Mary^2009-11-08^2012-01-31^45],[Tom^2009-11-11^2012-01-30^12],[

Step 2: Fix The Formatting


There are two problems with the output: The leading '[' is missing from the first row and an extra ',[' is tacked on to the end of the last row.

Those problems can't be solved by fiddling with UNLOAD clauses, it requires code:
BEGIN
   DECLARE @result LONG VARCHAR;
   UNLOAD
      SELECT * FROM Person ORDER BY first_name
      INTO VARIABLE @result
      ROW DELIMITED BY '],['
      DELIMITED BY '^'
      ESCAPES OFF
      HEXADECIMAL OFF
      QUOTES OFF;
   SELECT STRING ( '[', LEFT ( @result, LENGTH ( @result ) - 2 ) ) AS result;    
END;

The INTO VARIABLE clause on line 5 puts the output from UNLOAD into a local variable instead of a file, and the SELECT STRING statement fixes the two problems by tacking a '[' onto the front and chopping off the trailing ',['.

Now the formatting is OK:
result
'[Joe^2009-11-10^2012-02-01^1],[Mary^2009-11-08^2012-01-31^45],[Tom^2009-11-11^2012-01-30^12]'

Step 3: Turn It Into A General-Purpose Function


The UNLOAD statement expects a SELECT statement, not a string containing a SELECT statement like the function's supposed to use...

...what to do, oh, what to do?


Answer: Turn the whole UNLOAD statement into a string and use EXECUTE IMMEDIATE to run it!

Here's how:
CREATE FUNCTION to_string (
   IN @select LONG VARCHAR )
   RETURNS  LONG VARCHAR 
BEGIN
   DECLARE @unload LONG VARCHAR;
   DECLARE @result LONG VARCHAR;
   SET @unload = STRING (
      'UNLOAD ',
      @select,
      ' INTO VARIABLE @result ',
      ' DELIMITED BY ''^'' ',
      ' ROW DELIMITED BY ''],['' ',
      ' ESCAPES OFF HEXADECIMAL OFF QUOTES OFF' );
   EXECUTE IMMEDIATE @unload;
   RETURN STRING ( '[', LEFT ( @result, LENGTH ( @result ) - 2 ) );    
END;

The SET statement on lines 7 through 13 builds the entire UNLOAD statement and the EXECUTE IMMEDIATE on line 14 runs it.

The input SELECT is added to the UNLOAD on line 9.

The delimiter strings on lines 11 and 12 have their single quotes ''doubled up'' because they are embedded in the outer 'string literals'.

The magic of EXECUTE IMMEDIATE allows you to embed a reference to a local variable inside the statement to be executed (INTO VARIABLE @result on line 10) and then refer to that variable again afterwards: the LEFT ( @result, ... on line 15.

Dilbert.com


Monday, February 6, 2012

Latest SQL Anywhere EBF - 12.0.1.3554 for Windows

The three asterisks "***" show what's new since the previous list.

  • Only EBFs for the latest fully-supported versions of SQL Anywhere are shown here: 10.0.1, 11.0.1 and 12.0.1.

  • Note: Some earlier versions are also fully supported but they are not shown here; e.g., the 12.0.0.2758 EBF for Windows was recently released. For more information see SQL Anywhere Supported Platforms and Engineering Support Status.

  • Also: Just because an older version isn't "fully supported" doesn't mean you can't download files (or ask questions, or get help), it just means there won't be any more new EBFs released. For a complete list of everything available for download see Downloads - EBFs / Maintenance.

Current builds for the most active platforms...

HP-UX Itanium    12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2745 EBF       24 Jan 2012
                 10.0.1.3777 EBF       14 Oct 2008

IBM AIX          12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2745 EBF       26 Jan 2012

Linux x86/x64    12.0.1.3519 EBF       20 Dec 2011
                 11.0.1.2745 EBF       24 Jan 2012
                 10.0.1.4310 EBF       17 Jan 2012

Mac OS           12.0.1.3352 EBF       26 May 2011
                 11.0.1.2449 EBF       29 Jun 2010
                 10.0.1.4042 EBF       01 Oct 2010

Solaris SPARC    12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2745 EBF       26 Jan 2012
                 10.0.1.4310 EBF       17 Jan 2012

Solaris x64      12.0.1.3523 EBF       15 Dec 2011
                 11.0.1.2745 EBF       24 Jan 2012
                 10.0.1.3870 EBF       01 Apr 2009

Windows x86/x64  12.0.1.3554 EBF ***   31 Jan 2012 ***
                 11.0.1.2744 EBF       26 Jan 2012
                 10.0.1.4310 EBF       12 Jan 2012
[Download EBFs here] [Register for the Fuji Beta here]

...and for the other platforms:

HP-UX PA-RISC    10.0.1.3778 EBF                 16 Oct 2008

Linux Itanium    10.0.1.3777 EBF                 14 Oct 2008

NetWare          10.0.1.4036 EBF                 10 Mar 2010

Windows CE       10.0.1.4036 EBF                 09 Mar 2010

Windows Itanium  10.0.1 GA Upgrade from 10.0.0   26 Apr 2007
[Download EBFs here]


Friday, February 3, 2012

Coding Good Comments: Modification History

The easiest kind of comment to write is Modification History: A comment which identifies the "What?" and "When?" of each change made to the program code and serves to answer questions like "Did I remember to make some change?"

Too bad Modification History comments are also be the least important kind of comment, certainly less important than Exhortations and Explanations, probably less important than Section Titles.

Nevertheless, Modification History comments can be helpful when they provide a concise overview of changes made from the developer's point of view... with the emphasis on "concise" and "developer's point of view" since version control utilities and even source code comparison programs can provide all the excruciating syntactical detail you would ever need.

Example 1

The first few lines of Foxhound's most important and most heavily-modified stored procedure looks like this:
-- 2007 02 04 BC 1988a: Date begun.
-- (see bottom of file for earlier modification history)
-- 2011 12 05 BC 4025a: Published: Foxhound GA 1.2.4025a
-- 2011 12 25 BC 4033a: Removed: Old commented-out code.
-- 2012 02 19 BC 4046a: Added:   SELECT ... rroad_global_options.enable_schedules   INTO ... @enable_schedules   FROM rroad_global_options;
-- 2012 02 19 BC 4046a: Changed: Comment renumbered: -- Step 5 - Check to see if sampling has been turned off. 
-- 2012 02 19 BC 4046a: Changed: Comment renumbered: -- Step 6 - Get the properties. 
-- 2012 02 19 BC 4046a: Added:   Code section: -- Check the connection count.
-- 2012 02 19 BC 4046a: Added:   Code section: -- Check the connection sample schedule.
-- 2012 02 19 BC 4046a: Changed: IF @connection_properties_should_be_retrieved = 'Y' THEN

PARAMETERS MONITOR_DEBUG_MESSAGES; 

-------------------------------------------------------------------------------------------------------------------------------------------------
MESSAGE STRING ( '****************************** CREATE PROCEDURE rroad_monitor_sample' ) TO CLIENT;

BEGIN
   DROP PROCEDURE rroad_monitor_sample;
   EXCEPTION WHEN OTHERS THEN
END;

-----------------------------------------------------------------------------------
CREATE PROCEDURE rroad_monitor_sample (
   IN  @sampling_id                        UNSIGNED INTEGER )

BEGIN

Each Modification History comment is limited to one line, and includes the date, developer's initials and the build number which first included the change.

Some changes are eyecatchers, like "Published: Foxhound GA 1.2.4025a" which server to identify big events in the lifespan of the application.

Other changes include snippets of code to facilitate future searches: "Added: SELECT ... rroad_global_options.enable_schedules INTO ... @enable_schedules FROM rroad_global_options;"

When whole sections of code are affected, the Section Title comment is included, as in "Added: Code section: -- Check the connection count."

Generally speaking, only the most recent Modification History entries stand a chance of being important to the developer, which is why the older entries are moved out of the way: "(see bottom of file for earlier modification history)". In the case of this module there are 387 lines of Modification History comments at the bottom of the module... someday, they might be deleted, but they're not in the way and from time to time they're useful, so the effort required to choose the cutoff-point for each module has never seemed worthwhile.

Example 2

It is tempting to code Modification History comments down in the body of the code, close to the affected lines, especially when the effort required to make the changes was very high... but

Resist The Temptation!

Modification History comments just . . . aren't . . . that . . . important! ...and it's just that kind of clutter that gives comments a bad name.

With every rule, however, comes an exception, and Foxhound has exactly one. In the previous article on Exhortations this comment was described:

Here's an example from inside a SQL script that contains CREATE TABLE and other definitions for tables that are subject to the "Data Upgrade" process when an old copy of the application database is upgraded to a new version:
-- ***********************************************************************
-- ***** EVERY CHANGE DOCUMENTED HERE MUST ALSO BE DOCUMENTED IN     *****
-- ***** 015c_rroad_data_upgrade_startup.sql EVEN IF IT IS           *****
-- ***** NOT SIGNIFICANT TO THE DATA UPGRADE PROCESS.                *****
-- ***********************************************************************


The "MUST ALSO BE DOCUMENTED" exhortation is put into effect with a series of Modification History comments copied from the first module and embedded down in the code of the second:
-- 4026-9999 eighth historical era

   -- Changes requiring work in this module...
   -- 2011 12 18 BC 4026a: Added:   CREATE TABLE rroad_schedule (
   -- 2011 12 18 BC 4026a: Added:   CREATE TABLE rroad_schedule_day_entry (
   -- 2011 12 18 BC 4026a: Added:   CREATE TABLE rroad_schedule_period_entry (
   -- 2012 02 20 BC 4047a: Added:   Column rroad_alert.email_status                LONG VARCHAR NOT NULL );
   -- 2012 02 20 BC 4047a: Added:   Column rroad_all_clear.email_status                LONG VARCHAR NOT NULL );
   -- 2012 02 20 BC 4047a: Added:   Column rroad_alert_cancelled.email_status                LONG VARCHAR NOT NULL );

   -- Changes not requiring work in this module...
   -- 2011 12 15 BC 4026a: Changed: Comment: rroad_alerts_criteria.display_alerts marked "-- no longer used"
   -- 2011 12 27 BC 4033a: Added:   Column rroad_global_options.enable_schedules           VARCHAR ( 1 ) NOT NULL DEFAULT 'Y' CHECK ( @enable_schedules IN ( 'Y', 'N' ) ),
   -- 2011 12 27 BC 4034a: Added:   Column rroad_schedule.enforce_this_schedule            VARCHAR ( 1 ) NOT NULL DEFAULT 'Y' CHECK ( @enforce_this_schedule IN ( 'Y', 'N' ) ),
   -- 2011 12 27 BC 4036a: Added:   Column rroad_sampling_options.activate_sample_schedule VARCHAR ( 1 ) NOT NULL DEFAULT 'Y' CHECK ( @activate_sample_schedule IN ( 'Y', 'N' ) ),
   -- 2011 12 31 BC 4041a: Changed: rroad_schedule_day_entry.day_number ... CHECK ( @day_number BETWEEN 1 AND 7 ),
   -- 2011 12 31 BC 4041a: Changed: rroad_schedule_period_entry.period_number ... CHECK ( @period_number BETWEEN 1 AND 672 ),
   -- 2011 12 31 BC 4041a: Changed: rroad_schedule_period_entry.period_code ... CHECK ( @period_code IN ( 'Y', '.' ) ),
   -- 2012 02 17 BC 4044a: Added:   Column rroad_sampling_options.schedule_request         VARCHAR ( 100 ) NOT NULL DEFAULT '',

Ugly, but easy to do, and easy to check later on to see if anything got missed... and yes, "checking later" is a frequent task.

The Bottom Line

If you're not going to do a thorough job of writing Modification History comments, then don't bother trying... nothing is more wasteful than a poorly executed task that results in a work product nobody uses.

"Thorough" doesn't mean "provide the same level of detail as a source code comparison program."

It doesn't even mean "document every trivial syntax error" that you fixed... but it does mean "list every non-trivial maintenance task you performed on the module"... on the code and on the other comments.

What? All of a sudden comments aren't important? Well, if that's how you feel, see "don't bother trying"... most don't.


Wednesday, February 1, 2012

Coding Good Comments: Explanations

The code comes first: If there's a choice to be made between writing clear, understandable program code and writing comments to explain less-than-readable code, comments should take a back seat.

The reason is simple: It's the code that determines what the program does, not the comments. Comments aren't even compiled, let alone executed. So it shouldn't come as a surprise that maintenance programmers concentrate on the code, often skipping over the comments altogether.

Sadly, however, even readable, understandable program code tells only part of the story: It only answers the question, "How is this program accomplishing its tasks?" Very few executable statements directly address the question, "What is this program doing?", with the possible exception of a CALL statement where the procedure name has been carefully crafted to answer the "What?"

There are no exceptions, however, when it comes to answering the questions, "Why is the program doing this thing?" or the much harder "Why isn't the program doing this other thing?"

So, comments come second, but

Explanations Are Important


Explanation: A concise comment describing the "What" and "Why" of a section of program code while avoiding the "How" which should be apparent from reading the code itself.

Example 1

Here is a short section of simple code (basically a SELECT FROM DUMMY) with several explanations, some good, some not so good:
------------------------------------------------------------
      -- Perform the basic heartbeat or canarian query.
      -- This is done to calculate the heartbeat time as well check the connection.
      -- Note: The target database can be stopped and restarted, and the monitor should just keep trying to connect.
      -- Note: When Foxhound itself is is stopped and restarted, reconnection to target databases *should* be automatic.

      SET @sql = STRING ( 'SELECT dummy_col INTO @dummy_col FROM ', @proxy_owner, '.proxy_DUMMY' );

      SET @canarian_query_started_at  = CURRENT TIMESTAMP;
      SET @canarian_query_finished_at = @canarian_query_started_at;

      EXECUTE IMMEDIATE @sql;

      ROLLBACK; -- release locks, recover from possible failed proxy connection

      SET @connected_ok = 'Y';
      SET @canarian_query_finished_at = CURRENT TIMESTAMP;

      MESSAGE STRING ( 'DIAG ', CURRENT TIMESTAMP, ' ', CONNECTION_PROPERTY ( 'Number' ), 
         ' 204-4 Monitor ', @sampling_id, ': First heartbeat OK' ) TO CONSOLE DEBUG ONLY;

      -- Note: Because the proxy_DUMMY table is the last proxy object created, a 
      -- successful query against proxy_DUMMY may taken as proof that a connection exists
      -- and is usable. This is important because a previous connection attempt
      -- may be dropped (timed out) after it created some but not all necessary proxy objects.
      -- Such a partly-established connection must be treated as no connection at all, and
      -- that is guaranteed by creating proxy_DUMMY last.

In the comment "This is done..." the phrase "as well as check the connection" provides extra information about the "What?" of this code, so that's OK as an explanation.

The phrase "calculate the heartbeat time" is a bit redundant, however... not wrong, but not really necessary given the preceding section title "Perform the basic heartbeat or canarian query."

The two one-line "Note:" comments are a bit other-worldly because they state true facts that have very little (nothing?) to do with nearby code. These score zero on the Comment Value Scale; not a positive score because they offer no real value, but not a negative score because at least they aren't wrong.

The long "Note: Because the proxy_DUMMY table is ..." paragraph scores very high, however, because it explains a very important "Why?" for this otherwise trivial-looking section of code.

Example 2

This example shows two separate explanations that were added to the code at two separate times.

The later paragraph "It is possible..." came first, chronologically speaking, to explain why the unusual step of calling GET_IDENTITY was performed instead of just letting the later INSERT assign the value... so far, so good, the original "Why?" is covered.
------------------------------------------------------------
-- Calculate candidate primary key for successful sample.

-- Note: The following discussion is moot, but @candidate_sample_set_number is still
--       assigned because later code refers to it.

-- It is possible for this event to take some time before inserting a row
-- in rroad_sample_set. During that time, it is also possible for sampling to be 
-- stopped, and a "sampling stopped" row to be inserted. The row inserted by this
-- event should have a lower primary key than the "sampling stopped" event, so it
-- cannot wait until the actual INSERT rroad_sample_set statement to calculate the
-- value of the autoincrement column sample_set_number. The following statement 
-- pre-assigns a value which will only be used for the INSERT of a "successful" 
-- sample set; if that INSERT is not performed because of some problem, there will
-- be a gap in the primary key sequence.

SET @candidate_sample_set_number = GET_IDENTITY ( 'rroad_sample_set' );

The second explanation, the previous "Note: ...", was added later to explain that GET_IDENTITY was being called through laziness rather than real need. This explanation isn't quite as thorough, but it does serve to prevent the new maintenance programmer from searching the code to find the reason GET_IDENTITY was called.

In a perfect world, the call to GET_IDENTITY would have been removed as soon as it became unnecessary. In an imperfect world, it's easier to add the "Note:" when the call became moot... not just because it's less work to write the comment than change the code, but also because comments aren't compiled so the "Note:" doesn't have to be tested.

Example 3

The following example almost got left out because at first glance it looked like just another bad explanation: Who needs to be told that sa_locks needs DBA authority?
------------------------------------------------------------
   -- Create proxy_sa_locks if necessary.
   -- This is only done after a full connection process.
   -- Note: DBA authority is needed to call sa_locks.

Well, with this application (Foxhound) whether the user id has DBA authority or not controls on how much detail the user's going to get, and this comment explains to the maintenance programmer that information about row locks is going to be missing from that detail if the user doesn't have DBA authority.

So, this "Note: DBA authority is needed to call sa_locks" looks like a "How?" but it's really a "What?", albeit on a grander scale: "What does the maintenance programmer need to know about this code?"

Example 4

Here is a breathtakingly-verbose "Note:" that explains a very simple-looking IF NOT EXISTS RETURN control structure:
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------
-- Step 5 - Check to see if sampling has been turned off. 
------------------------------------------------------------------------------------------------------------------------
------------------------------------------------------------------------------------------------------------------------

IF  @ok = 'Y'  
AND @connected_ok = 'Y' THEN 

   -- Note: This code is an attempt to prevent a successful sample from appearing between 
   --       successive "Sampling stopped at" and "Sampling stopped" lines in the Monitor display.
   --       This process may already be executing the code above this point when sampling is stopped by
   --       a different process, and the @sample_started_at used by this process is calculated in the
   --       next section... which is after the the "Sampling stopped at" point calculated by the other
   --       process but before the eventual "Sampling stopped" point which will be calculated by some
   --       (probably much later) process.
   -- 
   --       In the following example, the sample at Apr 22 5:12:21 PM should not have been recorded:
   --          Apr 22 5:13:35 PM   1m 14.2s    0s / .1s ...
   --          Apr 22 5:13:21 PM   1m 18.8s   -- Sampling stopped --  
   --          Apr 22 5:12:21 PM   0s    0s / 0s    1 ...
   --          Apr 22 5:12:02 PM   1m 24.2s   -- Sampling stopped at --  
   --          Apr 22 5:10:38 PM   1m 10.1s    .1s / .1s ...
   --
   --       In the following example, the sample at 5:11:23 PM should not have been recorded:
   --          5:11:32 PM   8.5s    .1s / 0s ...
   --          5:11:27 PM    ALL CLEAR - Alert # 1: Foxhound has been unable to gather samples for 1m or longer. 
   --          5:11:25 PM   4m 54.7s   -- Sampling stopped --  
   --          5:11:23 PM   0s    0s / .1s ...
   --          5:06:30 PM   0s   -- Sampling stopped at --  
   --          5:06:30 PM   39.6s   -- Foxhound stopped --  
   --          5:05:50 PM   0s   -- Foxhound stopped at --  

   IF NOT EXISTS ( SELECT *
                     FROM rroad_sampling_options
                    WHERE rroad_sampling_options.sampling_id                = @sampling_id
                      AND rroad_sampling_options.sampling_should_be_running = 'Y' ) THEN

      RETURN; -- do not record this sample

   END IF;

Here's the reason for the long explanation: The bug very hard to find, and the fix so simple that no future maintenance programmer, not even the origial author, would understand the "Why?" of the IF-NOT-EXISTS-RETURN code without some help.

In fact, without the explanation, it's not clear that the code is even correct let alone necessary. Comments don't just save time, they can prevent mistakes (like removing a piece of code because it looks wrong.)

In this case the comment itself wasn't that hard to write: the sample data was gathered as part of the diagnostic effort, and the explanation was worked out carefully and even written down as part of that same effort... it took very little extra effort to copy the explantion into the code as an aid to future developers.

The Bottom Line

Explanations belong inside the code, not locked in the fading memory of the original author or lost inside some other document no developer will ever read.

That is, if you think explanations are important.

Folks who don't think explanations are important generally don't think readability's important either. That's why most code without comments is also unreadable... not because there are no comments, but because the author cares more about abstract measurements of "elegance" or believes that somehow readability contradicts correctness.