Monday, November 22, 2010

Pushing OPENSTRING and CROSS APPLY

The word "pushing" in the title has two meanings: First, to promote (push) the use of OPENSTRING to solve real-world problems, and second, to push OPENSTRING and CROSS APPLY beyond the examples shown in the Help.

Simply put...

SELECT ... FROM OPENSTRING is like coding SELECT ... FROM LOAD TABLE, and

CROSS APPLY lets you join OPENSTRING with another table in the FROM clause.

(For more information, scroll down to the "openstring-expression" section on the FROM clause page in the Help; also see Joins resulting from apply expressions.)

Let's say you have a table that contains a tab-delimited list of field values in a single string column, and you want to split those values apart and store them in separate columns in another table.

OPENSTRING and CROSS APPLY will let you do that in a single INSERT SELECT statement.

Here's an example of the input table; each value in the line_text column contains a tab-delimited list of field values describing a single radio program: station WAAV, dates Mon-Fri, time 10am and so on:
CREATE TABLE raw_text (
   line_number BIGINT NOT NULL 
                  DEFAULT AUTOINCREMENT
                  PRIMARY KEY CLUSTERED,
   line_text   LONG VARCHAR NOT NULL DEFAULT '' );

INSERT raw_text ( line_text ) VALUES ( 
   'Play - not available now\x09\x09WAAV\x09Mon-Fri\x0910am\x092 hours\x09Leland\x09NC\x09980 AM\x09\x09' );
INSERT raw_text ( line_text ) VALUES ( 
   'Play - not available now\x09\x09KFAR\x09Mon-Fri\x0912pm\x093 hours\x09Fairbanks\x09AK\x09660 AM\x09\x09' );
INSERT raw_text ( line_text ) VALUES ( 
   'Play - not available now\x09\x09News Talk 550\x09Mon-Fri\x0910am\x092 hours\x09Gainesville\x09GA\x09550 AM\x09\x09' );
INSERT raw_text ( line_text ) VALUES ( 
   'Play - not available now\x09\x09News Radio 1390\x09Mon-Fri\x0910am\x092 hours\x09Jackson\x09TN\x091390 AM\x09\x09' );
INSERT raw_text ( line_text ) VALUES ( 
   'Play - not available now\x09\x09KENN\x09Sat\x095pm\x093 hours\x09Farmington\x09NM\x091390 AM\x09' );
COMMIT;
Here's a table designed to hold each field value in a separate column:
CREATE TABLE radio_programs (
   line_number BIGINT NOT NULL PRIMARY KEY CLUSTERED,
   station     VARCHAR ( 100 ) NULL,
   dates       VARCHAR ( 100 ) NULL,
   times       VARCHAR ( 100 ) NULL,
   duration    VARCHAR ( 100 ) NULL,
   city        VARCHAR ( 100 ) NULL,
   state       VARCHAR ( 100 ) NULL,
   frequency   VARCHAR ( 100 ) NULL,
   line_text   LONG VARCHAR NOT NULL );
Here's the INSERT SELECT that fills the radio_programs table:
INSERT radio_programs
WITH AUTO NAME
SELECT *
  FROM raw_text 
       CROSS APPLY
       OPENSTRING ( VALUE STRING ( raw_text.line_text ) )
             WITH ( TABLE radio_programs (
                          filler(),
                          filler(),
                          station,
                          dates,
                          times,
                          duration,
                          city,
                          state,
                          frequency,
                          filler() ) )
             OPTION ( DELIMITED BY '\x09' ) AS programs;
Lines 6 through 18 shows the OPENSTRING call which acts like LOAD TABLE on the string in raw_text.line_text.

The VALUE clause on line 6 specifies where the input is coming from, and the OPTION clause on line 18 tells OPENSTRING that raw_text.line_text is tab-delimited rather than the default of comma-delimited.

The WITH clause on lines 7 through 17 tells OPENSTRING what to do with each of ten fields in each input string.

The TABLE clause on line 7 tells OPENSTRING that the radio_programs table is going to be the model or template for OPENSTRING to use. The column names on lines 10 through 16 tell OPENSTRING which radio_programs columns correspond to which input fields. The filler() fields on lines 8, 9 and 17 tell OPENSTRING to skip fields 1, 2 and 10 in the input.

The AS clause on line 18 is required to give the OPENSTRING call a correlation name in the FROM clause... but that name isn't used in this example.

The CROSS APPLY join operator on line 5 makes it possible for the OPENSTRING call to include a reference to a column in the raw_text table.

Here's what the final result looks like:


Friday, November 19, 2010

The fRiDaY File

This was first published on March 16, 2006:

What's more important? Readability!

Alas, in all the comments on this posting, nobody put readability first...


What's more important?

What's more important, for your code to be readable, or for it to be right?

Sorry, nobody gets to reject the premise of the question. If you code at all, you already have an answer, it's written right into the product of your labors.

How about this: What's more important, for your code to be correct, or for it to be fast?

I know my answers, what's yours?
I mean sure, we can debate what "readability" means, but what does it mean not putting it first? Do you have so little respect for yourself that you want to make your life more difficult rather than easier?

If a piece of code is not readable, how exactly do you ensure that it is correct? Oh, right... by testing... let me know how that works out for you.

If a piece of code is not readable, how exactly do you fix it when it fails to work properly? Well, if you're me, you start by rewriting it so it's readable.

If a piece of code is not readable, how exactly do you improve it's performance? Well, if you're me... same answer.

But, apparently, that's just me...

I am alone

Wednesday, November 17, 2010

Isn't it just possible?

Here's a line that rang a bell with me:

Isn't it just possible that my inability to profit from OOP reflects a problem with OOP itself and not my own incompetence? - "Objects Never? Well, Hardly Ever!" by Mordechai Ben-Ari, Communications of the ACM, September 2010

I am not alone!

Here's another excerpt...
I am not the only one whose intuition fails when it comes to OOP. ... Again, isn't it just possible that the intuition of experienced software engineers is perfectly OK, and that it is OOP that is not intuitive and frequently even artificial?
To put those quotes in context, here are first two paragraphs from the article:
At the 2005 SIGCSE (Special Interest Group in Computer Science Education) Symposium in St. Louis, MO, a packed audience listened to the Great Objects Debate: Should we teach "objects first" or "objects later"?1 In the objects-first approach, novices are taught object-oriented programming (OOP) in their initial introduction to programming, as opposed to an objects-later approach, where novices are first introduced to procedural programming, leaving OOP to the end of the first semester or the end of the first year. Kim Bruce and Michael Kölling spoke in favor of the objects-first approach, while their opponents Stuart Reges and Eliot Koffman argued for teaching procedural programming first. One of Bruce's arguments was: since OOP is dominant in the world of software development, it should be taught early. I later contacted Bruce to ask for a warrant for the dominance of OOP, but he could not give me one, nor could any of several other experts to whom I posed the same question.

I claim that the use of OOP is not as prevalent as most people believe, that it is not as successful as its proponents claim, and, therefore, that its central place in the CS curriculum is not justified.
The full text of the article is behind a "Pay Wall" so you have join the Association for Computing Machinery or cough up some $$ to read it.

Speaking of which, I've been a member since Before Time Began (1973) and it is only recently (the past few years) when the ACM's flagship magazine "Communications" has been worth reading for practitioners of The Dark Arts (you know, programmers).

Now, it's well worth the expense. Not a single issue goes by that doesn't provoke at least one "Aha!" moment.

Aha! The Programmer's New Paradigm!

This is not an excerpt from the article, this is what the article provoked in me:
A Programmer who cares for nothing but the elegance of his code hires two consultants who promise him the finest programming paradigm whose benefits are invisible to anyone who is unfit for his position or "just hopelessly stupid". The Programmer cannot see the benefits himself, but pretends that he can for fear of appearing unfit for his position or stupid; his colleagues do the same. When the consultants report that the paradigm is ready, they show him how it works and the Programmer starts developing applications with it. A young intern on the team calls out that the paradigm has no benefits at all and the cry is taken up by others. The Programmer cringes, suspecting the assertion is true, but holds himself up proudly and continues along the same path. - with apologies to Hans Christian Andersen



One more excerpt, not from me, not from the article, but from a comment on the article...
"... in a design oriented field such as ours, fads are all to easy to hatch. It takes considerable will to resist fads and stay focused on the real issues." — Alan Kay, September 11, 2010

Who's Alan Kay, you ask?

Oh, nobody important, just some random guy on the internet :)

Monday, November 15, 2010

Crosstab, Rotate, Pivot, Normalize

The "Crosstab, Rotate, Pivot" article from earlier this year should have been named "Crosstab, Rotate, Pivot, Denormalize" because it showed how to take a perfectly nice table like this

and muck it up to look like this
without hard-coding any of the data values in the SQL.

What about going the other way?

What if you have a fat squat denormalized table with a bazillion columns
and you want to turn it into a tall skinny table with a bazillion rows?
And then (for bonus points) what if you wanted to keep rotating to get a different fat squat version like this?
First things first; here's the first fat squat table:

--------------------------------------------------------------------------------
-- Step 1: Initialize data.

CREATE TABLE t1 (
c1 VARCHAR ( 10 ) NOT NULL,
Ford_count INTEGER NOT NULL,
Hyundai_count INTEGER NOT NULL,
Honda_count INTEGER NOT NULL,
Chevrolet_count INTEGER NOT NULL,
PRIMARY KEY ( c1 ) );

INSERT t1 VALUES ( 'AZ', 5000, 5000, 1000, 3000 );
INSERT t1 VALUES ( 'CA', 1000, 2000, 9000, 7000 );
INSERT t1 VALUES ( 'FL', 9000, 7000, 2000, 1000 );
INSERT t1 VALUES ( 'MA', 2000, 6000, 5000, 3000 );
INSERT t1 VALUES ( 'NY', 4000, 5000, 1000, 6000 );
COMMIT;
Here's the tall skinny table:

CREATE TABLE t1_normalized (
c1 VARCHAR ( 10 ) NOT NULL,
c2 VARCHAR ( 128 ) NOT NULL,
c3 INTEGER NOT NULL,
PRIMARY KEY ( c1, c2 ) );
Here's the code that fills the tall skinny table (Step 2) and then rotates it again to create the second fat squat table (Step 3):

BEGIN

DECLARE @sql LONG VARCHAR;

--------------------------------------------------------------------------------
-- Step 2: Normalize table.

SELECT STRING (
'INSERT t1_normalized \x0d\x0a',
LIST (
STRING (
'SELECT c1, \x0d\x0a',
' ''', SYSCOLUMN.column_name, ''',\x0d\x0a',
' ', SYSCOLUMN.column_name, '\x0d\x0a',
' FROM ', SYSTABLE.table_name ),
'\x0d\x0aUNION ALL \x0d\x0a'
ORDER BY SYSCOLUMN.column_id ) )
INTO @sql
FROM SYSCOLUMN
INNER JOIN SYSTABLE
ON SYSTABLE.table_id = SYSCOLUMN.table_id
WHERE SYSTABLE.table_name = 't1'
AND SYSCOLUMN.column_name LIKE '%?_count' ESCAPE '?';

SELECT @sql; -- for display

EXECUTE IMMEDIATE @sql;

--------------------------------------------------------------------------------
-- Step 3: Pivot c1 values into columns.

SELECT STRING (
'SELECT c2',
LIST (
STRING (
',\x0d\x0a SUM ( ( IF t1_normalized.c1 = ''',
t1_distinct.c1,
''' THEN 1 ELSE 0 ENDIF ) * t1_normalized.c3 ) AS "',
t1_distinct.c1,
'"' ),
''
ORDER BY t1_distinct.c1 ),
'\x0d\x0a INTO #t1_pivot',
'\x0d\x0a FROM t1_normalized',
'\x0d\x0a GROUP BY t1_normalized.c2' )
INTO @sql
FROM ( SELECT DISTINCT c1
FROM t1_normalized ) AS t1_distinct;

SELECT @sql; -- for display

EXECUTE IMMEDIATE @sql;

SELECT * FROM t1 ORDER BY c1; -- original data, for checking
SELECT * FROM t1_normalized ORDER BY c1, c2; -- normalized data, for checking
SELECT * FROM #t1_pivot ORDER BY c2; -- pivot table

END;
Step 2 is the interesting part: it builds a SQL statement dynamically, at runtime, and then uses the magic EXECUTE IMMEDIATE feature to execute that statement. EXECUTE IMMEDIATE isn't a new feature, it's been around forever, and although the code in this article has only been tested on SQL Anywhere 11.0.1 and 12.0.0 I'm pretty sure it works on older versions like 9 and 10, maybe even earlier.

The code on lines 19 through 23 looks in the SYSTABLE and SYSCOLUMN system tables for all the "_count" columns in table "t1". (Yes, this code depends on the being able to identify the columns of interest by how they are named: see the LIKE predicate on line 23.)
08.SELECT STRING (
09. 'INSERT t1_normalized \x0d\x0a',
10. LIST (
11. STRING (
12. 'SELECT c1, \x0d\x0a',
13. ' ''', SYSCOLUMN.column_name, ''',\x0d\x0a',
14. ' ', SYSCOLUMN.column_name, '\x0d\x0a',
15. ' FROM ', SYSTABLE.table_name ),
16. '\x0d\x0aUNION ALL \x0d\x0a'
17. ORDER BY SYSCOLUMN.column_id ) )
18. INTO @sql
19. FROM SYSCOLUMN
20. INNER JOIN SYSTABLE
21. ON SYSTABLE.table_id = SYSCOLUMN.table_id
22. WHERE SYSTABLE.table_name = 't1'
23. AND SYSCOLUMN.column_name LIKE '%?_count' ESCAPE '?';
24.
25.SELECT @sql; -- for display
26.
27.EXECUTE IMMEDIATE @sql;
The code on lines 8 through 18 builds a SQL statement in a string, and puts that string in the local LONG VARCHAR variable @sql.

The value placed in @sql consists of an 'INSERT...' (line 9) followed by a LIST (line 10) of 'SELECT...' statements (lines 12 through 15) separated by the 'UNION ALL' operator (line 16). The STRING() and LIST() functions are magic too, just like EXECUTE IMMEDIATE... all well worth learning.

The LIST - ORDER BY on line 17 doesn't affect the final outcome, but it is nice to have the generated SELECTs appear in the same order as the columns appear in the original fat squat table.

Confused yet?

Maybe it will help to see what gets put in @sql; an INSERT statement which creates one row in t1_normalized for every column in t1:

INSERT t1_normalized
SELECT c1,
'Ford_count',
Ford_count
FROM t1
UNION ALL
SELECT c1,
'Hyundai_count',
Hyundai_count
FROM t1
UNION ALL
SELECT c1,
'Honda_count',
Honda_count
FROM t1
UNION ALL
SELECT c1,
'Chevrolet_count',
Chevrolet_count
FROM t1

Here are the tables again; first, the fat squat input "FROM t1":
...and the tall skinny output "INSERT t1_normalized":
Oh, by the way... the code in Step 3 uses pretty much the same technique shown in Crosstab, Rotate, Pivot to produce the rotated-again fat squat table:

Does it work in the real world?

If you have the impression I sit around all day coding "SELECT FROM t1", let me set the record straight: I own and maintain some of the nastiest real-world code that's ever been created. And it's in that real world where the real problems arise, like the need to normalize and denormalize data to make it possible for the inevitable Queries From Hell to run in finite time. Necessity is the mother of Invention (although I do have to admit, it does sound pompous to use the word "invention" in a sentence :)

OK, so here's some real code, warts and all, the horribly denormalized table that supports the "Peaks since" line in the Foxhound database monitor:



Truly fat, truly squat:

CREATE TABLE rroad_peaks (

-- *****************************************************************
-- ***** THIS TABLE IS A CANDIDATE FOR POST-SETUP DATA UPGRADE *****
-- *****************************************************************

sampling_id UNSIGNED INTEGER NOT NULL,
peaks_calculated_to_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peaks_calculated_after TIMESTAMP NOT NULL DEFAULT CURRENT TIMESTAMP,
peaks_calculated_after_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
earliest_preserved_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_sample_elapsed_msec BIGINT NOT NULL DEFAULT 0,
peak_canarian_query_elapsed_msec BIGINT NOT NULL DEFAULT 0,
peak_ActiveReq BIGINT NOT NULL DEFAULT 0,
peak_CheckpointUrgency BIGINT NOT NULL DEFAULT 0,
peak_interval_Chkpt BIGINT NOT NULL DEFAULT 0,
peak_ConnCount BIGINT NOT NULL DEFAULT 0,
peak_CurrIO BIGINT NOT NULL DEFAULT 0,
peak_DBFileFragments BIGINT NOT NULL DEFAULT 0,
peak_LockCount BIGINT NOT NULL DEFAULT 0,
peak_MultiProgrammingLevel BIGINT NOT NULL DEFAULT 0,
peak_RecoveryUrgency BIGINT NOT NULL DEFAULT 0,
peak_UnschReq BIGINT NOT NULL DEFAULT 0,
peak_executing_connection_count BIGINT NOT NULL DEFAULT 0,
peak_idle_connection_count BIGINT NOT NULL DEFAULT 0,
peak_waiting_connection_count BIGINT NOT NULL DEFAULT 0,
peak_total_blocked_connection_count BIGINT NOT NULL DEFAULT 0,
peak_total_temporary_file_bytes BIGINT NOT NULL DEFAULT 0,
peak_rate_Bytes DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_Disk DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_BytesReceived DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_BytesSent DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_CachePanics DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_CacheSatisfaction DECIMAL ( 30, 6 ) NOT NULL DEFAULT 1.0, -- a ratio, with the smallest value defined as the "peak"
peak_rate_DiskRead DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_DiskWrite DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_FullCompare DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_IndAdd DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_IndLookup DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_IndSatisfaction DECIMAL ( 30, 6 ) NOT NULL DEFAULT 1.0, -- a ratio, with the smallest value defined as the "peak"
peak_rate_LogWrite DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_cpu_percentage_string VARCHAR ( 10 ) NOT NULL DEFAULT '',
peak_cpu_percentage_number DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_QueryLowMemoryStrategy DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_Req DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_Commit DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_rate_Rlbk DECIMAL ( 30, 6 ) NOT NULL DEFAULT 0.0,
peak_sample_elapsed_msec_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_canarian_query_elapsed_msec_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_ActiveReq_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_CheckpointUrgency_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_interval_Chkpt_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_ConnCount_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_CurrIO_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_DBFileFragments_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_LockCount_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_MultiProgrammingLevel_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_RecoveryUrgency_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_UnschReq_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_executing_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_idle_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_waiting_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_total_blocked_connection_count_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_total_temporary_file_bytes_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_Bytes_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_Disk_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_BytesReceived_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_BytesSent_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_CachePanics_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_CacheSatisfaction_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_DiskRead_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_DiskWrite_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_FullCompare_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_IndAdd_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_IndLookup_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_IndSatisfaction_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_LogWrite_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_cpu_percentage_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_QueryLowMemoryStrategy_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_Req_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_Commit_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
peak_rate_Rlbk_sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY ( sampling_id ) );

Here's the code to normalize the "_sample_set_number" columns into a tall, skinny table:

BEGIN

DECLARE @sql LONG VARCHAR;

DECLARE LOCAL TEMPORARY TABLE normalized_peaks (
sampling_id UNSIGNED INTEGER NOT NULL,
column_name VARCHAR ( 128 ) NOT NULL,
sample_set_number UNSIGNED BIGINT NOT NULL DEFAULT 0,
PRIMARY KEY ( sampling_id, column_name ) );

SELECT STRING (
'INSERT normalized_peaks \x0d\x0a',
LIST (
STRING (
'SELECT sampling_id, \x0d\x0a',
' ''', SYSCOLUMN.column_name, ''',\x0d\x0a',
' ', SYSCOLUMN.column_name, '\x0d\x0a',
' FROM ', SYSTABLE.table_name ),
'\x0d\x0aUNION ALL \x0d\x0a'
ORDER BY SYSCOLUMN.column_id ) )
INTO @sql
FROM SYSCOLUMN
INNER JOIN SYSTABLE
ON SYSTABLE.table_id = SYSCOLUMN.table_id
WHERE SYSTABLE.table_name = 'rroad_peaks'
AND SYSCOLUMN.column_name LIKE '%?_sample_set_number' ESCAPE '?';

SELECT @sql; -- for display

EXECUTE IMMEDIATE @sql;

SELECT * FROM rroad_peaks ORDER BY sampling_id;
SELECT * FROM normalized_peaks ORDER BY sampling_id, column_name;

END;

Here's what the normalized table looks like:
And just to drive the point home, here's the answer to the question "Why go to all that trouble with EXECUTE IMMEDIATE blah blah, why not just hand-code the solution?"

The answer is, the rroad_peaks table is subject to change (especially new columns) and the normalization logic is just one more piece of code that does NOT have to change along with it; i.e., this is the generated INSERT, this is the code that doesn't have to be maintained:

INSERT normalized_peaks
SELECT sampling_id,
'peaks_calculated_to_sample_set_number',
peaks_calculated_to_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peaks_calculated_after_sample_set_number',
peaks_calculated_after_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'earliest_preserved_sample_set_number',
earliest_preserved_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_sample_elapsed_msec_sample_set_number',
peak_sample_elapsed_msec_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_canarian_query_elapsed_msec_sample_set_number',
peak_canarian_query_elapsed_msec_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_ActiveReq_sample_set_number',
peak_ActiveReq_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_CheckpointUrgency_sample_set_number',
peak_CheckpointUrgency_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_interval_Chkpt_sample_set_number',
peak_interval_Chkpt_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_ConnCount_sample_set_number',
peak_ConnCount_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_CurrIO_sample_set_number',
peak_CurrIO_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_DBFileFragments_sample_set_number',
peak_DBFileFragments_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_LockCount_sample_set_number',
peak_LockCount_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_MultiProgrammingLevel_sample_set_number',
peak_MultiProgrammingLevel_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_RecoveryUrgency_sample_set_number',
peak_RecoveryUrgency_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_UnschReq_sample_set_number',
peak_UnschReq_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_executing_connection_count_sample_set_number',
peak_executing_connection_count_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_idle_connection_count_sample_set_number',
peak_idle_connection_count_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_waiting_connection_count_sample_set_number',
peak_waiting_connection_count_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_total_blocked_connection_count_sample_set_number',
peak_total_blocked_connection_count_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_total_temporary_file_bytes_sample_set_number',
peak_total_temporary_file_bytes_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_Bytes_sample_set_number',
peak_rate_Bytes_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_Disk_sample_set_number',
peak_rate_Disk_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_BytesReceived_sample_set_number',
peak_rate_BytesReceived_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_BytesSent_sample_set_number',
peak_rate_BytesSent_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_CachePanics_sample_set_number',
peak_rate_CachePanics_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_CacheSatisfaction_sample_set_number',
peak_CacheSatisfaction_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_DiskRead_sample_set_number',
peak_rate_DiskRead_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_DiskWrite_sample_set_number',
peak_rate_DiskWrite_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_FullCompare_sample_set_number',
peak_rate_FullCompare_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_IndAdd_sample_set_number',
peak_rate_IndAdd_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_IndLookup_sample_set_number',
peak_rate_IndLookup_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_IndSatisfaction_sample_set_number',
peak_IndSatisfaction_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_LogWrite_sample_set_number',
peak_rate_LogWrite_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_cpu_percentage_sample_set_number',
peak_cpu_percentage_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_QueryLowMemoryStrategy_sample_set_number',
peak_rate_QueryLowMemoryStrategy_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_Req_sample_set_number',
peak_rate_Req_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_Commit_sample_set_number',
peak_rate_Commit_sample_set_number
FROM rroad_peaks
UNION ALL
SELECT sampling_id,
'peak_rate_Rlbk_sample_set_number',
peak_rate_Rlbk_sample_set_number
FROM rroad_peaks

Saturday, November 13, 2010

Cisco Presents Software Excellence Award To Sybase

You heard about it from me first, here, and then again here, and now it's official:


Award Recognizes Sybase’s Commitment to the Success of its Partners

DUBLIN, CALIF. - Sybase, Inc., an SAP company (NYSE: SAP) and an industry leader in enterprise and mobile software, today announced that it has received the 2010 Software Excellence Supplier Award from Cisco. This prestigious award recognizes those software suppliers who engage with Cisco to ensure a high level of customer satisfaction.

The distinction was awarded during Cisco’s 19th annual Supplier Appreciation Event, held September 29, 2010, at the Santa Clara Convention Center. The event celebrated the commitment of Cisco’s preferred suppliers to ongoing customer satisfaction and accelerated growth as demonstrated by their operational excellence.

“Sybase understands that high-quality, leading-edge technology is critical to information management, and they deliver even greater value by embracing the new business models Cisco must embrace to meet customer needs" said Prentis Wilson, vice president, Global Supplier Management, Cisco. "Ultimately, Sybase partners with us proactively to anticipate and deliver for our customers"

Cisco presented awards to suppliers in recognition of their contributions to Cisco’s success in fiscal year 2010. Cisco also reaffirmed the importance of supplier relationships as the company pursues its corporate vision to build the human network and transform the way people live, work, plan and learn.

"The Cisco Software Excellence award is a recognition of our strategy to go beyond just delivering product innovation" said Brian Vink, vice president of Data Management Products, Sybase. "At Sybase we put a lot of focus on the intangibles of an ISV partnership such as collaborating on roadmap, providing unmatched support, and being flexible in all aspects of the relationship"

More than 10 million users have benefited from applications that have been deployed by ISVs who rely on the Sybase® SQL Anywhere® embedded database server to power their business critical systems. SQL Anywhere’s ease-of-use, out-of-the-box performance, and self-management innovations address the evolving requirements of solution providers across many vertical markets. To learn more, visit www.sybase.com/sqlanywhere.
Interestingly, the press release doesn't mention SQL Anywhere by name until the last paragraph, and then doesn't connect SQL Anywhere with Cisco.

That's OK, I'm here to fix that...

Here's what the original announcement said:
Cisco currently embeds SQL Anywhere in nearly 20 products, and cited Sybase iAnywhere's efforts to ensure Cisco's success with the technology. During the presentation, Cisco praised Sybase iAnywhere's unique approach, and the technical and relationship support provided by the Sybase team.

Friday, November 12, 2010

The fRiDaY File

This was first published on August 20, 2000:



It's going to take a while to get that image out of your head, isn't it?


Some background: PowerPhoto used ASA 7 remote databases, MobiLink and an ASE 12 consolidated database.
No, I didn't work on the project personally, but a colleague did... and when I stumbled upon one of the Fun! Fantasy! kiosks in the wild, I couldn't resist.
The technology worked but... even if PowerPhoto's business model hadn't sucked (it did), the timing certainly did (remember 2000?), and in 2004 The Kiosk Factory acquired PowerPhoto's assets.

After that, it gets murky... The Kiosk Factory may or may not exist today, perhaps (or perhaps not) as Walters Interactive... the website(s) are kinda dead and there ain't no hits in Google News.

It just goes to show... sometimes a bad idea is still bad ten years later... like Breck in a leather bustier :)

How do I create a larger DOS box?

Folks laugh at me because I code in Wordpad and test in dbisql. I just lower my head and mutter under my breath, "Go ahead, laugh, I've yet to find myself on a client workstation that doesn't have all the tools I need to solve their problems."

That, and a browser... and Foxhound [obligatory promotional plug]... but I need to get...

Back on topic...


I regularly run a 30K batch file to build Foxhound from beginning to end, from dbinit through dbisql to Carbonite and InstallShield... lots and lots of commands scrolling off the screen never to be seen again.

Once in a while I notice an error message scrolling by, and I am never fast enough pressing Function + Pause to catch it before it's lost off the top.

Finally... after how many years putting up with this?... I asked Shalmaneser, I mean Google this question:
How do I create a larger DOS box?
The first hit suggested using the Properties - Layout dialog box for the DOS box, so that's what I did:
  • First, I used right-mouse - drag and drop - "Create Shortcuts Here" to make a Windows shortcut for the *.bat file, and then

  • I clicked right-mouse - Properties on the new shortcut and picked the Layout tab, and finally

  • I set the Screen buffer size - Height to the maximum 9999. I also made it wider (150) and fattened up the "Window size" to 150 by 50.

Here's the big fat DOS box in action; I still can't figure out the error message at the bottom of the window, but at least I can see it now (click on it to see full-size):

Wednesday, November 10, 2010

Everyone hates Ticketmaster, except...

The November issue of Wired magazine contains this article about startup companies trying to compete with TicketMaster:

Everyone Hates Ticketmaster — But No One Can Take It Down


Spoiler Alert!


Here's the last line, the bottom line:
"... ticketing is not about the fans or the bands. It’s about the venues. And for them, Ticketmaster works."
Why does Ticketmaster work? Because
"... the company’s system is almost supernaturally reliable."
What does that have to do with SQL Anywhere? Well, SQL Anywhere is one of the reasons no one can take down TicketMaster...
"Because the ARCHTICS® ticketing systems are deployed into client sites with small or non-existant IT staffs, Ticketmaster requires a database to power its solution that could be deployed and managed with minimal administration. However, the high demands of the application meant it was not acceptable that this would come at the expense of performance or features." - Ticketmaster SQL Anywhere success story
So, everyone hates TicketMaster, except me :)

Monday, November 8, 2010

Couldn't find sqla.stackexchange.com

UPDATE: SQLA is back up, and backed up, and no, I still don't know what happened.



I don't know what's happened, I've sent an email, all we can do is wait and see.

What's more important?

What's more important, for your code to be readable, or for it to be right?

Sorry, nobody gets to reject the premise of the question. If you code at all, you already have an answer, it's written right into the product of your labors.

How about this: What's more important, for your code to be correct, or for it to be fast?

I know my answers, what's yours?

Friday, November 5, 2010

The fRiDaY File

This was first published on March 15, 2006:

EXPRTYPE, DATEDIFF, MICROSECOND, BIGINT and Volker Barth

Some of the minor differences between Volker Barth and me are apparent in this photo (I'm the one on the right):



However, there is a much bigger, hidden difference between Volker and me: when Volker reads something, he remembers what he's read and is able to use it in his daily life.

Unlike me, apparently.

You see, Volker's read the "What's New" sections in the SQL Anywhere Version 12 Help. I know that because of his comment on this blog post about how DATEDIFF is now returning BIGINT as well as supporting the new MICROSECOND date part.

I also know that I also read all the What's New stuff in Version 12... I know that because, well, I had to, didn't I? In order to write 10 Cool New Features In SQL Anywhere 12?

Didn't I?

I'm sure I did :)

OK, That's 12, This Is Now


For those of you working with earlier versions of SQL Anywhere, and even those lucky enough to be running Version 12, the question sometimes arises, "What data type am I getting?"

Older Help files often don't tell you what data type is returned by a builtin function. For example, the Version 9.0.2 Help file doesn't tell you that DATEDIFF returns INTEGER.

Plus, you may have coded an expression and you need to know what the resulting data type is so that, for example, you're not getting an integer when you want three decimal places.

The EXPRTYPE function was introducted in Version 9.0.0 8.0.3 for just this purpose. You pass it a 'SELECT-statement-inside-single-quotes', plus a number 1, 2, 3 telling EXPRTYPE which SELECT list item you are interested in, and it returns a string containing the data type for that item.
exprtype() works with 8.0.3, though it is not documented there. FWIW, the 8.0.3 maintenance release did not have its own doc set, just a "Readme file" with the news and changes - and unfortunately, that made it newer than the official docs, and therefore even the v9-V11 docs don't list the changes from 8.0.3. – Volker Barth
Here's what EXPRTYPE says about DATEDIFF in 9.0.2 8.0.3:

SELECT ' Version:' AS "Date Part", @@Version AS "Data Type"
UNION
SELECT ' 1. YEAR', EXPRTYPE ( 'SELECT DATEDIFF ( YEAR, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 2. QUARTER', EXPRTYPE ( 'SELECT DATEDIFF ( QUARTER, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 3. MONTH', EXPRTYPE ( 'SELECT DATEDIFF ( MONTH, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 4. WEEK', EXPRTYPE ( 'SELECT DATEDIFF ( WEEK, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 5. DAY', EXPRTYPE ( 'SELECT DATEDIFF ( DAY, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 6. HOUR', EXPRTYPE ( 'SELECT DATEDIFF ( HOUR, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 7. MINUTE', EXPRTYPE ( 'SELECT DATEDIFF ( MINUTE, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 8. SECOND', EXPRTYPE ( 'SELECT DATEDIFF ( SECOND, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 9. MILLISECOND', EXPRTYPE ( 'SELECT DATEDIFF ( MILLISECOND, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
ORDER BY 1;

Date Part,Data Type
' Version:','8.0.3.5379'
' 1. YEAR','int'
' 2. QUARTER','int'
' 3. MONTH','int'
' 4. WEEK','int'
' 5. DAY','int'
' 6. HOUR','int'
' 7. MINUTE','int'
' 8. SECOND','int'
' 9. MILLISECOND','int'

Here's what it says about DATEDIFF in Version 12:

SELECT ' Version:' AS "Date Part", @@Version AS "Data Type"
UNION
SELECT ' 1. YEAR', EXPRTYPE ( 'SELECT DATEDIFF ( YEAR, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 2. QUARTER', EXPRTYPE ( 'SELECT DATEDIFF ( QUARTER, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 3. MONTH', EXPRTYPE ( 'SELECT DATEDIFF ( MONTH, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 4. WEEK', EXPRTYPE ( 'SELECT DATEDIFF ( WEEK, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 5. DAY', EXPRTYPE ( 'SELECT DATEDIFF ( DAY, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 6. HOUR', EXPRTYPE ( 'SELECT DATEDIFF ( HOUR, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 7. MINUTE', EXPRTYPE ( 'SELECT DATEDIFF ( MINUTE, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 8. SECOND', EXPRTYPE ( 'SELECT DATEDIFF ( SECOND, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT ' 9. MILLISECOND', EXPRTYPE ( 'SELECT DATEDIFF ( MILLISECOND, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
UNION
SELECT '10. MICROSECOND', EXPRTYPE ( 'SELECT DATEDIFF ( MICROSECOND, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 )
ORDER BY 1;

Date Part,Data Type
' Version:',12.0.0.2589
' 1. YEAR',int
' 2. QUARTER',int
' 3. MONTH',int
' 4. WEEK',int
' 5. DAY',int
' 6. HOUR',bigint
' 7. MINUTE',bigint
' 8. SECOND',bigint
' 9. MILLISECOND',bigint
'10. MICROSECOND',bigint

If you like EXPRTYPE, you'll love sa_describe_query().

Only in 12, though.

Wednesday, November 3, 2010

Product Suggestion: Improve ISQL - Tools - Index Consultant




Let's say you have some slow SQL that can't be run as a separate, single SQL statement but can only live inside a BEGIN block... let's say it's part of a stored procedure or web service or some such.

Furthermore, let's say you really really really really really need some help... you've followed the 64 Easy Steps described here and you've looked at graphical plans until your head hurts, and you still don't know what to do.

And then, let's say, you have an epiphany: "The Index Consultant will tell me what to do!"

So you grab your code and paste it into ISQL and fiddle with it until it runs... slowly, of course, because that's your problem... and then you press Tools - Index Consultant.

You've done that before, and it's worked, and the Index Consultant is WONDERFUL!

But not this time...




Instead of two clicks (Tools - Index Consultant), it wants you to use the Application Profiling feature in Sybase Central. I'm not sure how many clicks that would be (fifty? 64? five hundred?) because I haven't been able to get it to work. It either comes up empty or Sybase Central crashes.

Which brings me to this suggestion: ISQL - Tools - Index Consultant SHOULD WORK on batches as well as single statements.

Yes, I know, lots of folks can get Application Profiling to work for them. But (I'm guessing here) there's a larger group of people who have just given up. I'm part of that group, in fact I'm a serial surrenderer... every six months or a year I make a serious attempt to use Application Profiling, using Version 10, Version 11, Version 12, and every time it's the same.

I used to shout at the monitor, "Has nobody ever heard of usability testing?"

Now I just relax, sippa cuppa, get on with other things.

Dilbert.com

Monday, November 1, 2010

Database Tracing In English

The interweb has finally run out of storage space for images. Or, I've run out of patience pressing Alt + Print Screen.

Either way, this article is...

  • a text-only description

  • of how to get the query plan

  • in SQL Anywhere Version 11.0.1.2276

  • for a SQL statement

  • that lives inside a block of code...
...like an application, or a stored procedure, or a trigger, or something as simple as a batch in ISQL containing a cursor fetch loop.

It's not easy. It should be easy, because ISQL has a wonderful "Plan Viewer", but it's not; here's why:

There... is... no... plan...


For example, if you have a BEGIN block containing a FOR loop that runs just fine in ISQL, albeit slowly, when you try to run Tools - Plan Viewer on the whole block you get this message:
There is no plan for the SQL statement.
[Sybase][ODBC Driver][SQL Anywhere]Syntax error near 'DECLARE' on line 2
If, in foolish desperation, you highlight the FOR loop and try to run the Plan Viewer on that, you'll see a message like this:
There is no plan for the SQL statement.
[Sybase][ODBC Driver][SQL Anywhere]Plan can not be generated for this type of statement

Who knew?


But wait! There is a way! It's called Database Tracing! and it comes in the box with SQL Anywhere! (take THAT, evil Borg!)
Note: If I spoke German (I don't) this article could have been "Database Tracing In German". The specific language isn't important except for the fact it is NOT "Database Tracing In Hieroglyphs". In other words, all text, no pictures. "A picture is worth a thousand words" means words are necessary to explain what you are looking at. Well, except for pronography art :)

Database Tracing In 64 Easy Steps


There's only 55 or 60 steps, some of these are [ahem] editorial comment:
  1. Start Sybase Central.

  2. Connect to your database. Don't forget to do that. If you forget, your whole experience will be an empty one.

  3. See the "Mode" item appear in the menu bar. Q: Why wasn't it there before, disabled but visible? A: Bad GUI design.

  4. Pay NO ATTENTION to the Mode item, it just leads to the Application Profiling Wizard which in turn leads to unhappiness.

  5. Instead, right-click on the grey "oil drum" icon that represents your database in the left pane, and click on "Tracing..."

  6. See the "Database Tracing Wizard" window appear.

  7. ...or maybe not. Everything looks different the second time you do this. Q: Why is this stuff so hard to explain? A: Bad GUI design.

  8. Let's assume this is your first time, life's too short to describe every branch of a tree that has a thousand forks.

  9. Click on Next.

  10. See the "Tracing Detail Level" window appear.

  11. Check "High detail".

  12. Click on Next.

  13. See the "Edit Tracing Levels" window appear.

  14. Uncheck everything... really, uncheck all 7 items, one by one, click click click, they're useless.

  15. Click on New.

  16. See the "Add Tracing Level" dialog box appear.

  17. Choose these values:
    Scope: database
    Tracing type: plans_with_statistics
    Condition: none

  18. Take a moment to wonder why, if those are the default values for a "New" entry, why wasn't that entry already there? A: Bad GUI design.

  19. At this point, if you listen carefully, you may hear a voice say "Don't do that! It's too expensive!"

  20. Disregard the voice, click on Add.

  21. Click on Next.

  22. See the "Create External Database" window appear.

  23. Check "Do not create a new database".

  24. Click on Next.

  25. See the "Start Tracing" window appear.

  26. Check "Save tracing in this database".

  27. Check "No limit".

  28. Click on Finish.

  29. Wait a bit... it's either going to work... or it's not (see next section).

  30. If it does work...

  31. See the Sybase Central display appear as if nothing was happening... but it is, behind the scenes.

  32. To confirm that, look for this message in your database's console log...
    Diagnostic tracing is being sent to 'links=tcpip{host=127.0.0.1};ENG=sss;DBN=ddd;Encryption=SIMPLE'

  33. Switch over to ISQL, or your application, whatever, and run your test(s).

  34. When you're done running your test(s), switch back to Sybase Central.

  35. Right-click on the database icon in the left pane, and click on Tracing - Stop tracing with save.

  36. See the "Stop Tracing With Save" progress window appear and disappear... cool horizontal barber pole!

  37. NOW, go back and click on Mode - Application Profiling on the menu bar.

  38. ...that's "Mode" and then "Application Profiling", not "Application Profiling". Q: Why does a sub-menu item have the same name as a menu item? A: Bad GUI design.

  39. See the "Application Profiling Wizard" window appear.

  40. Click on Cancel.

  41. See the pretty blue "Application Profiling Details" pane appear at the bottom of the Sybase Central window.

  42. Click on the line of text "Open Analysis File or Connect to a Tracing Database". Yes, it's a link... you didn't know that, did you? A: Bad GUI design.

  43. See the "Open Analysis Or Connect To Tracing Database" dialog box appear.

  44. Check "In a tracing database"... even though you don't think you have a tracing database, you do... it's YOUR database.

  45. Click on Open.

  46. See the "Connect to a Tracing Database" dialog box open.

  47. Fill in these fields to connect to YOUR database...
    Identification tab - User ID: dba
    Identification tab - Password: xxx
    Database tab - Server name: sss
    Database tab - Database name: ddd

  48. Click on OK.

  49. See the "Tracing Database" display appear in the "Application Profiling Details" pane.

  50. Don't touch the buttons! Look down, wayyyy down, see the line of upside-down tabs? You didn't notice them at all, did you? A: Bad GUI design.

  51. Click on the "Database Tracing Data" tab.

  52. See a second line of upside-down tabs appear, with the "Summary" tab showing.

  53. Find the query you're interested in... not the BEGIN block line, but SELECT you coded in the FOR statement.

  54. On that query line, right-click "Show the Detailed SQL Statements for the Selected Summary SQL Statement"... yes, we have no verbosity.

  55. See the display switch to the "Details" tab.

  56. Find the query you're interested in... again...

  57. Take a moment to remember the "LogExpensiveQueries" feature in 9.0.2, and how you thought that was complicated.

  58. Right-click "View More SQL Statement Details for the Selected Statement".

  59. See the "SQL Statement Details" dialog box appear.

  60. See the line of upside-down tabs at the bottom of the dialog box. You STILL didn't notice them appear? Yeah, neither did I. We're just dumb, I guess.

  61. Click on the "Query Information" tab.

  62. See the plan! Woohoo!

  63. It's just like the ISQL Plan Viewer except crappier.

  64. Why "crappier"? Try saving the plan to a file, you'll see. Same thing in 12, thanks for asking.
But... you do get to see the query plan! So that's good news.

ATTACH... TRACING... could... not... connect...


Here is Database Tracing's most popular common error message:
An error has occurred - tracing was not attached to the database.
ATTACH TRACING could not connect to the tracing database
[Sybase][ODBC Driver][SQL Anywhere]ATTACH TRACING could not connect to the tracing database
SQLCODE: -1097
SQLSTATE: HY000
SQL Statement: ATTACH TRACING TO LOCAL DATABASE
You can get that error...
  • if you specify the dbsrv11 -x none command line option (TCPIP is required even for a local database), or

  • if you specify the dbsrv11 -sb 0 command line option, or

  • if you use the SQL Anywhere 12.0.0.2589 engine on a database created with 11.0.1.2276.
The latter combination (V12 Database Tracing on a V11 database) comes with a bit more excitement later on: the engine crashes on shutdown.

In every case, the "ATTACH TRACING could not connect" message gives no clue as to the real problem, and that's a real problem in itself.

Saturday, October 30, 2010

Turning Dark Clouds into Silver Linings - the slides

If you couldn't attend the conference to hear Glenn Paulley speak about data management in the cloud, you can read what he had to say here.

Of particular interest to me was this point from slide 6:

Self-management is largely all about performance
  • A significant exception: error handling
If you don't think error handling is a big deal, have a look at this this conversation about a bug a feature a bug an interesting idiosyncracy in SQL Anywhere.

Friday, October 29, 2010

The Seven Deadly Habits of an Oracle DBA

Seriously, it doesn't say "Oracle" in the title of this article, and the points it makes apply to a lot of non-Oracle shops including some using SQL Anywhere:

The Seven Deadly Habits of a DBA
But... the writer IS talking about Oracle, and Habit #5 does apply to every single large Oracle (and IBM) shop I've ever dealt with.


Habit #5. THE BLAME GAME: "Don't look at me, it's the developer's fault that SQL is in production"

Some DBAs have a real "us versus them" mentality when it comes to developers in their organization. They see themselves not as facilitators helping the developers develop quality code from a database standpoint, but rather as guardians who prevent poor-quality code from making it into production. This might seem like semantics, but a confrontational relationship between developers and DBAs results in a lack of developer initiative and significant slowdowns in release cycles.

Cures:
  • Select DBAs who understand it's their responsibility to work as an integrated team with the developers they support.

  • Cultivate a team attitude by structuring continuous DBA involvement in every project rather than at review milestones.

  • Consider assigning an individual DBA in a developer support role. If it's clearly in the job description, there's more motivation to do it well.


The "Blame Game" title is lame, the important point is the us-versus-them mentality that destroys creativity and productivity.

Wednesday, October 27, 2010

Getting a BIGINT from DATEDIFF

Did you know that DATEDIFF returns a signed INTEGER value? Not BIGINT?


SELECT EXPRTYPE ( 'SELECT DATEDIFF ( DAY, CURRENT TIMESTAMP, CURRENT TIMESTAMP + 1 )', 1 ) AS "Data Type";

Data Type
'int'

Who cares, you ask?


Well, did you know that DATEDIFF ( MILLISECOND, ... ) craps out at 25 days?

More specifically, at some point between 24 and 25 for the number of days returned by DATEDIFF ( DAY, ... ), the same call using DATEDIFF ( MILLISECOND, ... ) will blow past the limit for INTEGER.

BEGIN
DECLARE @sqlcode INTEGER;
DECLARE @sqlstate VARCHAR ( 5 );
DECLARE @errormsg VARCHAR ( 32767 );

DECLARE @ok BIGINT;
DECLARE @splat BIGINT;

SELECT DATEDIFF ( MILLISECOND,
CURRENT TIMESTAMP,
DATEADD ( DAY, 24, CURRENT TIMESTAMP ) )
INTO @ok;

BEGIN

SELECT DATEDIFF ( MILLISECOND,
CURRENT TIMESTAMP,
DATEADD ( DAY, 25, CURRENT TIMESTAMP ) )
INTO @splat;

EXCEPTION WHEN OTHERS THEN
SELECT SQLCODE, SQLSTATE, ERRORMSG()
INTO @sqlcode, @sqlstate, @errormsg;
MESSAGE STRING (
'EXCEPTION raised by "SELECT INTO @splat" at ',
CURRENT TIMESTAMP,
': SQLCODE = ', @sqlcode,
', SQLSTATE = ', @sqlstate,
', ERRORMSG() = ', @errormsg )
TO CLIENT;

END;

SELECT @ok, @splat;

END;

@ok,@splat
2073600000,(NULL)

EXCEPTION raised by "SELECT INTO @splat" at 2010-10-16 08:52:49.610: SQLCODE = -158, SQLSTATE = 22003, ERRORMSG() = Value datediff(millisecond,2010-10-16 08:52:49.610,2010-11-10 08:52:49.610) out of range for destination
You can probably figure out the exact "splat!" point between 24 and 25 days using DATEADD ( HOUR, ... ) or MINUTE or even SECOND. Note, however, that DATEDIFF ( SECOND, ... ) and MINUTE have their own splat! points, and the effect of multiple splat! points on code verbosity will soon become apparent.

Who cares? I do!


Or at least, the Foxhound Database Monitor cares... Foxhound lives and dies on the calculation of elapsed times between two arbitrary timestamps. Tiny values need to be reasonably precise, so the calculations are done in milliseconds. Huge intervals (days, weeks, years) must to be accomodated so BIGINT is used. And to reduce code complexity BIGINT milliseconds are used throughout (there's a bunch of code devoted to formatting intervals for display, and sticking to milliseconds for input to that code makes it easier).
Sounds like a job for FLOAT? Hah! ...don't talk to me about floating point numbers, they're icky sloppy things, not to be touched or handled without gloves.

Yes, I am a Data Type Bigot and proud of it. In olden days my motto was, "If it ain't greater than zero and less than 32767 I'm not interested!"

Now it's "Give me fixed point or give me death!"

Besides, DATEDIFF returns an INTEGER, and it *still* craps out at 25 days if you CAST it as FLOAT.
One workaround is to accept FLOAT-like behavior in a BIGINT value (large values are not perfectly precise), and turn failing DATEDIFF ( MILLISECOND, ... ) calls into ones that work: DATEDIFF ( SECOND, ... ) * 1000, DATEDIFF ( MINUTE, ... ) * 60 * 1000 and so on.

Here's a warts-and-all excerpt from Foxhound; the columns to look at are started_at, completed_at and run_msec:

CREATE TABLE rroad_purge_run (
run_number BIGINT NOT NULL DEFAULT AUTOINCREMENT PRIMARY KEY, -- do not INSERT or UPDATE this column
progress VARCHAR ( 100 ) NOT NULL DEFAULT 'Starting',
started_at TIMESTAMP NOT NULL DEFAULT CURRENT TIMESTAMP, -- do not INSERT or UPDATE this column
is_complete VARCHAR ( 1 ) NOT NULL DEFAULT 'N' CHECK ( @complete IN ( 'Y', 'N' ) ),
completed_at TIMESTAMP NOT NULL DEFAULT TIMESTAMP, -- do not INSERT or UPDATE this column

must_trigger_next_purge VARCHAR ( 1 ) NOT NULL DEFAULT 'N' CHECK ( must_trigger_next_purge IN ( 'Y', 'N' ) ),
sample_purge_interval VARCHAR ( 100 ) NOT NULL DEFAULT '',
uninteresting_connections_purge_interval VARCHAR ( 100 ) NOT NULL DEFAULT '',
purge_speed VARCHAR ( 100 ) NOT NULL DEFAULT '',
exception_delete_count BIGINT NOT NULL DEFAULT 0,
orphan_sample_set_delete_count BIGINT NOT NULL DEFAULT 0,
old_sample_set_delete_count BIGINT NOT NULL DEFAULT 0,
uninteresting_connections_delete_count BIGINT NOT NULL DEFAULT 0,
exception_delete_msec INTEGER NOT NULL DEFAULT 0,
orphan_sample_set_delete_msec INTEGER NOT NULL DEFAULT 0,
old_sample_set_delete_msec INTEGER NOT NULL DEFAULT 0,
uninteresting_connections_delete_msec INTEGER NOT NULL DEFAULT 0,

run_msec BIGINT NOT NULL COMPUTE (
CASE
WHEN ABS ( DATEDIFF ( YEAR, started_at, completed_at ) ) >= 4083
THEN CAST ( DATEDIFF ( HOUR, started_at, completed_at ) AS BIGINT ) * 60 * 60 * 1000

WHEN ABS ( DATEDIFF ( YEAR, started_at, completed_at ) ) >= 68
THEN CAST ( DATEDIFF ( MINUTE, started_at, completed_at ) AS BIGINT ) * 60 * 1000

WHEN ABS ( DATEDIFF ( DAY, started_at, completed_at ) ) >= 24
THEN CAST ( DATEDIFF ( SECOND, started_at, completed_at ) AS BIGINT ) * 1000

ELSE DATEDIFF ( MILLISECOND, started_at, completed_at )
END ) );
The table rroad_purge_run is used for monitoring Foxhound itself, in particular the internal database purge process.

The started_at column is initialized by DEFAULT CURRENT TIMESTAMP when the row is inserted, and completed_at is updated by the DEFAULT TIMESTAMP clause every time the row is updated. The completed_at column doesn't contain a real "completed at" value until the purge process is finished... unless the process crashes along the way, in which case it's pretty close.

The run_msec COMPUTE clause shows how various DATEDIFF splat! points are avoided by making some conservative tests: If the difference is 4083 years or more use DATEDIFF ( HOUR, ... ), else if the difference is 68 years or more use MINUTE, else so on.

Here is a simulation showing how the COMPUTE works for small intervals. After the first UPDATE run_msec is 593 and after the second run_msec = 1140:

INSERT rroad_purge_run ( run_number ) VALUES ( DEFAULT ); -- work begins

WAITFOR DELAY '00:00:00.5'; -- some work is done

UPDATE rroad_purge_run
SET exception_delete_count = 123 -- a column is changed
WHERE run_number = 1;

SELECT started_at, completed_at, run_msec
FROM rroad_purge_run;

started_at,completed_at,run_msec
'2010-10-16 08:11:37.910','2010-10-16 08:11:38.503',593

WAITFOR DELAY '00:00:00.5'; -- more work is done

UPDATE rroad_purge_run
SET orphan_sample_set_delete_count = 567 -- another column is changed
WHERE run_number = 1;

SELECT started_at, completed_at, run_msec
FROM rroad_purge_run;

started_at,completed_at,run_msec
'2010-10-16 08:11:37.910','2010-10-16 08:11:39.050',1140
Here's another simulation showing how the COMPUTE works for 23 versus 25 days; there's no EXCEPTION, but there is also a slight loss of precision (the 11 milliseconds is lost: 1987200011 versus 2160000000):

UPDATE rroad_purge_run
SET completed_at = DATEADD ( DAY, 23, DATEADD ( MILLISECOND, 11, started_at ) )
WHERE run_number = 1;

SELECT started_at, completed_at, run_msec
FROM rroad_purge_run;

started_at,completed_at,run_msec
'2010-10-16 08:11:37.910','2010-11-08 08:11:37.921',1987200011

UPDATE rroad_purge_run
SET completed_at = DATEADD ( DAY, 25, DATEADD ( MILLISECOND, 11, started_at ) )
WHERE run_number = 1;

SELECT started_at, completed_at, run_msec
FROM rroad_purge_run;

started_at,completed_at,run_msec
'2010-10-16 08:11:37.910','2010-11-10 08:11:37.921',2160000000

Monday, October 25, 2010

Sybase goes crazy hiring

Well, maybe not all of Sybase, but surely one part is hiring like the recession's over. Do this Google search, see how many hits you get:

jobs "Sybase Federal, an SAP GSS Company"
A little bird told me that the successful 2010 US Census project has been a real door-opener for SQL Anywhere and MobiLink inside the US federal government.

Saturday, October 23, 2010

Cisco appreciates SQL Anywhere

For those of you who don't read SQLA, here's an excerpt from a recent "Sybase Partner News" email (the emphasis is mine):

Sybase Wins Cisco's Annual Supplier Appreciation Award for Software Excellence

September 30, 2010 - presented during its annual Supplier Appreciation Conference, this is the first year Cisco recognized software suppliers in addition to several categories for hardware suppliers. Cisco currently embeds SQL Anywhere in nearly 20 products, and cited Sybase iAnywhere's efforts to ensure Cisco's success with the technology. During the presentation, Cisco praised Sybase iAnywhere's unique approach, and the technical and relationship support provided by the Sybase team.

Friday, October 22, 2010

Well, don't leave us hanging... is this a bug or not?

Back on October 9 I posted "What's going on here?" about the suspicious behavior of a simple SELECT with and without a WHERE clause.

The first line said "Can something this simple really be so wrong?" and the last line said "This is a bug in SQL Anywhere 11.0.1.2276, right?"

Anonymous said...
Well, don't leave us hanging... is this a bug or not?

October 14, 2010 10:53 AM
[redacted] said...
I say it's a bug. The documentation pretty clearly states that the ORDER BY clause will be respected (and is in fact more or less required) for TOP and START AT.

Is there an index on the "data" column?

October 14, 2010 8:55 PM
Well, [redacted] is not alone. I thought it was a bug, too, until someone pointed out to me...

That's how TOP works!

TOP is evaluated after the WHERE clause has done it's thing. Long after. I should know that, I used up ten pages in my book talking about the "Logical Execution of a SELECT". That section listed 15 separate steps, with the WHERE clause coming in at Step Number 3 and the TOP clause way down at Step Number 12.

Oh, and ORDER BY is back at Step Number 9.

In my defense, the actual SELECT I was working with was way more complex than the one described in "What's going on here?"... but I still wasted several hours over two days.

Still don't get it?

Don't feel bad, you haven't spent two days thinking about it like I did.

And not everyone can be Mark C. or V Barth or Phil, who all got it right, right away...
Mark C. said...
Breck: If there are five rows in the "top 10" of original query where data != 'B' then these five rows would not be included in the second query's result set and this would cause the output of the second query to be different.

October 9, 2010 11:21 AM
V Barth said...
Oh, a riddle?

I suggest there are 5 rows in ther pkey range 1-9 WHERE data = 'B' IS NOT TRUE. Therefore the rows 10-14 are still part of the 2nd select but get skipped by the START AT clause as they are now among the first 9 rows.

October 10, 2010 11:27 AM
Phil said...
What's the data for pkey 1 through 9? I'm not an expert, but if there are 5 rows with data != 'B', then I would expect this result. I would read this query as "give me rows 10 through 15 that meet these conditions" not "give me the rows that meet these conditions from rows 10 through 15."

October 11, 2010 11:45 AM
If you got fooled into thinking it was a bug in SQL Anywhere, it might be because rows 1 through 9 were left out of "What's going on here?". I did that on purpose so you could be misled just like I was.

Here's the full demo...

CREATE TABLE t (
pkey INTEGER NOT NULL DEFAULT AUTOINCREMENT PRIMARY KEY,
data VARCHAR ( 10 ) NOT NULL );

INSERT t VALUES ( 1, 'A' );
INSERT t VALUES ( 2, 'A' );
INSERT t VALUES ( 3, 'A' );
INSERT t VALUES ( 4, 'A' );
INSERT t VALUES ( 5, 'A' );
INSERT t VALUES ( 6, 'B' );
INSERT t VALUES ( 7, 'B' );
INSERT t VALUES ( 8, 'B' );
INSERT t VALUES ( 9, 'B' );
INSERT t VALUES ( 10, 'B' );
INSERT t VALUES ( 11, 'B' );
INSERT t VALUES ( 12, 'B' );
INSERT t VALUES ( 13, 'B' );
INSERT t VALUES ( 14, 'B' );
INSERT t VALUES ( 15, 'B' );
INSERT t VALUES ( 16, 'B' );
INSERT t VALUES ( 17, 'B' );
INSERT t VALUES ( 18, 'B' );
INSERT t VALUES ( 19, 'B' );
INSERT t VALUES ( 20, 'B' );
COMMIT;

SELECT TOP 5 START AT 10 *
FROM t
ORDER BY pkey;

pkey,data
10,'B'
11,'B'
12,'B'
13,'B'
14,'B'

SELECT TOP 5 START AT 10 *
FROM t
WHERE data = 'B'
ORDER BY pkey;

pkey,data
15,'B'
16,'B'
17,'B'
18,'B'
19,'B'

My new excuse? There were 22 million rows in the actual table. That's my story, and I'm sticking to it.

Wednesday, October 20, 2010

It's good to be INSENSITIVE

Here's something you may have heard from time to time:

Don't use cursors to make changes. Use set-oriented SQL statements instead.
But hey, sometimes you HAVE to write a COBOL-style program, one that uses an old-fashioned input-process-repeat loop to manipulate a result set one record, er, row at a time.

Sure, you can spend a couple of days figuring out how WINDOW works with UPDATE, or whether MERGE will work for your convoluted requirements... or you can spend a couple of hours writing a fetch loop.

If you do use a cursor, here's another slogan (this is the short form, the long form comes later):
Always make your cursors INSENSITIVE.
An insensitive cursor is a safe cursor, all other forms are spooky scary. If you like spooky scary, then fine, go ahead. But be prepared for never knowing if your code's going to work in all scenarios... well, you can never know that anyway, but with other kinds of cursors you can be pretty sure you WILL have problems, eventually.

Why is it good to be insensitive? Because an insensitive cursor is a stable cursor, it doesn't matter what goes on around it (in other connections, even the same connection), the result set is fixed when the loop starts. With other types of cursors the rules are fantastically complicated... and the rules change from release to release.

OK, you've got questions, "Always make your cursors INSENSITIVE" is really simplistic. Maybe the long form will help:
If you are going to INSERT, UPDATE or DELETE any of the tables involved in a cursor definition, either directly (your code inside the fetch loop) or indirectly (say, when your connection fires a trigger that makes such a change, or when some other connection, even an EVENT that your code fires, makes such a change), then
  • always specify both INSENSITIVE and FOR READ ONLY, and

  • never use WHERE CURRENT OF, always use UPDATE and DELETE statements with ordinary WHERE clauses.
If you end up having performance problems, then consider changing INSENSITIVE to something else. Most of the time (80%? 90%? 99%?) you won't have to, and life will be that much safer and easier.
Here's a SQL fetch loop template using the wonderful FOR loop syntax:

FOR [loop name] AS [cursor name] INSENSITIVE CURSOR FOR
SELECT t.primary_key_column_1_of_2 AS @primary_key_column_1_of_2,
t.primary_key_column_2_of_2 AS @primary_key_column_2_of_2,
t.[some other column] AS @[some other column],
t.[some other column] AS @[some other column]
FROM t
WHERE [some predicates]
ORDER BY [some columns]
FOR READ ONLY
DO
...
[references to the @variables implicitly declared in the SELECT]
...
INSERT t ( [some column names] ) VALUES ( [some values] )
...
UPDATE t
SET t.[some other column] = [some value],
t.[some other column] = [some value]
WHERE t.primary_key_column_1_of_2 = @primary_key_column_1_of_2
AND t.primary_key_column_2_of_2 = @primary_key_column_2_of_2;
...
DELETE t
WHERE t.primary_key_column_1_of_2 = @primary_key_column_1_of_2
AND t.primary_key_column_2_of_2 = @primary_key_column_2_of_2;
...
END FOR;


Question: Isn't FOR READ ONLY redundant when you code INSENSITIVE? You can't use WHERE CURRENT OF with an INSENSITIVE cursor, can you?

Answer: Once upon a time (Version 7) INSENSITIVE cursors could be updatable. Besides, the rules for cursors change all the time, who knows what INSENSITIVE with FOR UPDATE might mean in the future... maybe at runtime it will morph into a value-sensitive cursor without telling you.

If you see the following statement in the Help for Version 8 or later, just ignore it, it's wrong (and it's being fixed):
INSENSITIVE ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.
Let's beat the point to death, er, drive the point home... if you printed out all the Help files, here's how you could fix them:
Version 5 is OK: A cursor declared INSENSITIVE ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 6 is OK: INSENSITIVE cursors ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 7 is OK: INSENSITIVE cursors ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 8: INSENSITIVE ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 9: INSENSITIVE ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 10: INSENSITIVE ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 11: INSENSITIVE clause ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.

Version 12: INSENSITIVE clause ... It does see the effect of PUT, UPDATE WHERE CURRENT, DELETE WHERE CURRENT operations on the same cursor.


More stuff...
Insensitive cursors
FOR statement
DECLARE CURSOR statement
Does anyone else WANT TO KNOW how cursors really work?

Monday, October 18, 2010

Turning Dark Clouds into Silver Linings

Here is the abstract of the keynote speech Glenn Paulley will be giving at the CIKM 2010 - 19th ACM International Conference on Information and Knowledge Management in Toronto, October 26 to 30, 2010:



Turning Dark Clouds into Silver Linings

Data management services are ubiquitous in the industry. With some important exceptions, relational database systems are the platform upon which many applications depend, including mainframe servers, web browsers, and handheld devices. The vast majority of these installations run unattended. Consequently, self-management, self-tuning, and self-healing features are of great importance to these systems. Cloud computing architectures, due to their inherent dynamics, add another level of flexibility - and complexity - to the problems of database self-management.

However, application developers continue to experience unpredictable performance and reliability issues with the application's software stack. Over the past few years, these issues have led to a variety of proposals to address the problem, including weak consistency models and the abandonment of SQL as a data management sub-language.

In this talk I'll present an overview of the data management problems in relational database systems that are exacerbated by cloud computing architectures, discuss the state-of-the-art in self-management technology, and conclude with some ideas for future research to address these problems.

Speaker Information



Glenn Paulley is a Director with Sybase iAnywhere Engineering, where he manages the research and development team responsible for query processing in SQL Anywhere, Sybase's self-managing relational database server. He joined Sybase iAnywhere in 1995. During his 20-year industrial career Glenn has held previous positions at Amdahl Corporation and at a large Canadian insurance company. He holds a Ph.D in Computer Science from the University of Waterloo. His research interests include software usability, query optimization, information systems architecture, design of Management Information Systems, topics in systems analysis, interfaces to database systems, database query languages, user models, multidatabase systems, and indexing techniques.

Thursday, October 14, 2010

Using HTTPS with your SQL Anywhere-based web server

This article is based on this SQLA question-and-answer, How do I set up a TYPE RAW web service to use HTTPS? and on this not-yet-published Foxhound FAQ: How do I specify the HTTPS transport-layer security for Foxhound?



If you have built TYPE RAW web services in SQL Anywhere, or (I'm guessing) TYPE HTML etcetera, you can support HTTPS without making any changes to your SQL code.

You can do this by modifying the SQL Anywhere startup command line to specify RSA encryption and the HTTPS protocol as follows:
  • Obtain an identity certificate and the associated private key for your server.

  • Store the identity certificate file in a known location.

  • Change the -xs option to specify https on the dbsrv*.exe command line used to start your SQL Anywhere server:
    Specify the identity certificate file and private key in the -xs https identity= and identity_password= parameters.

    Note that the default port for HTTPS is 443.
Here is an example of a dbsrv11.exe command line modified to allow only HTTPS access to Foxhound data using the sample certificate "%SQLANY11%\Bin32\rsaserver.id" that comes with SQL Anywhere 11:

(Note: Only the -xs line had to be changed, all the other stuff was there before.)

"%SQLANY11%\Bin32\dbspawn.exe"^
-f^
"%SQLANY11%\Bin32\dbsrv11.exe"^
-c 50p^
-ch 75p^
-cr-^
-gk all^
-gn 120^
-n foxhound1^
-o foxhound1_debug.txt^
-oe foxhound1_debug_startup.txt^
-on 10M^
-qn^
-sb 0^
-x none^
-xd^
-xs https(identity="%SQLANY11%\Bin32\rsaserver.id";identity_password=test;port=443;maxsize=0;to=600;kto=600)^
foxhound1.db^
-n f

If you want to allow both HTTP and HTTPS access, specify both as follows:

-xs http(port=80;maxsize=0;to=600;kto=600),https(identity="%SQLANY11%\Bin32\rsaserver.id";identity_password=test;port=443;maxsize=0;to=600;kto=600)^

To read more about -xs, see Starting the database server with transport-layer security and the -xs dbeng12/dbsrv12 server option in the Help.

How come I don't have to code SECURE ON?


You can add the SECURE ON clause to every single web service if you want...

CREATE SERVICE service_name TYPE 'RAW'
AUTHORIZATION OFF USER user_name SECURE ON
AS CALL procedure_name (
:parameter1,
:parameter2 );

...but you don't have to. If you have complete administrative control over the command line used to start SQL Anywhere, then omitting the SECURE ON clause gives you more flexibility in deciding whether or not, and when, to support HTTP and/or HTTPS.

That's the case with the SQL Anywhere database that is Foxhound: it's up to each customer to decide if they want HTTP (the default) and/or HTTPS, and if they want to lock it down to use only HTTPS they must have control over the command line.

To read more about SECURE ON, see the Help on the CREATE SERVICE statement and Mark Culp's answer to this question in SQLA.

Saturday, October 9, 2010

What's going on here?

Can something this simple really be so wrong? Here's a simple query in SQL Anywhere 11.0.1.2276 that returns 5 rows, starting at row number 10, from a table t where pkey is the PRIMARY KEY column t, and that column is DEFAULT AUTOINCREMENT with values 1, 2, 3...:


SELECT TOP 5 START AT 10 *
FROM t
ORDER BY pkey;

pkey,data
10,'B'
11,'B'
12,'B'
13,'B'
14,'B'

So far so good... now supposing we add WHERE data = 'B' to the query; shouldn't the result set be the same?

But it's not! Look here:

SELECT TOP 5 START AT 10 *
FROM t
WHERE data = 'B'
ORDER BY pkey;

pkey,data
15,'B'
16,'B'
17,'B'
18,'B'
19,'B'

What's going on here? Clearly rows 10 through 14 have data = 'B', why aren't those rows showing up in the second query?

This is a bug in SQL Anywhere 11.0.1.2276, right?

For the answer see Well, don't leave us hanging... is this a bug or not?