Sybase ASE
All versions
Last updated: 25 July 2010
ASE Quiz Questions: answers 2010
 
More ASE Quiz questions: 2010 - 2009 - 2008 - 2007 - 2006 - 2005 - 2004 - 2003 - 2002 - 2001

July 2010
As you know (I'm assuming), when a client connects to the ASE server, it sets a bunch of connection attributes. These include the username and password, the servername being connected to, the network packet size to be used, and possibly many more.
For some of these connection attributes, their values are visible once connected to ASE: for example, the hostname, application name and network packet size are available in master..sysprocesses, in columns hostname, program_name and network_pktsz, respectively.
It can be useful to specify a meaningful value for the application name in your application. Not only does this allow you (as well as code in a login trigger) to recognise your application, but also some ASE features use the application name. For example, resource limits and user-defined temporary databases can be bound to applications with a specific name.

Unfortunately, not all applications set meaningful values in these attributes. If you have a third-party application which you'd like to connect to ASE using a specific network packet size, or specify a particular application name, this isn't easy since you cannot modify the compiled code of that application.
Yet, there is a way to specify custom values for such connection properties, even when you're unable to modify the application itself.

How?

Answer:
The answer to this question lies in a little-known aspect of OpenClient: the OpenClient configuration file, known as 'ocs.cfg'. This configuration file allows you to specify values for connection attributes: when an application connects to the ASE server, it first checks the ocs.cfg file to see if any of its connection attributes are specified; if so, the value from ocs.cfg will be used, overriding any value that already may have been set by the application code.

Now, if you search on your system for a file named ocs.cfg, you will likely not find one. Indeed, the file does not exist by default, which is one of the reasons why this feature is not very well-known. If it exists, the ocs.cfg file is located in $SYBASE/$SYBASE_OCS/config/ocs.cfg (Unix/Linux) or %SYBASE%\%SYBASE_OCS%\ini\ocs.cfg (Windows). Note that this is on the system where the client application is located.
As I said, by default the ocs.cfg file does not exist. When you look in $SYBASE/$SYBASE_OCS/config or %SYBASE%\%SYBASE_OCS%\ini you'll find a file name sample.cfg which is the template for ocs.cfg. If you want to use ocs.cfg, copy sample.cfg and edit as needed.

The first thing you'll notice when you have copied sample.cfg to ocs.cfg, is that isql, probably as well as most other client applications, will suddenly fail to start with an error message like this:
CS-LIBRARY error:
        comn_get_cfg: user api layer: internal common library error: Configuration 
        section isql not found.
This error occurs because when an application sees that ocs.cfg exists, it looks for a section in the file specifically for this application. In the case of isql, it looks for a section marked [isql] in ocs.cfg.
In general, an application looks for a section [xyz] when its application name attribute has been set to xyz by the application. If no application name has been set by the application itself, it will use the section marked [DEFAULT].

So, for isql, try adding the following lines to your ocs.cfg (put these below the [DEFAULT] section):
[isql] 
	CS_APPNAME=testappname
	CS_HOSTNAME=testhost
Now try to connect again with isql, and then run the following query. You should see the hostname and application name that you specified in ocs.cfg:
1> select program_name, hostname from sysprocesses where spid=@@spid
2> go
 program_name                   hostname
 ------------------------------ ------------------------------
 testappname                    testhost

(1 row affected)
If you'd want to define a particular network packet size for your application (say, 4096 bytes), you'd add a line CS_PACKETSIZE=4096.

Go here for a list of connection properties. Note that most of the properties listed here do not apply to regular applications, or are "for retrieval only" which means they cannot be set in the ocs.cfg file.

Obviously, when using ocs.cfg, there are some downsides. First, all applications using the same $SYBASE/$SYBASE_OCS environment will need to have a section in ocs.cfg file or they will be unable to connect.
Second, the ocs.cfg effectively becomes part of your application environment, so you'd need put it under version control etc.

Despite the downsides, I wish I had known about ocs.cfg earlier. I recall a consulting job back in the 90's where we were battling with a 3rd-party telco application whose vendor was not very cooperative. Using ocs.cfg would have solved some of my problems, had I known about it.

Just as with the quiz question for May 2010 (see below), this makes me realise OpenClient is an essential part of our infrastructure, yet we're often unaware of its role or its capabilities.

How would you rate this ASE quiz question?
Great    OK    Average    Boring     


May 2010
Here is an issue I stumbled over last week.
As you probably know, ASE 15 has increased the maximum length for many identifiers from 30 to 255 bytes. This applies to names of tables, column, stored procedures etc.
I was writing a small piece of SQL to run a particular test, and as it happened, the length of a name of one of the tables exceeded 30 bytes.
The reason I noticed the identifier was longer than 30 bytes was that this error message was raised:
1> create table this_is_a_kinda_long_table_name (a int)
2> go
Msg 103, Level 15, State 205
Server 'SYB155', Line 1
The identifier that starts with 'this_is_a_kinda_long_table_nam' is too long. 
Maximum length is 30.
Now that's strange!
I double-checked I wasn't accidentally connected to a 12.x server (and I wasn't, this was ASE 15.5). But if this was ASE 15.x, then a 31-character table name should be allowed....

What's going on here?

Answer:
First, I checked the documentation -- and indeed, ASE 15.0 and 15.5 do indeed allow 255 bytes for most types of identifiers, as it says here.
So at least I got that part right. But why then, was ASE unhappy with my 31-character table name?

The first thing to look at when seeing funny behaviour is your non-default configuration parameter settings (sp_configure nondefault is terribly useful here). I noticed that 'compatibility mode' was enabled in my server, and since that's a 12.5-compatibility feature, I reckoned that might have something to do with it. Unfortunately, disabling it made no difference at all, and the error still occurred.
I then simplified my SQL code to reduce it to the simplest reproduction, which turned out to be the create table statement shown above (the problem originally occurred in a select-into statement inside a stored procedure).

But now what? I scratched my head for a while, and I couldn't really find anything that looked wrong or that would explain why I was getting this error. So, frankly, I had no clue what it was that I was doing wrong -- since that was pretty much clear: somehow I had messed up, but if only I knew what....

Without a better option available, I decided to swallow my pride and ask my collegues what I was missing. Within 4 minutes, the answer landed in my mailbox (thanks to Sean Kiely and Bret Halford who responded at the same time). Their advice: check the version of the client you're running. If the client is 12.5, that explains why the identifier is too long.
Hmmm... that sounded like a plan -- especially because this possibility had not crossed my mind. Without understanding how the client version could make a difference, I followed their advice. And indeed: I was running the 12.5 version of isql. When running the same create table statement with isql 15.0, it worked fine, without error, as it should.

With my immediate problem solved, there was just one small but nagging question left in my mind: why does it make a difference if you use isql 12.5 or isql 15.0? This just didn't seem to make any sense to me. After all, isql only sends some text to the ASE server and fetches the results, right?
Wrong. As another collegue reminded me (thanks, Dave Putz), isql actually does a lot more, but normally that's all transparent and you'll never notice unless you hit some special combination of circumstances (like character set conversion -- see this quiz question for March 2010).

Now, when an OpenClient application connects to ASE, one of the things happening invisibly to the application is that the client-side OpenClient libraries and the ASE server exchange information on their capabilities.
One aspect of this is whether the client is able to handle 255-byte identifiers: OpenClient 15.x will indicate to an ASE 15.x server that it can handle such long identifiers, but obviously OpenClient 12.5 will not signal having this capability, since that did yet not exist in the days of 12.5. Consequently, when an OpenClient 12.5 application connects to an ASE 15.x server, the server knows that this client will be unable to handle identifier names longer than 30 bytes.
When ASE knows that the client cannot handle large identifiers, it will disallow the use of longer-than-30-bytes identifiers during the session for that client, so as to avoid messy failures during client-server communication.

Why does it matter whether the client is be able to handle such long names? For sending SQL code to the server, which is basically just a bunch of ASCII characters to the client, indeed this capability does not matter.
But for other things, it does. Two examples are bulk copy operations (e.g. BCP) and RPCs (remote procedure calls), which both involve the notion of an object name that is handled by the OpenClient layer.

So, to summarise... there are more things happening between client and server, Horatio, than are dreamt of in your philosophy...

How would you rate this ASE quiz question?
Great    OK    Average    Boring     


April 2010
Here's one from the Crazy Tricks Department: We're always about making SQL queries run faster -- but what about the opposite? What about making a query run slower?
So, let's just say you want to slow down a query against a particular table; how would you achieve that?
Apart from the technical aspect, here's question #2: what *valid* reason (i.e. not for just messing up systems) could you have for wanting to slow down, rather than speed up, a query?

Answer:
Note: I've added some updates to this answer, indicated by .

When you have a system where all queries are encapsulated in stored procedures, you could simply add a waitfor delay statement into the stored procedure. But that's too trivial for what we're after here -- we want a way to make a query against a particular table run slower, irrespective where the query comes from.

This can indeed be achieved, provided you're running ASE 15.0.2 or later. The first part of the trick is to add a computed column (CPC) to a table; the second part is to put a SQL user-defined function (UDF) in that CPC; the third part is to put a waitfor delay statement inside that UDF.
Here's an example:
use my_db
go
create function f_slow
  returns int 
as 
   waitfor delay '00:00:00.1'
   return 0
go
create table t (a int, b as my_db.dbo.f_slow() materialized)
go
When inserting into this table, the value of CPC 'b' needs to be computed, since it is a materialized CPC (meaning the computed value is actually stored on disk). Computing the CPC's value requires executing the UDF f_slow(), which means a 0.1-second delay happens -- or whatever long or short interval was specified in the UDF.
Of course, this could also have been achieved by putting a delay in an insert trigger. But I'm using this example as the introduction to the real trick, which applies to selecting from the table, where no triggers apply.

So let's do the same, but now with a virtual CPC (whose value is calculated when queried, and not stored on disk) instead of a materialized CPC as above:
use my_db
go
create function f_slow  
  returns int 
as 
   waitfor delay '00:00:00.1'
   return 0
go
create table t (a int, b as my_db.dbo.f_slow())
go
When querying column 'b', the UDF will be evaluated for every row accessed with a select statement (or any other statement referring to that column). Which means that a 0.1-second delay will kick for every row accessed. Note that inserts will not be slowed down since the value of a virtual CPC is only calculated when it is queried (nothing stops you from putting both a virtual CPC and a materialized CPC in a table BTW).
Obviously, this trick only works when column 'b' is referenced in the query, so it must be in the select list or somewhere in a predicate. When column 'b' is in the select list, the UDF will be evaluated for every qualifying row; when the column is in a predicate, the UDF will be evaluated for every row being scanned, basically (in reality it can be a little less obvious when the UDF is evaluated, especially when subqueries are involved; and when the column is involved in a join predicate, the UDF may be evaluated more than once, depending on the join type and join order, among other things. But this summary is good enough to understand the big picture).

If your queries don't refer to column 'b' but only to column 'a' the delay will not kick in. However, you could use additional tricks like putting a view on top of the table and effectively swapping columns 'a' and 'b' by defining the value of CPC 'b' to be identical to 'a':
use my_db
go
create function f_slow (@i int)
  returns int 
as 
   waitfor delay '00:00:00.1'
   return @i
go
create table t (a int, b as my_db.dbo.f_slow(a))
go
create view v (a,b)
as
   select b, a from t
go
In addition, you could rename the view to the name of the table (after renaming the table itself). Any queries that would perform updates will likely fail on the non-updatable view, but that can be worked around with Instead-Of Triggers (a separate topic, indeed).

Anyway, when using this CPC-with-UDF-doing-waiting trick, you'll see the session be in alarm sleep status in sp_who during the waitfor delay. Also, you may need to increase the configuration parameter setting for number of alarms if this ends up being used by many concurrent users.



As for the second part of the question: why would you deliberately want to slow a query down?
Actually, doing something like this is not completely outlandish, though it is admittedly a bit exotic (or sick, depending on your viewpoint).
I can see this potentially being useful in what I would call 'expectation management'. Imagine you're building a system which is designed to run with a database of a large size, but initially the database size is still small. Consequently, initially the application may run very fast due to the database being small, but you know it will get slower when the database has reached its expected full size. This will inevitably annoy the end-users of the system, who don't like to experience a slowdown ("the system was nice and fast when it was new, but it's so much slower now"). Of course, the initial performance was unrealistically good and the users should not have complained when things reached their eventual 'normal' state -- but psychologically, humans just don't see things that way.
To protect yourself against such sentiments, it might be perfectly justifiable to build an artificial slowdown into the system, and gradually reduce the amount of slowdown as the database grows in size.
Obviously, there is a thin ethical line here -- this approach could just as well be used to make the initially released system to perform *so* slowly that additional contracts for more consulting/implementation/tuning work could be obtained. I'm clearly not suggesting that line of thought should be followed.

Another potential application could be in Quality of Service (QoS): perhaps there are some users for whom you want to provide a distinctly less-performing system than for others (and perhaps offer to upgrade those users to a better-performing offering for just a modest monthly fee). Since you can test for the user of the current session inside the UDF, there's no reason why you couldn't invoke a longer or shorter wait for particular users only.

So for those who really tune in to this approach, you could make the delay configurable. Just let the UDF read the delay from a table, which you can update if you want things to slow down a bit more or less. Here's a simple example:
create table time2wait (tm varchar(10))
insert time2wait values ('00:00:02')
go

create function f_slow (@i int) 
  returns int 
as 
	declare @w varchar(10) 
	select @w = tm from time2wait 
	waitfor delay @w 
	return @i
go


There may well be other potential applications... there's no trick too crazy, or there is an application for it somewhere...

A few weeks after publishing this quiz question, I realised it's worth mentioning some additional things. For example, the approach above will show column 'b' when selecting all columns of the table, which may be undesirable since it might confuse existing applications. To work around this, you can create a function index on the same expression as defined for the CPC. Creating a function index has the effect of adding a materialized computed column to the table, but the column will be hidden, and therefore not visible in the result set.
You can do this as follows:
use my_db
go
create function f_slow (@i int)
  returns int 
as 
   waitfor delay '00:00:00.1'
   return 0
go
create table t (a int)
go
create index ix_slow on t ( my_db.dbo.f_slow(a) )
go
As is hopefully clear from the examples above, the hidden materialized CPC that gets created for the function index will slow down inserts. However it will not have any effect on selects, since that requires a virtual CPC. Note that for updates, a materialized CPC will be re-evaluated only when it references a column that's being updated. Obviously, both for updates and deletes, referencing a virtual CPC somewhere in the query will have the effect of evaluating the CPC.
It is in fact also possible to slow down updates even when you're not updating a column referenced by the CPC. I'll leave that as a challenge for the reader, and a possible quiz question at a later time.

How would you rate this ASE quiz question?
Great    OK    Average    Boring     


March 2010
I never know in advance what my next quiz question will be about, but this week the topic came to me easily. I wasted an evening trying to solve a wacky problem while getting some SQL to run on a system (a separate box) that was being prepared for demo purposes.
This turned out to be one of those cases where it's proven true that there's always something left to learn (as I mentioned last month).

The problem first surfaced when using sp_autoformat to format the contents of a table (if you don't know sp_autoformat, you should get acquainted -- try running 'sp_autoformat sysusers' as an example of what it does)
sp_autoformat is supposed to format each column as narrow as possible, given the actual data values it contains, trimming off trailing spaces from [var]char columns, among other things.
My problem was that sp_autoformat was formatting all [var]char columns with a bunch of spaces at the end, which is exactly what should NOT happen. Worse, it really messed up the formatting of some output that was supposed to fit on an 80-character window.

After checking my own SQL code was not at fault, I zoomed in on what sp_autoformat was doing (try adding the @trace=4 parameter). It seemed sp_autoformat was doing everything right, yet every column was still formatted longer than the longest value it contained. Weird.

Eventually I could simplify the problem and reproduce it without sp_autoformat, as follows:
1> select substring('abc', 1, 3)
2> go

 ------
 abc

(1 row affected)
Can you spot the problem? I'm doing a substring() whose result should be 3 characters long, but the resulting column is in fact 6 characters.
This seemed to defy all logic. What on earth is going on here?

Answer:
I was really stuck with this issue, and I didn't see a way out. I had already tried whether it was specific for a particular ASE version. The problem seemed to occur in any ASE 15 version, at least 15.0.3 and 15.5. However, when I ran the same SQL on an ASE 15.x server on my own system (i.e. not the demo box), everything worked fine, and the result was 3 characters long, as expected.
I tweaked both ASE 15 servers until they were fully identical in configuration settings, and still I was stuck: on the demo system, substring('abc',1,3) still generated an incorrect 6-character result, whereas on my own system it produced a 3-character result. At this point there was *no* difference between those two ASE 15 servers, other than that they were running on different hosts.

When I tried running my SQL on a 12.5 server on the demo box, the problem did not occur and the result was 3 characters long as it should be. So was this an ASE 15-specific problem?
I searched the ASE documentation for clues, but to no avail. Then, at the point when hard liquor looked like the only direction where solutions might be found, I realised that there had been some kind of message when I connected to the 12.5 server on the demo box for testing my reproduction:
% isql -Usa -P -S ASE1254 
Msg 2401, Level 11, State 2:
Server 'ASE1254':
Character set conversion is not available between client character set 'utf8'
and server character set 'iso_1'.
No conversions will be done.
1> 
I had ignored this message, but suddenly it seemed a good idea to pay attention. What was this saying?
Apparently the client on the demo box was using utf8 as its character set, and the server was using the default of iso_1. Hmmmm.... but so what? I did not get this message when connecting to the ASE 15 servers.
Curious, I tried running the same repro on a 12.5 server on my own system. This also worked correctly (3-character result), but there was no such message after connecting.

At this point it started to dawn on me that the issue might not be on the side of the server, but somehow involve the client. Something with character sets...
I recalled my little purple booklet has a section on character set stuff, although I don't think I've ever used that section for any practical purpose. The section ('Character set & sort order') didn't provide any clues, but the last line referred to some global variables towards the back of the quickref guide. There, I found the global variable @@client_csname, which supposedly indicates the character set used by the client for the current session.
When I selected this variable on the demo box (where the problem occurs), it appeared to be utf8. On my own system, where the problem did not occur, the client was using iso_1. So when the client uses utf8, somehow the [var]char result set columns are getting longer? That still sounded odd, but at least that's something we can experment with.

So, on the demo box, I ran the following. Using -Jiso_1 forces isql to use iso_1 as its client character set:
% isql -Usa -P -S ASE155     # uses utf8
1> select substring('abc', 1, 3)
2> go

 ------
 abc

(1 row affected)
1> exit
%
%
% isql -Usa -P -S ASE155 -Jiso_1    # uses iso_1
1> select substring('abc', 1, 3)
2> go

 ---
 abc

(1 row affected)
1> exit
%
We seemed to be getting somewhere now... At this point, I could draw at least two conclusions: (i) the issue was caused by the client using utf8 and (ii) the difference between my own system and the demo box was that my own system has environment settings such that the client uses iso_1 by default, whereas on the demo box, the client uses utf8 by default (as it turned out, this was because the environment variable LANG was unset on my own system but set to 'en_US.UTF-8' on the demo box).

Some questions still remained though, such as: why does the problem not occur on the ASE 12.5 servers? And why is there no warning about character set conversion in the ASE 15 servers? And most of all: why does a client character set of utf8 produce a 6-character string, and with iso_1, a 3-character string?

After reading through the documentation on ASE character set matters, things started to become more clear. Obviously, utf8 is a Unicode character set, which can represent many more symbols than, say, ASCII or iso_1. This means that Unicode characters can occupy 2 bytes, whereas iso_1 characters occupy only 1 byte. ASE -- or rather, OpenClient-- will automatically perform character set conversion where possible, and when converting from a single-byte character set (iso_1) to a multi-byte character set (utf8), a single character in the single-byte character set to may be represented by more than one byte in a multi-byte character set. This is actually indicated by the global variable @@client_csexpansion which indicates the factor of this expansion. Indeed, it turns out that this variable contains the value 2 when the client character set is utf8, and 1 when it is iso_1.
So there we are: with a client using utf8 and the server using iso_1, ASE will expand each iso_1 character to a 2-byte utf8 character. In isql, this means that the resulting column is shown as 6 positions, even though it is technically still 3 characters of 2 bytes each. When the client uses iso_1, the result set column consists of 3 1-byte characters, shown as 3 positions by isql.
To return to my original SQL formatting problem, by forcing the client to use iso_1 (with isql -Jiso_1 or by setting LANG to blanks), or suppressing character set conversion altogether (with isql -J), all my formatting worked fine again.

At this point, I thought a single-malt scotch was indeed well-deserved. By now I had spent hours on this problem.

While being really happy with myself, I realised there might have been an even simpler reproduction. And indeed: simply running "select 'a' " would have given a 2-character result column for utf8, but only a 1-character column for iso_1 (you can test this yourself by simply forcing utf8 or iso_1 with isql -Jutf8 or isql -Jiso_1, respectively -- this is assuming that your ASE server uses iso_1; but by now I'm sure you get the idea).

As for the two remaining questions: I'm assuming the OpenClient version that comes with ASE 15 has unicode support provided by default, whereas in 12.5 it does not. This must be the reason why the warning about character set conversion occurred only on 12.5. And this is also the reason why the 12.5 servers produce the correct 3-character result: character set conversion simply didn't happen for utf8, and this was what the warning message from isql was trying to alert me to.
(I guess there must be some kind of way to make character set conversion work for utf8 in 12.5 too -- but as you can tell from all of the above, my awareness of character set aspects in ASE is pretty limited, although recently I've been learning fast).

When you think about it, there's a lot of stuff happening behind the scenes when it comes to character sets. It's one of those things you usually don't notice -- until you work on a different system which may have different environment settings. Keep this in mind when writing SQL code where the formatting of results matters -- and test with different character sets.

How would you rate this ASE quiz question?
Great    OK    Average    Boring     


February 2010
In 2010, it's 21 years ago I got involved in what turned out to be my first project involving ASE (then named Sybase SQL Server).
After so many years, I liked to think I knew what was worth knowing about that very first thing you encounter when entering into the world of ASE: the isql utility.

Turns out, I was wrong.

While doing some performance benchmarking recently, I ended up staring at some strange-looking results that just couldn't be right.
In essence, this was a comparison between a stored proc and isql both doing the same 10,000 simple selects (this was not what was I was testing, but where I ended up after digging into the results).
The stored proc looked like this:
create proc p
as
  declare @cnt int
  set @cnt = 0

  while @cnt < 10000
  begin
	  select * from my_tab
	  set @cnt = @cnt + 1
  end
go
The stored procedure was then kicked off with a straightforward isql script:
exec p
go 
The isql-based test just took the following simple script as input:
select * from my_tab
go 10000
(I've written about the 'go 10000' approach before; see the May 2005 quiz question for more).

Now, the strange thing was that, even though in both cases the exact same query was executed 10,000 times, the test with the stored procedure was about 4 times slower than than the test with the 'go 10000' script. That made no sense, since the stored proc test should be faster, or at least equal, since there's less processing overall.
Here's why: the 'go 10000' test sends the query to the server 10,000 times, while the stored proc test send the 'exec p' to the server only once. Otherwise, both tests perform the same select query 10,000 times, and produce the same 10,000 result sets.
So logically, the 'go 10000' test should be slower than the stored proc test since it has to do more client-server roundtrips. But what I was seeing is that it was actually 4 times faster than the stored proc test.

The statement cache was enabled (so there was no optimizer overhead anywhere), and I had verified that the query plans and I/O counts were identical. The table had 100 rows and 20 columns, and the data was static. There were no other users in the server.

What was going on?

Answer:
The concept of 'go N' in isql is an elegant way of executing a piece of SQL N times with minimal keyboard work. For example, it is useful to quickly generate a bunch of rows of test data (again, see here for an example).
With 'go N', isql sends the preceding query batch to the server, and receives the corresponding result sets. Repeat, until N is reached (obviously, N=1 by default).

When I couldn't figure out what was going on with these strange results, I decided to swallow my pride and ask some engineering collegues at Sybase, all of whom have been around for a long time. Interestingly enough, they all came up with different explanations (names withheld to avoid public embarrassment):
  • Collegue #1: "isql isn't sending the batch 10,000 times, it's sending it only once and it gets executed 10,000 times at the server."
  • Collegue #2: "When you use 'go 10000', only the result set of the last query is sent back to the client, so there is much less network traffic which explains why the stored proc is slower."
  • Collegue #3: "The stored procedure is slower because it is generating more DONE_IN_PROC TDS tokens, causing additional overhead."
Collegue #1 was outright wrong: I could easily tell from the MDA tables (monSysSQLText) that the query *was* sent 10,000 times. And even if he'd been right, it wouldn't explain why the stored proc was so much slower.
Collegue #2 was also wrong (though appeared to be thinking in the right direction): from the network I/O monitoring in sp_sysmon I could see roughly the same amount of data being sent back from the server to the client. The minimal difference might be explained by the theory of collegue #3, but there was no way this difference could explain the factor-4 performance gap.

Eventually it was Collegue #4 who got it right. When doing a 'go N', the query batch is indeed sent to the server N times. Also, the result set is sent back to the client N times. However, for all except the last one, isql just discards the result set, and doesn't actually process it. You'll notice that isql indeed shows only one result set and then prints "N xacts:", but I had always assumed all result sets would be fully read by isql, even though they wouldn't be printed except for the last one.
As it turned out, isql doesn't even bother to read out the individual rows and columns for all result sets except the last one. For 10,000 result sets of 100 rows and 20 columns each, the amount of client processing that would require adds up, and by skipping this processing, isql ended up faster than the stored proc.

With that wisdom, it is clear how the isql part of the test should be performed in order to make a fair comparison with the stored procedure: generate a SQL script with the query, followed by 'go', repeated 10,000 times. isql will then fully process all result sets, just as it does for the stored procedure.

Lesson Learned: using 'go 10000' is not a good way to do a realistic end-to-end performance test.
Second Lesson Learned: no matter how well you know ASE, there's always something new left to learn... as me and my collegues found out.


PS. For completeness, sqsh behaves exactly the same as isql when it comes to the handling of 'go N'.
How would you rate this ASE quiz question?
Great    OK    Average    Boring     


January 2010
You have a SQL script file containing tables, data, stored procedures etc., which are an essential part of the applications running on your ASE system. Periodically, as changes to your applications are implemented, the contents of this script changes too, and it needs to be re-run (with 'isql') against the ASE database for the application.
When running this script, you're assuming a database named 'PROD' to exist, since this is the place where the tables, stored procedures (etc) are located. However, you need to be 100% sure that this database indeed exists, and also that it can be accessed (it could inacessible due to having been marked 'suspect' for example, or it could have been put in single-user mode or in dbo-use-only mode).
So, if for some reason, there is no database named 'PROD', or it cannot be used, the script should not run, but raise an error message indeed.
This is less trivial to implement than it sounds.

The question: How can you conditionally abort execution of your SQL script somewhere in the middle of the script when database 'PROD' cannot be accessed?

Answer:
To abort a SQL script based on some condition that you detect in your SQL, use the built-in function syb_quit(). When running select syb_quit(), the ASE server will terminate the connection with the client immediately. This is the right technique to abort a SQL script somewhere half-way.

For the case described above, you could structure things as follows in your SQL script:
use master
go

-- this is needed to make sure the error message is displayed 
-- in case of a script abort
set flushmessage on
go

-- check database 'PROD' exists:
if not exists (select * from dbo.sysdatabases where name = 'PROD')
begin
    print "We seem to have a problem: database 'PROD' does not exist."
    print "Aborting..."

    -- now terminate the script:
    select syb_quit()
    -- this line is never reached!
end
go

-- OK, the database seems to exist...
use PROD
go
-- but we need to verify we can actually *use* this database
if db_name() != 'PROD'
begin
    print "We seem to have a problem: I cannot access database 'PROD'."
    print "Aborting..."

    -- now terminate the script:
    select syb_quit()
    -- this line is never reached!
end
go

...rest of SQL script...

When executing syb_quit(), the script is terminated and the rest of the SQL is not executed.
Since the client application doesn't know what's going on, it will detect that the connection is lost and print an error message, like this:
% isql -U my_login -P mysecr3t -S PROD_SERVER -i my_sql_script.sql
We seem to have a problem: database 'PROD' does not exist.
Aborting...
CT-LIBRARY error:
        ct_results(): network packet layer: internal net library error: 
Net-Library operation terminated due to disconnect
%

The sort of thing we're also protecting against here include: running the script as the wrong login/user and running it in the wrong server (you coudl of course add a check for the right server name).
Keep in mind that without such a check, the script would proceed and execute whatever SQL is in the script, in the database you happen to be in (probably 'master'). That may not be the right thing to do....

When you think about it, it's actually odd not to include some checks like these: it means that (i) you're absolutely sure such a situation could never occur and (ii) if it did, it would be harmless. Would those assumptions indeed be correct for your system? I bet they wouldn't.

Another example where such a check-and-abort construct can be useful is when you want to ensure that a script file gets executed against the proper version of ASE. For example, a script containing SQL UDFs should be executed against ASE 15.0.2 or later: when running it against ASE 12.x or 15.0.1, errors will result since SQL UDFs are supported only as of version 15.0.2. See the downloadable file behind this link for an example of such a check.

Lastly, back in ASE version 12.0, when syb_quit() was introduced, the function was left undocumented, leaving some ASE users wondering whether this was safe to use. However, these days syb_quit() is normally documented in the ASE manuals, and it can safely be used.
How would you rate this ASE quiz question?
Great    OK    Average    Boring     


More ASE Quiz questions: 2010 - 2009 - 2008 - 2007 - 2006 - 2005 - 2004 - 2003 - 2002 - 2001


 
 This document is located at www.sypron.nl/quiz2010a.html