Showing posts with label performance. Show all posts
Showing posts with label performance. Show all posts

Tuesday, August 09, 2016

Informix in SPARC.... by Oracle! / Informix em SPARC... pela Oracle

Oracle publishing information about Informix on their servers (original version here: http://informix-technology.blogspot.com/2016/08/informix-in-sparc-by-oracle-informix-em.html )


English version
In 2001, 15 years ago, IBM acquired Informix Software company. Competitors, in particular Oracle tried to tell customers that Informix would go away fast. 15 years later we're still around and still introducing unique features in the product, while keeping the simplicity, robustness and performance that made up Informix's DNA.
Now,, again 15 years later, we can see an Oracle blog post with a performance comparison for Informix 12.1 running on Oracle''s own processor (SPARC S7) against Intel's hardware (E5 v4). It's interesting to see how Oracle is trying to show their hardware customers running Informix that they should stay with SPARC... 15 years after they told the world "Informix is gone".... 15 years in which they tried to tell Informix customers they should move to Oracle.
It's a good thing IBM doesn't try to kill our hardware competitors by discontinuing our software products on the competitor's hardware like some try to do



Versão Portuguesa
Em 2001, há 15 anos, a IBM adquiriu a empresa Informix Software. A concorrência, em particular a Oracle, tentou dizer aos clientes que o Informix desapareceria rapidamente. 15 anos depois ainda cá estamos e continuamos a introduzir funcionalidades únicas no produto enquanto mantemos a simplicidade, robustez e eficiência que fizeram o ADN do Informix
Agora, repito 15 anos depois, podemos ver um artigo num blog da Oracle com uma comparação de performance do Informix 12.1 a correr no processador da Oracle (SPARC S7) contra hardware da Intel's (E5 v4). É interessante ver como a Oracle está a tentar mostrar aos seus clientes de hardware que devem manter-se em SPARC... 15 anos depois de terem dito ao mundo que o "Informix is gone".... 15 anos em que tentaram dizer aos clientes Informix que deveriam mudar-se para Oracle.
É bom que a IBM não tente matar os seus concorrentes de hardware descontinuando os seus produtos de software nas plataformas da concorrência como alguns tentam fazer

Wednesday, September 03, 2014

INDEX SKIP SCAN

This article is written in Englsih and Portuguese (original version here)
Este artigo está escrito em Inglês e Português (versão original aqui)


English version
What I'm writing about is not really a new feature... It was introduced in 11.70.xC1 and even then it was a port from Informix XPS and I suppose other RDBMs may have something like this. The reason why I'm writing about this now is that I recently went through some situations that made evident the reason why it was introduced

Problem description

It may not be evident to most people working in databases, but the way the data is physically distributed on disk will have a serious impact on the query resolution time. Imagine a simple scenario where we're joining two tables by customer_num for example. One of the tables is relatively large (200M rows) and for each customer_num it contains several records (tens). Let's call it the events table. We're joining it with another table (let's just call it a temp table) where we also have the customer_num and some other fields to which we apply some conditions.
The query plan is pretty obvious. Filters on the temp table and a nested loop with an index lookup on the events table.
This description mimics a real customer situation where they were complaining that the same query, roughly with the same amount of data and similar distribution jumped from around 1H to several hours. Apparently nothing relevant was changed. So why would the query take so much longer now?!

After examining the situation the conclusion was pretty obvious, although the root cause was still unknown: The query plan was the same, but the query now was "always" waiting for I/O. Assuming no problems were happening on the I/O system, what could explain this?

The explanation

After some discussion we figured out what changed. In previous situations, the events table was loaded by a process that worked as a single thread, and the way it compiled the information made the INSERTs in "batch mode". In practice, all the records from a single customer were inserted sequentially, so they were physically near on the disk. Due to some other changes, now the table was loaded using parallel threads, each working on a group of customers. So in practice each customer's records were split across several (sometimes far) physical disk pages.
It's important to note that from the informix perspective we were doing exactly the same steps:
  1. Filter the records on the temp table, extracting a stream of customer_num
  2. For each customer_num (NESTED LOOP), do an index lookup on the events table. This returns a set of rowids (physical page pointers on the data structure)
  3. For each rowid extracted before, access the physical page that contains the matching data.
Why would it take longer then?:
  • In the previous situation, with a single access to a page we would retrieve several rows of data
  • Even when the number of records for a customer_num would require more than one page, it was very likely the pages were physical contiguous. Due to several layers of caches (physical disks, controllers, arrays etc.) the second page access was probably already in cache (the Informix page size is small and no disk will transfer "only" 2 or 4KB on each operation). So the sub-sequent disk I/Os for the same customer would probably not require a real physical I/O 
Is this enough to explain the performance difference? Yes! Definitely. We tested to load ordered data into the table and got the expected results.
This is a good reason for the existence of CLUSTERED INDEXES.

INDEX SKIP SCAN

So, how is the above problem related to the feature called INDEX SKIP SCAN? To be honest it isn't... INDEX SKIP SCAN will not be used in the above situation. But I'm not (yet) completely crazy. In practice this feature tries to solve the same problem. How does it work? The skip scan was introduced when we released the multi index scan. When we use more than one index on the same table for a query, we gather several sets of ROWIDs. And it's highly likely that some of these ROWIDs are repeated among different sets. So it makes sense that we eliminate (skip) some ROWIDs from the scan (to avoid fetching the same rows several times). That's where the name comes from. In order to do this, each set of ROWIDs must be ordered and then merged.
Although it was designed for multi index access, the process can be useful for a single index also. Before the feature, Informix would fetch the page for each rowid retrieved from the index. With the feature it will try to buffer the rowids, sort them, and access the pages after. The result of this is that we increase the probability of fetching sequential (or at least close enough) pages, so that we take advantage of the underlying caches.

Testing

In order to show the effect of this physical layout, I prepared a relatively simple test case. I generated 2M rows of a table using an AWK script. This table has just two columns:
  • col1 INTEGER
  • col2 CHAR(75)
The data was created in a way that the values for column col1 vary between 1 and 40000. On average each value in col1 has 50 rows with that value. Maximum repetition is 82:

castelo@primary:informix-> dbaccess stores -

Database selected.

> select first 3 col1 c1, count(*) c2 from test_data group by 1 order by 2 desc;
select first 3 col1 c1, count(*) c2 from test_data group by 1 order by 2;
select avg(c2) from (select col1 c1, count(*) c2 from test_data group by 1 );

         c1               c2 
      38137               82
      30478               79
      29224               79
3 row(s) retrieved.

> 
         c1               c2 
      33710               24
      39038               26
      32313               26
3 row(s) retrieved.
> 
           (avg) 
50.0000000000000
1 row(s) retrieved.

> 


I then created a few SQL scripts:
  1. test_query_count
    Does a simple count WHERE col1 BETWEEN 1000 and 1400
  2. test_query_data_full
    Does a SELECT * of the same records, forcing a SEQUENTIAL SCAN
  3. test_query_data_index_skip_scan
    Does a SELECT * of the same records forcing the new feature
  4. test_query_data_index
    Does a SELECT * of the same records using the normal INDEX PATH
  5. test_query_force_sort
    This is the trickiest one. I do a SELECT * using the ROWIDs gathered from an inline view that uses the same criteria and ORDERs the ROWIDs. The goal here is to emulate the feature in a way that it's compatible with older versions
All the scripts write the query plan to a file and insert a record into a control table that has the identifier for the query and the start and stop time.
castelo@primary:informix-> for script in test_query*.sql
do
  printf '===============================================\n%s\n===============================================\n' $script
  cat $script
  printf "\n\n\n"
done
 ===============================================
test_query_count.sql
===============================================
!rm sqexplain_query_count.out
SET EXPLAIN FILE TO 'sqexplain_query_count.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Simple count', CURRENT YEAR TO FRACTION(3));
SELECT --+ EXPLAIN
COUNT(*) FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Simple count';



===============================================
test_query_data_full.sql
===============================================
!rm sqexplain_data_full.out
SET EXPLAIN FILE TO 'sqexplain_data_full.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Sequential scan (force)', CURRENT YEAR TO FRACTION(3));
UNLOAD TO /dev/null
SELECT --+ EXPLAIN, FULL (test_data)
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Sequential scan (force)';



===============================================
test_query_data_index_skip_scan.sql
===============================================
!rm sqexplain_data_index_skip_scan.out
SET EXPLAIN FILE TO 'sqexplain_data_index_skip_scan.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Data with index skip scan (force)', CURRENT YEAR TO FRACTION(3));
UNLOAD TO /dev/null SELECT --+ EXPLAIN, MULTI_INDEX(test_data)
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Data with index skip scan (force)';



===============================================
test_query_data_index.sql
===============================================
!rm sqexplain_data_index.out
SET EXPLAIN FILE TO 'sqexplain_data_index.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Data with index', CURRENT YEAR TO FRACTION(5));
UNLOAD TO /dev/null SELECT --+ EXPLAIN
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Data with index';



===============================================
test_query_force_sort.sql
===============================================
!rm sqexplain_force_sort.out
SET EXPLAIN FILE TO 'sqexplain_force_sort.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Data with index and sorted rowIDs', CURRENT YEAR TO FRACTION(3));
UNLOAD TO /dev/null SELECT --+ EXPLAIN
a.* FROM test_data a, (SELECT rowid r FROM test_data c WHERE col1 BETWEEN 1000 AND 1400 ORDER BY 1) b
WHERE a.rowid = b.r;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Data with index and sorted rowIDs';


Finally I created a SHELL script that recreates and loads the data table and the control table and then executes each of these scripts. It restarts the engine between each script to clean up the caches Let's see the results:

castelo@primary:informix-> cat test_script.sh;printf "\n\n";./test_script.sh 2>&1| grep -v "^$"
#!/bin/bash

printf "Recreating and loading table...\n"
dbaccess stores <<EOF
DROP TABLE IF EXISTS test_data;
DROP TABLE IF EXISTS test_data_results;

CREATE RAW TABLE test_data
(
        col1 INTEGER,
        col2 CHAR(75)
) IN dbs1 EXTENT SIZE 5000 NEXT SIZE 5000;


BEGIN WORK;
LOCK TABLE test_data IN EXCLUSIVE MODE;
LOAD FROM test_data.unl INSERT INTO test_data;
COMMIT WORK;

ALTER TABLE test_data TYPE (standard);

CREATE INDEX ix_test_data_1 ON test_data(col1) IN dbs2;

CREATE TABLE test_data_results
(
        access_type VARCHAR(255),
        query_start DATETIME YEAR TO FRACTION(3),
        query_end DATETIME YEAR TO FRACTION(3)
);

EOF
printf "Done...\n"
for SCRIPT in test_query_count.sql test_query_data_full.sql test_query_data_index_skip_scan.sql test_query_data_index.sql test_query_force_sort.sql
do
        printf "Stopping informix...\n"
        onmode -ky
        printf "Starting informix...\n"
        oninit -w
        printf "Running script $SCRIPT\n"
        dbaccess stores $SCRIPT
        printf "\nDone...\n"
done

dbaccess stores <<EOF
SELECT
        access_type, query_start, query_end, query_end - query_start elapsed_time
FROM
        test_data_results;
EOF


Recreating and loading table...
Database selected.
Table dropped.
Table dropped.
Table created.
Started transaction.
Table locked.
2000000 row(s) loaded.
Data committed.
Table altered.
Index created.
Table created.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_count.sql
Database selected.
Explain set.
1 row(s) inserted.
      (count(*)) 
           20267
1 row(s) retrieved.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_data_full.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_data_index_skip_scan.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_data_index.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_force_sort.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Database selected.
access_type   Simple count
query_start   2014-09-02 13:08:12.141
query_end     2014-09-02 13:08:12.259
elapsed_time          0 00:00:00.118
access_type   Sequential scan (force)
query_start   2014-09-02 13:08:29.215
query_end     2014-09-02 13:08:36.838
elapsed_time          0 00:00:07.623
access_type   Data with index skip scan (force)
query_start   2014-09-02 13:08:54.250
query_end     2014-09-02 13:09:03.403
elapsed_time          0 00:00:09.153
access_type   Data with index
query_start   2014-09-02 13:09:21.684
query_end     2014-09-02 13:10:53.474
elapsed_time          0 00:01:31.790
access_type   Data with index and sorted rowIDs
query_start   2014-09-02 13:11:12.682
query_end     2014-09-02 13:11:22.874
elapsed_time          0 00:00:10.192
5 row(s) retrieved.
Database closed.
castelo@primary:informix->

We can see the results in a table:

Query
Start
Stop
Elapsed
Simple count
13:08:12.141
13:08:12.259
00:00:00.118 (0s)
Sequential scan (force)
13:08:29.215
13:08:36.838
00:00:07.623 (7.6s)
Data with index skip scan (force)
13:08:54.250
13:09:03.403
00:00:09.153 (9.1s)
Data with index
13:09:21.684
13:10:53.474
00:01:31.790 (1m31s)
Data with index and sorted rowIDs
13:11:12.682
13:11:22.874
00:00:10.192 (10.1s)
So, here are the important points:
  1. SELECT COUNT(*) is extremely fast. Just count the index entries that match the criteria
  2. A sequential scan takes around 8s. The table size is small.
  3. Forcing the MULTINDEX path allows the INDEX SKIP SCAN feature and it's around the same as a full sequential scan
  4. Going through the normal index PATH is extremely slow. 1.5 minutes compared to ~10s for the other options. That's the price we pay for scattered reads.
  5. By simulating the feature we get a time very close to the feature itself
I also used a script I mentioned before, called ixprofiling, to show the real work being done do solve each query. For the query that uses INDEX SKIP SCAN:

castelo@primary:informix-> onmode -ky;oninit -w;ixprofiling -z -i stores test1.sql

Database selected.

Engine statistics RESETed. Query results:
Query start time: 17:31:06.017924000

UNLOAD TO /dev/null SELECT --+ EXPLAIN, MULTI_INDEX(test_data)
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
20267 row(s) unloaded.

Query stop time: 17:31:15.806221000

Thread profiles (SID: 6)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
40641 0     0     0     0     526   0     0     0     18335 0     0     0     2     1     0     0     36962 0.340086802 sqlexec     
LkWs         IOWs         nIOW         IdxBR        Name                               
------------ ------------ ------------ ------------ -----------------------------------
0.0          9.3030077117 18157        0            sqlexec                            

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
20267 0     0     0     18018 0     495   0     0     0     18076 0     1     test_data
20307 0     0     0     109   0     0     0     0     0     365   0     0     test_data#ix_test_data_1

Database closed.

real    0m11.807s
user    0m0.000s
sys     0m0.090s
castelo@primary:informix->

For the normal query:

castelo@primary:informix-> onmode -ky;oninit -w;ixprofiling -z -i stores test2.sql

Database selected.

Engine statistics RESETed. Query results:
Query start time: 17:45:39.873507000

UNLOAD TO /dev/null SELECT --+ EXPLAIN
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
20267 row(s) unloaded.

Query stop time: 17:47:11.803271000

Thread profiles (SID: 6)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
40720 0     0     0     0     31    0     0     0     20608 0     0     0     1     0     0     0     950   0.261092628 sqlexec     
LkWs         IOWs         nIOW         IdxBR        Name                               
------------ ------------ ------------ ------------ -----------------------------------
0.0          6.2963172635 320          0            sqlexec                            

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
20267 0     0     0     17921 0     0     0     0     0     39562 0     0     test_data
20386 0     0     0     206   0     0     0     0     0     587   0     0     test_data#ix_test_data_1

Database closed.

real    1m35.757s
user    0m0.050s
sys     0m0.080s
castelo@primary:informix->


Things to note:
  1. The number of disk reads is roughly the same.
  2. The number of buffer reads on the partitions is significantly higher in the second case
  3. The CPU time is actually higher (slightly) on the first query
If we compare sections of onstat -g ioa we confirm the work done in terms of I/O was similar, but we know one of them took much longer:

I/O queues:
AIO I/O queues with INDEX SKIP SCAN:
q name/id    len maxlen totalops  dskread dskwrite  dskcopy
 fifo   0      0      0        0        0        0        0
drda_dbg   0      0      0        0        0        0        0
sqli_dbg   0      0      0        0        0        0        0
  kio   0      0     32      725      408      317        0
  kio   1      0     33    20342    20274       68        0
  adt   0      0      0        0        0        0        0
  msc   0      0      1        4        0        0        0


AIO I/O queues for normal query:
q name/id    len maxlen totalops  dskread dskwrite  dskcopy
 fifo   0      0      0        0        0        0        0
drda_dbg   0      0      0        0        0        0        0
sqli_dbg   0      0      0        0        0        0        0
  kio   0      0     33     9667     9653       14        0
  kio   1      0     33    11051    11035       16        0
  adt   0      0      0        0        0        0        0
  msc   0      0      1        4        0        0        0


Apparently the slower one did less work! Some more writes for the first. Hard to tell if significant.


AIO I/O vps with INDEX SKIP SCAN:
class/vp/id s  io/s totalops  dskread dskwrite  dskcopy  wakeups  io/wup  errors tempops
 fifo  7  0 i   0.0        0        0        0        0        0   0.0       0        0
  kio -1  0 i   0.5      379      210      169        0      579   0.7       0        0
  kio -1  1 i  25.4    19839    19792       47        0    33783   0.6       0        0
  msc  6  0 i   0.0        4        0        0        0        4   1.0       0        9
  aio  5  0 i   0.6      469      195       84        0      469   1.0       0        0
 [...]


AIO I/O vps for normal query:
class/vp/id s  io/s totalops  dskread dskwrite  dskcopy  wakeups  io/wup  errors tempops
 fifo  7  0 i   0.0        0        0        0        0        0   0.0       0        0
  kio -1  0 i  56.2     9506     9492       14        0    15327   0.6       0        0
  kio -1  1 i  61.5    10395    10379       16        0    14895   0.7       0        0
  msc  6  0 i   0.0        4        0        0        0        4   1.0       0        9
  aio  5  0 i   2.7      459      195       74        0      459   1.0       0      460
  [...]

Roughly the same work, but more evely split across the two kio threads for the slower query. Possibly an indication that the I/Os are slower.


AIO big buffer usage summary with INDEX SKIP SCAN:
class                 reads                                   writes
       pages    ops  pgs/op  holes  hl-ops hls/op      pages    ops  pgs/op
fifo      0       0   0.00      0       0   0.00           0      0   0.00
drda_dbg  0       0   0.00      0       0   0.00           0      0   0.00
sqli_dbg  0       0   0.00      0       0   0.00           0      0   0.00
 kio  21665   20002   1.08    983     151   6.51         871    216   4.03


AIO big buffer usage summary for normal query:

class                 reads                                   writes
       pages    ops  pgs/op  holes  hl-ops hls/op      pages    ops  pgs/op
fifo      0       0   0.00      0       0   0.00           0      0   0.00
drda_dbg  0       0   0.00      0       0   0.00           0      0   0.00
sqli_dbg  0       0   0.00      0       0   0.00           0      0   0.00
 kio  22576   19871   1.14   1888     279   6.77         498     30  16.60 
 
Not a huge difference for the number of operations.

Conclusion

Don't underestimate the real effect of data dispersion on disk. This situation shows clearly the impact. On another customer situation I had very recently I saw around 4 times performance degradation when comparing the sorted rowids method to the natural method.
I'd like to see this feature being more naturally used in Informix (outside the scenario of MULTI_INDEX path).

Versão Portuguesa
O assunto sobre o qual vou escrever não é propriamente novidade. Foi introduzido na versão 11.70.xC1 e mesmo aí foi um port de uma funcionalidade que já existia no Informix XPS e penso que em outras bases de dados do mercado. A razão porque decidi descrever esta funcionalidade prende.se com o facto de ter estado envolvido em situações recentemente onde ficou evidente a razão para a sua criação.

Descrição do problema

Pode não ser evidente para todas as pessoas que trabalham com bases de dados, mas a forma como os dados estão físicamente dispostos nos discos terá um impacto significativo na resolução das queries. Imagine um cenário simples onde façamos um JOIN entre duas tabelas por customer_num por exemplo. Uma dessas tabelas é relativamente grande (200M registos) e para cada customer_num contém vários registos (dezenas). Vamos chamar a esta tabela "eventos". Estamos a fazer o JOIN com outra tabela (chamemos-lhe apenas uma tabela temporária) onde temos o campo customer_num e outros campos aos quais aplicamos uma ou mais condições. O plano de execução será bastante óbvio. Filtros aplicados na tabela temporária e um NESTED LOOP JOIN com procura por índice na tabela de eventos.
Esta descrição é uma réplica de uma situação real num cliente onde se estavam a queixar que para o mesmo processo, grosseiramente com o mesmo volume de dados e distribuições semelhantes o processo tinha passado de 1H para várias. Aparentemente nada tinha mudado. Sendo assim de onde vinha a lentidão?!


Depois de analizar a situação a conclusão foi bastante óbvia, apesar se a causa original ser ainda desconhecida: O plano de execução não se tinha alterado, mas agora a query estava "sempre" à espera de I/O. Assumindo que não havia problemas no sistema de I/O o que poderia causar isto?

A explicação

Após alguma discussão, descobrimos o que tinha mudado. Na situação anterior, a tabela de eventos era carregada por um único processo e a forma de processamento resultava em INSERTs em "batch". Na prática, todos os registos de um mesmo customer_num eram inseridos sequencialmente, pelo que resultava numa ordenação física no disco. Devido a algumas mudanças nos processos, agora a tabela era carregada usando processos paralelos, cada um trabalhando num conjunto distinto de clientes. Por isso na prática,  os registos para um mesmo customer_num acabavam dispersos (por vezes muito afastadas) em várias páginas em disco.
É importante salientar que do ponto de vista do Informix estávamos a fazer exactamente os mesmos passos:

  1. Filtrar os registos na tabela temporária, obtendo uma lista de valores de customer_num
  2. Para cada customer_num (NESTED LOOP), fazer uma pesquisa no índice da tabela de eventos. Isto retorna uma lista de rowIDs (ponteiros para as páginas físicas dos dados)
  3. Para cada rowID obtido anteriromente, aceder à página de dados e obter os dados correspondentes
Porque demoraria mais então?
  • Na situação anterior, com um único acesso a uma página obtinhamos normalmente mais que um registo de dados (os acessos futuros já estavam em cache
  • Mesmo quando o número de registos para um customer_num precisasse de mais de uma página, as páginas sub-sequentes estavam contíguas em disco. E devido a vários níveis de caches (discos físicos, controladores, arrays etc.) o acesso às segundas páginas já seria resolvido em cache (o tamanho de página do Informix é pequeno e nenhum sistema de armazenamento faz transferências de "apenas" 2 ou 4KB em cada operação). Ou seja, os segundos acessos raramente necessitariam de uma operação física em disco
Será isto suficiente para explicar a diferença de performance? Sim! Sem sombra para dúvidas. Testámos carregar a tabela de eventos de forma "ordenada" e os tempos de execução voltaram a rondar 1H.
Eis uma boa razão para a existência de CLUSTERED INDEXES.

INDEX SKIP SCAN

Bom, então como está relacionado o problema acima com a funcionalidade chamada INDEX SKIP SCAN? Para ser honesto não está... INDEX SKIP SCAN não seria utilizado na situação acima. Mas (ainda) não estou completamente louco. Na prática esta funcionalidade  resolve o mesmo problema. Como funciona? O INDEX SKIP SCAN foi introduzido juntamente com o MULTI INDEX SCAN. Quando usamos mais que um índice numa mesma tabela, obtemos tantos conjuntos de rowIDs quanto os índices que estamos a utilizar. E é muito provável que haja duplicações entre estes conjuntos. Por isso faz todo o sentido que eliminemos (SKIP) os duplicados para evitar tentar obter a mesma linha duas vezes. É daí que vem o nome. Cada conjunto de rowIDs têm de ser ordenado e depois é feito o merge dos mesmos.
Apesar de ter sido desenhado para o MULTI INDEX ACCESS, o processo pode ser útil para um único indíce também. Antes desta funcionalidade  o acesso às páginas de dados era directo e pela ordem que os rowIDs eram encontrados. Com a funcionalidade activa é feita uma ordenação dos valores de rowID e depois então é feito o acesso aos dados. Daqui resulta uma probabilidade muito maior de que se façam I/Os de páginas consecutivas ou pelo menos mais próximas. Isto permite aproveitar melhor as caches que estejam envolvidas.

Testando

Com o objectivo de mostrar os efeitos da distribuição fisica dos dados, preparei um caso de teste relativamente simples. Gerei 2M de linhas para uma tableam usando um script AWK. Esta tabela tem apenas duas colunas:
  • col1 INTEGER
  • col2 CHAR(75)
Os dados foram criados de uma forma em que os valores da col1 variam entre 1 e 40000. Em média, cada um destes valores corresponde a 50 registos na tabela. O valor mais repeitdo tem 82 ocorrências:

castelo@primary:informix-> dbaccess stores -

Database selected.

> select first 3 col1 c1, count(*) c2 from test_data group by 1 order by 2 desc;
select first 3 col1 c1, count(*) c2 from test_data group by 1 order by 2;
select avg(c2) from (select col1 c1, count(*) c2 from test_data group by 1 );

         c1               c2 
      38137               82
      30478               79
      29224               79
3 row(s) retrieved.

> 
         c1               c2 
      33710               24
      39038               26
      32313               26
3 row(s) retrieved.
> 
           (avg) 
50.0000000000000
1 row(s) retrieved.

> 

Depois criei alguns scripts SQL:
  1. test_query_count
    Faz um COUNT(*) simples WHERE col1 BETWEEN 1000 and 1400
  2. test_query_data_full
    Faz um SELECT * dos mesmos registos, forçando um SEQUENTIAL SCAN
  3. test_query_data_index_skip_scan
    Faz um SELECT * dos mesmos registos forçando a nova funcionalidade
  4. test_query_data_index
    Faz um SELECT * dos mesmos registos, usando o normal INDEX PATH
  5. test_query_force_sort
    Este é o mais rebuscado. Faz um SELECT * usando ROWIDs obtidos numa inline view que usa o mesmo critério e um ORDERs dos ROWIDs. O objectivo é simular a funcionalidade, numa forma que seja compatível com versões anteriores
Todos os scripts escrevem o plano de execução num ficheiro e inserem numa tabela de controlo um registo que tem o identificador da query e o tempo de início e fim da mesma..
castelo@primary:informix-> for script in test_query*.sql
do
  printf '===============================================\n%s\n===============================================\n' $script
  cat $script
  printf "\n\n\n"
done
 ===============================================
test_query_count.sql
===============================================
!rm sqexplain_query_count.out
SET EXPLAIN FILE TO 'sqexplain_query_count.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Simple count', CURRENT YEAR TO FRACTION(3));
SELECT --+ EXPLAIN
COUNT(*) FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Simple count';



===============================================
test_query_data_full.sql
===============================================
!rm sqexplain_data_full.out
SET EXPLAIN FILE TO 'sqexplain_data_full.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Sequential scan (force)', CURRENT YEAR TO FRACTION(3));
UNLOAD TO /dev/null
SELECT --+ EXPLAIN, FULL (test_data)
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Sequential scan (force)';



===============================================
test_query_data_index_skip_scan.sql
===============================================
!rm sqexplain_data_index_skip_scan.out
SET EXPLAIN FILE TO 'sqexplain_data_index_skip_scan.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Data with index skip scan (force)', CURRENT YEAR TO FRACTION(3));
UNLOAD TO /dev/null SELECT --+ EXPLAIN, MULTI_INDEX(test_data)
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Data with index skip scan (force)';



===============================================
test_query_data_index.sql
===============================================
!rm sqexplain_data_index.out
SET EXPLAIN FILE TO 'sqexplain_data_index.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Data with index', CURRENT YEAR TO FRACTION(5));
UNLOAD TO /dev/null SELECT --+ EXPLAIN
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Data with index';



===============================================
test_query_force_sort.sql
===============================================
!rm sqexplain_force_sort.out
SET EXPLAIN FILE TO 'sqexplain_force_sort.out';
INSERT INTO test_data_results (access_type, query_start) VALUES ('Data with index and sorted rowIDs', CURRENT YEAR TO FRACTION(3));
UNLOAD TO /dev/null SELECT --+ EXPLAIN
a.* FROM test_data a, (SELECT rowid r FROM test_data c WHERE col1 BETWEEN 1000 AND 1400 ORDER BY 1) b
WHERE a.rowid = b.r;
UPDATE test_data_results SET query_end = CURRENT YEAR TO FRACTION(3) WHERE access_type = 'Data with index and sorted rowIDs';


Por último criei um script SHELL que recria e carrega a tabela de dados e a tabela de controlo. Depois executa cada um destes scripts, fazendo um reinício do motor para limpar caches. Vejamos os resultados:

castelo@primary:informix-> cat test_script.sh;printf "\n\n";./test_script.sh 2>&1| grep -v "^$"
#!/bin/bash

printf "Recreating and loading table...\n"
dbaccess stores <<EOF
DROP TABLE IF EXISTS test_data;
DROP TABLE IF EXISTS test_data_results;

CREATE RAW TABLE test_data
(
        col1 INTEGER,
        col2 CHAR(75)
) IN dbs1 EXTENT SIZE 5000 NEXT SIZE 5000;


BEGIN WORK;
LOCK TABLE test_data IN EXCLUSIVE MODE;
LOAD FROM test_data.unl INSERT INTO test_data;
COMMIT WORK;

ALTER TABLE test_data TYPE (standard);

CREATE INDEX ix_test_data_1 ON test_data(col1) IN dbs2;

CREATE TABLE test_data_results
(
        access_type VARCHAR(255),
        query_start DATETIME YEAR TO FRACTION(3),
        query_end DATETIME YEAR TO FRACTION(3)
);

EOF
printf "Done...\n"
for SCRIPT in test_query_count.sql test_query_data_full.sql test_query_data_index_skip_scan.sql test_query_data_index.sql test_query_force_sort.sql
do
        printf "Stopping informix...\n"
        onmode -ky
        printf "Starting informix...\n"
        oninit -w
        printf "Running script $SCRIPT\n"
        dbaccess stores $SCRIPT
        printf "\nDone...\n"
done

dbaccess stores <<EOF
SELECT
        access_type, query_start, query_end, query_end - query_start elapsed_time
FROM
        test_data_results;
EOF


Recreating and loading table...
Database selected.
Table dropped.
Table dropped.
Table created.
Started transaction.
Table locked.
2000000 row(s) loaded.
Data committed.
Table altered.
Index created.
Table created.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_count.sql
Database selected.
Explain set.
1 row(s) inserted.
      (count(*)) 
           20267
1 row(s) retrieved.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_data_full.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_data_index_skip_scan.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_data_index.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Stopping informix...
Starting informix...
Running script test_query_force_sort.sql
Database selected.
Explain set.
1 row(s) inserted.
20267 row(s) unloaded.
1 row(s) updated.
Database closed.
Done...
Database selected.
access_type   Simple count
query_start   2014-09-02 13:08:12.141
query_end     2014-09-02 13:08:12.259
elapsed_time          0 00:00:00.118
access_type   Sequential scan (force)
query_start   2014-09-02 13:08:29.215
query_end     2014-09-02 13:08:36.838
elapsed_time          0 00:00:07.623
access_type   Data with index skip scan (force)
query_start   2014-09-02 13:08:54.250
query_end     2014-09-02 13:09:03.403
elapsed_time          0 00:00:09.153
access_type   Data with index
query_start   2014-09-02 13:09:21.684
query_end     2014-09-02 13:10:53.474
elapsed_time          0 00:01:31.790
access_type   Data with index and sorted rowIDs
query_start   2014-09-02 13:11:12.682
query_end     2014-09-02 13:11:22.874
elapsed_time          0 00:00:10.192
5 row(s) retrieved.
Database closed.
castelo@primary:informix->

Podemos ver os resultados numa tabela:

Query
Start
Stop
Elapsed
Simple count
13:08:12.141
13:08:12.259
00:00:00.118 (0s)
Sequential scan (force)
13:08:29.215
13:08:36.838
00:00:07.623 (7.6s)
Data with index skip scan (force)
13:08:54.250
13:09:03.403
00:00:09.153 (9.1s)
Data with index
13:09:21.684
13:10:53.474
00:01:31.790 (1m31s)
Data with index and sorted rowIDs
13:11:12.682
13:11:22.874
00:00:10.192 (10.1s)
Aqui ficam os pontos importantes:
  1. SELECT COUNT(*) é extremamente rápido. Apenas conta o número de entradas no índice que verificam o critério
  2. Uma leitura sequencial e integral da tabela demora cerca de 10s. O tamanho da tabela é pequeno.
  3. Forçando o acesso com o MULTI_INDEX permitimos o INDEX SKIP SCAN e demora aproximadamente o mesmo que o sequential scan neste caso.
  4. Através do normal INDEX PATH, é extremamente lento. 1.5 minutos comparado com cerca de 10s das outras alternativas. É este o preço que pagamos por leituras dispersas
  5. Simulando a funcionalidade, com o truque de ordenar os rowids, conseguimos um valor parecido com o obtido usando a própria funcionalidade
Também usei um script que já referi noutr(s) artigos, chamado ixprofiling, para demonstrar o trabalho real feito por cada query. Para a query que usa o INDEX SKIP SCAN:
castelo@primary:informix-> onmode -ky;oninit -w;ixprofiling -z -i stores test1.sql

Database selected.

Engine statistics RESETed. Query results:
Query start time: 17:31:06.017924000

UNLOAD TO /dev/null SELECT --+ EXPLAIN, MULTI_INDEX(test_data)
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
20267 row(s) unloaded.

Query stop time: 17:31:15.806221000

Thread profiles (SID: 6)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
40641 0     0     0     0     526   0     0     0     18335 0     0     0     2     1     0     0     36962 0.340086802 sqlexec     
LkWs         IOWs         nIOW         IdxBR        Name                               
------------ ------------ ------------ ------------ -----------------------------------
0.0          9.3030077117 18157        0            sqlexec                            

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
20267 0     0     0     18018 0     495   0     0     0     18076 0     1     test_data
20307 0     0     0     109   0     0     0     0     0     365   0     0     test_data#ix_test_data_1

Database closed.

real    0m11.807s
user    0m0.000s
sys     0m0.090s
castelo@primary:informix->
Para a query normal:
castelo@primary:informix-> onmode -ky;oninit -w;ixprofiling -z -i stores test2.sql

Database selected.

Engine statistics RESETed. Query results:
Query start time: 17:45:39.873507000

UNLOAD TO /dev/null SELECT --+ EXPLAIN
* FROM test_data WHERE col1 BETWEEN 1000 AND 1400;
20267 row(s) unloaded.

Query stop time: 17:47:11.803271000

Thread profiles (SID: 6)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
40720 0     0     0     0     31    0     0     0     20608 0     0     0     1     0     0     0     950   0.261092628 sqlexec     
LkWs         IOWs         nIOW         IdxBR        Name                               
------------ ------------ ------------ ------------ -----------------------------------
0.0          6.2963172635 320          0            sqlexec                            

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
20267 0     0     0     17921 0     0     0     0     0     39562 0     0     test_data
20386 0     0     0     206   0     0     0     0     0     587   0     0     test_data#ix_test_data_1

Database closed.

real    1m35.757s
user    0m0.050s
sys     0m0.080s
castelo@primary:informix->
Notas:
  1. O número de leituras em disco é aproximadamente o mesmo
  2. O número de leituras de buffers por partição na segunda query é sinificativamente mais alto
  3. O tempo de CPU na verdade é mais alto (pouco) na primeira opção
Se compararmos algumas secções de um onstat -g ioa podemos confirmar as semlhanças entre o trabalho feito de uma forma e de outra:

I/O queues:
AIO I/O queues com INDEX SKIP SCAN:
q name/id    len maxlen totalops  dskread dskwrite  dskcopy
 fifo   0      0      0        0        0        0        0
drda_dbg   0      0      0        0        0        0        0
sqli_dbg   0      0      0        0        0        0        0
  kio   0      0     32      725      408      317        0
  kio   1      0     33    20342    20274       68        0
  adt   0      0      0        0        0        0        0
  msc   0      0      1        4        0        0        0

AIO I/O queues para a query  normal:
q name/id    len maxlen totalops  dskread dskwrite  dskcopy
 fifo   0      0      0        0        0        0        0
drda_dbg   0      0      0        0        0        0        0
sqli_dbg   0      0      0        0        0        0        0
  kio   0      0     33     9667     9653       14        0
  kio   1      0     33    11051    11035       16        0
  adt   0      0      0        0        0        0        0
  msc   0      0      1        4        0        0        0

Aparentemente o mais lento até fez menos trabalho. Mais algumas escritas para o primeiro. É difícil extrair o significado disto

AIO I/O vps com INDEX SKIP SCAN:
class/vp/id s  io/s totalops  dskread dskwrite  dskcopy  wakeups  io/wup  errors tempops
 fifo  7  0 i   0.0        0        0        0        0        0   0.0       0        0
  kio -1  0 i   0.5      379      210      169        0      579   0.7       0        0
  kio -1  1 i  25.4    19839    19792       47        0    33783   0.6       0        0
  msc  6  0 i   0.0        4        0        0        0        4   1.0       0        9
  aio  5  0 i   0.6      469      195       84        0      469   1.0       0        0
 [...]

AIO I/O vps para a query normal:
class/vp/id s  io/s totalops  dskread dskwrite  dskcopy  wakeups  io/wup  errors tempops
 fifo  7  0 i   0.0        0        0        0        0        0   0.0       0        0
  kio -1  0 i  56.2     9506     9492       14        0    15327   0.6       0        0
  kio -1  1 i  61.5    10395    10379       16        0    14895   0.7       0        0
  msc  6  0 i   0.0        4        0        0        0        4   1.0       0        9
  aio  5  0 i   2.7      459      195       74        0      459   1.0       0      460
  [...]

Grosso modo o mesmo trabalho, mas mais distribuído entre as threads kio no caso da query mais lenta. Possivelmente uma indicação de que as operações estão a demorar mais.

AIO big buffer usage summary com INDEX SKIP SCAN:
class                 reads                                   writes
       pages    ops  pgs/op  holes  hl-ops hls/op      pages    ops  pgs/op
fifo      0       0   0.00      0       0   0.00           0      0   0.00
drda_dbg  0       0   0.00      0       0   0.00           0      0   0.00
sqli_dbg  0       0   0.00      0       0   0.00           0      0   0.00
 kio  21665   20002   1.08    983     151   6.51         871    216   4.03


AIO big buffer usage summary para a query normal:
class                 reads                                   writes
       pages    ops  pgs/op  holes  hl-ops hls/op      pages    ops  pgs/op
fifo      0       0   0.00      0       0   0.00           0      0   0.00
drda_dbg  0       0   0.00      0       0   0.00           0      0   0.00
sqli_dbg  0       0   0.00      0       0   0.00           0      0   0.00
 kio  22576   19871   1.14   1888     279   6.77         498     30  16.60 
 
Não existe uma grande diferença no número de operações

Conclusão

Nunca subestime o efeito real da dispersão de dados em disco. Esta situação demonstra o potencial impacto com clareza. Numa outra situação num cliente, que tive muito recentemente, verifiquei uma degradação de performance de 4 vezes quando comparava o método de ordenar os ROWIDs com o método "natural". Gostaria de ver esta funcionalidade ser usada mais naturalmente no Informix (fora dos cenários de utilização do MULTI_INDEX path).

Tuesday, October 02, 2012

UDRs: COALESCE

This article is written in English and Portuguese
Este artigo está escrito em Inglês e Português


English version:

Introduction
This is another article in the UDR series. In a discussion on IIUG mailing list someone was complaining about the lack of COALESCE function in Informix. The first answer was that nested NVL() calls could be used as a replacement, or an SPL function could be written to implement it. But the same person warned that the SPL procedure would have a significant performance impact. I made a couple of tests and verified the same. So I decided to test a UDR to implement it.
But before diving into it, a few warnings are in order... The true COALESCE() function or SQL construct is a very flexible operation that takes an undetermined number of arguments of unknown and possibly different data types. I'm not sure if something like that can be implemented in a user defined function (UDR). So, for the scope of this article I'll assume two restrictions: A fixed number of maximum arguments (this would be easy to change) and that all the arguments belong to the same type (although the engine could cast them).
I'd also like to thank John Miller, the Senior Technical Staff Member of the Informix team, and a well known member of the Informix community, for his input, suggestions and code review.

The code
You can find the C UDR source code at the end of this article. I'll just go through it, to better explain how it works, but the juicy part is the comparison between several methods that will follow.

Lines 1 to 8 are just the usual include sections.

Lines 11 to 15 is the function header. As you can see at the C code level it receives ten LVARCHARs and returns an LVARCHAR. The reason why the LVARCHAR was choose is because it has implicit casts for most if not all the data types. This means that when we define the function at the SQL level we can use any data type we like (or create several functions with different signatures that allows for a broader use).

Lines 17 to 20 include an auxiliary variable declaration and initialization with the function mi_fp_nargs() which returns the number of parameters defined for the function.

Line 23 defines a loop that checks if any of the arguments is not null. If it finds one, it returns that argument. Unfortunately I could not find an easy way to make this piece of code generic (automatically adaptable to a different number of parameters), so a long switch statement was used.

If none of the arguments is non-NULL, then at lines 62-63 it returns a NULL value.

The compilation

As usual I use a simple makefile to generate the dynamic library containing the code:
include $(INFORMIXDIR)/incl/dbdk/makeinc.linux86_64


MI_INCL = $(INFORMIXDIR)/incl
CFLAGS = -DMI_SERVBUILD $(CC_PIC) -I$(MI_INCL)/public $(COPTS)
LINKFLAGS = $(SHLIBLFLAG) $(SYMFLAG) $(LD_SHARED_FLAGS)

all: ix_coalesce

clean:
        rm *.udr *.o

ix_coalesce: ix_coalesce.udr
        @echo "Library genaration done"

ix_coalesce.o: ix_coalesce.c
        @echo "Compiling..."
        $(CC) -c $(CFLAGS) -o $@ $?

ix_coalesce.udr: ix_coalesce.o
        @echo "Creating the library..."
        $(SHLIBLOD) $(LINKFLAGS) -o $@ $?



In the end, after the make command we should have a dynamic linked library that we can use to create the function in SQL. The make process is simply:

tpch@kimball:informix-> make
Compiling...
cc -c -DMI_SERVBUILD -fPIC -I/opt/informix/srvr1170fc5/incl/public -g -o ix_coalesce.o ix_coalesce.c
Creating the library...
gcc -shared -m64 -Bsymbolic -shared -m64 -o ix_coalesce.udr ix_coalesce.o
Library genaration done

tpch@kimball:informix-> ls -lia ix_coalesce.udr
1654803 -rwxr-xr-x 1 informix informix 8273 Aug 13 00:00 ix_coalesce.udr

Creating the function in SQL
Once we get the compiled code in the form of a dynamic loadable library we need to create the function in SQL, referencing the C code function.
This is done with this simple SQL code:
DROP FUNCTION IF EXISTS coalesce_udr;
CREATE FUNCTION coalesce_udr(
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL
) RETURNING INTEGER
WITH (NOT VARIANT, HANDLESNULLS)
EXTERNAL NAME '/opt/informix/work/coalesce/ix_coalesce.udr(ix_coalesce)'
LANGUAGE C;

A few notes:
  1. I'm defining an UDR that takes INTEGERs as arguments. Internally they'll be treated as LVARCHARs. If we need COALESCE() for other types of arguments we could create other functions, with the same name but different parameter types. This is called overloading and is perfectly supported in Informix
  2. I used HANDLESNULLS because without it, if we use NULL as arguments the C functions are not called and return NULL
  3. EXTERNAL NAME specifies the "path" to the C code function. In this case it's the pathname of the dynamic loadable library we created and the function name (inside the library) between parentheses

Tests and speed comparison
So, the purpose of this is to compare several ways to overcome the lack of a native COALESCE SQL construct. The ways I considered were:
  1. Using nested NVL() functions directly in the SQL statement. This is the fastest and was considered the reference
  2. Creating an SPL function called coalesce_basic that receives the arguments and has a nested NVL() statement inside the SPL
  3. Creating an SPL function called coalesce_if that is similar to the previous, but instead of a nested NVL() structure has a sequence of IF statements
  4. The C code function explained above
You can find the code for all this alternatives at the end of the article. After creating the functions I have created a test set of data which has 10M rows and 10 fields on each row.
Each row has only one field which is not NULL. And the non NULL field changes sequentially in each row. An AWK script used to generate the test data is also shown at the end.

I've loaded the file into a table and I've run an UNLOAD just to make sure I get the table into memory. After all this I run the UNLOAD again, using the 10 columns as arguments for the several functions (or SQL) I mentioned above. The UNLOAD is made to /dev/null to minimize I/O times. Again you can check the SQL code at the end of the article. For each function type I insert into a temporary table the start and finish time. In the end I run a query that obtains the time comparison of each of he functions with the simple SQL construct using the nested NVLs directly on the query.

I've run this several times and the results were always very similar. And the timings obtained look like this (two samples):
using  Nested NVL:        0 00:00:35.48369 100.00%
using  Nested NVL in SPL: 0 00:02:47.96074 473.34%
using  IF based in SPL:   0 00:03:27.54880 584.91%
using  COALESCE_UDR:      0 00:00:47.85661 134.86%


using  Nested NVL:        0 00:00:35.31295 100.00%
using  Nested NVL in SPL: 0 00:02:45.31578 468.14%
using  IF based in SPL:   0 00:03:26.77770 585.55%
using  COALESCE_UDR:      0 00:00:47.78794 135.32%


Conclusions

The conclusions are pretty obvious, but let's go through them:
  1. There is nothing faster than the nested NVLs. So if your main concern is the the speed you should use it. The obvious drawback is that the code will look "strange" to users of other databases and it's not a good solution if you're porting an application or query
  2. Nested NVL in SPL has a huge performance impact. Note that the code is similar to the above but will run in an SPL, so it will require function calling.
  3. IF based SPL is even slower than the previous
  4. The performance of the C UDR that we've created is not that bad... Yes, we see a performance impact of between 30 and 40%, but would that be significant? Note that in the test case we're calling the function 10M times... and the impact was 13s... What would happen if the result set had a few lines? Or a few hundreds? Also note that the test case is accessing data in the database cache. And it's not evaluating any WHERE clause. So, it lloks like a good compromise between performance and flexibility. As usual, the C UDR's are pretty fast.

Versão Portuguesa:

Introdução
Eis mais um artigo da série dedicada às UDRs (User Defined Routines). Numa discussão na lista de correio do IIUG, alguém se queixava da inexistência da função COALESCE no Informix. A primeira resposta foi que NVL() encadeados poderiam ser usados como substituto, ou também um procedimento SPL. Mas a mesma pessoa alertou que um procedimento SPL teria um impacto de performance muito significativo. Executei alguns testes e pareceu-me que isto se comprovava. Daí ter decidido criar uma UDR para implementar o COALESCE.
Mas antes de mergulhar no assunto convém analisar alguns pontos... O verdadeiro COALESCE é mais uma construção SQL que uma função. É uma operação muito flexível que recebe um número não determinado de argumentos de tipos possivelmente diferentes. Não sei se tal seria possível implementar numa UDR. Portanto, no contexto deste artigo vou assumir duas restrições: Um número fixo para o máximo de argumentos (isto poderia ser facilmente mudado) e que todos os argumentos são do mesmo tipo (embora o motor pudesse fazer a conversão dos parâmetros).
Gostaria também de agradecer ao John Miller, membro do staff técnico da equipa Informix, um membro muito reconhecido da comunidade Informix internacional, pelo seu input, sugestões e revisão de código.

O código
Pode consultar o código C do UDR no final deste artigo. Vou apenas explicar alguns passos desse código para que entenda como funciona, mas a parte mais interessante será a comparação entre os diversos métodos que se seguirá.

Nas linhas 1 a 8 estão os habituais includes.

Linhas 11 a 15 são o cabeçalho da função. Como pode verificar, ao nível do código C a função recebe e retorna LVARCHARs. A razão para tal é que existem conversões (casts) implícitos para quase todos, senão todos, os tipos de dados. Isto significa que quando se definem as funções ao nível do SQL podemos usar qualquer tipo de dados que queiramos (ou criar várias funções com assinaturas diferentes para permitir um utilização mais abrangente).

Linhas 17 a 20 incluem uma declaração de variável auxiliar e respetiva inicialização com a função mi_fp_nargs() que retorna o número de parâmetros definidos para a função.

A linha 23 define um ciclo que verifica se algum dos argumentos contém um valor não nulo. Se encontrar um, retorna esse argumento. Infelizmente não encontrei uma forma de tornar esta parte do código genérica (adaptável automaticamente a um diferente número de parâmetros), por isso uma longa instrução switch foi usada.

Se nenhum dos argumentos for não nulo, então nas linhas 62-63 um valor NULL é retornado.


A compilação

Como vem sendo hábito nestes artigos sobre UDRs, utilizo um makefile simples para gerar a biblioteca dinâmica contendo o código:
include $(INFORMIXDIR)/incl/dbdk/makeinc.linux86_64


MI_INCL = $(INFORMIXDIR)/incl
CFLAGS = -DMI_SERVBUILD $(CC_PIC) -I$(MI_INCL)/public $(COPTS)
LINKFLAGS = $(SHLIBLFLAG) $(SYMFLAG) $(LD_SHARED_FLAGS)

all: ix_coalesce

clean:
        rm *.udr *.o

ix_coalesce: ix_coalesce.udr
        @echo "Library genaration done"

ix_coalesce.o: ix_coalesce.c
        @echo "Compiling..."
        $(CC) -c $(CFLAGS) -o $@ $?

ix_coalesce.udr: ix_coalesce.o
        @echo "Creating the library..."
        $(SHLIBLOD) $(LINKFLAGS) -o $@ $? 
 
No final, após executar o comando make devemos ter uma biblioteca dinâmica que podemos usar para criar a nossa função ao nível do SQL. O processo de make é simples:
tpch@kimball:informix-> make
Compiling...
cc -c -DMI_SERVBUILD -fPIC -I/opt/informix/srvr1170fc5/incl/public -g -o ix_coalesce.o ix_coalesce.c
Creating the library...
gcc -shared -m64 -Bsymbolic -shared -m64 -o ix_coalesce.udr ix_coalesce.o
Library genaration done

tpch@kimball:informix-> ls -lia ix_coalesce.udr
1654803 -rwxr-xr-x 1 informix informix 8273 Aug 13 00:00 ix_coalesce.udr

Criando a função em SQL
Após termos compilado o código e produzido a biblioteca dinâmica necessitamos de criar a função em SQL, referenciando a função em C.
Isto é feito com este simples código SQL:
DROP FUNCTION IF EXISTS coalesce_udr;
CREATE FUNCTION coalesce_udr(
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL,
        INTEGER DEFAULT NULL
) RETURNING INTEGER
WITH (NOT VARIANT, HANDLESNULLS)
EXTERNAL NAME '/opt/informix/work/coalesce/ix_coalesce.udr(ix_coalesce)'
LANGUAGE C;

Algumas notas:
  1. Estou a definir uma UDR que recebe INTEGERs como argumentos. Internamente serão tratados como LVARCHARs. Se necessitarmos de COALESCE para outros tipos de dados podíamos criar outras funções, com o mesmo nome mas diferentes tipos de parâmetros. A isto chama-se overloading e é perfeitamente suportado em Informix
  2. Usei a opção HANDLESNULLS porque sem ela, se usarmos NULL num argumento a função não é chamada e o retorno será NULL
  3. EXTERNAL NAME indica o caminho (path) da função em C. Neste caso é o caminho para a biblioteca dinâmica criada anteriormente e o nome da função (dentro da biblioteca) entre parênteses

Testes e comparação de tempos
Portanto, o objetivo é comparar várias formas de contornar a falta de um COALESCE nativo. As alternativas consideradas foram:
  1. Usar NVL() encadeados diretamente nas instruções SQL. Esta é a mais rápida e será considerada como referência
  2. Criar uma SPL chamada coalesce_basic que recebe argumentos e que contém a estrutura de NVL encadeados no seu código
  3. Criar uma SPL chamada coalesce_if semelhante à anterior, mas que em vez dos NVL() encadeados tem uma estrutura de IFs
  4. O código C explicado neste artigo

Pode encontrar o código para todas estas alternativas no final do artigo. Depois de criar as funções criei um conjunto de dados de teste, composto de 10M de linhas com 10 campos cada uma.
Cada linha tem apenas um campo não NULL. E o campo não NULL muda sequencialmente em cada linha. Utilizei um script AWK para criar os dados, e este script encontra-se também no final do artigo.

Carreguei o ficheiro numa tabela e executei um UNLOAD só para garantir que os dados desta tabela são colocados em memória (na buffer cache). Após tudo isto executo o UNLOAD novamente, usando as 10 colunas como argumentos para as várias alternativas acima. O UNLOAD é efectuado para /dev/null para minimizar os tempos de I/O. Mais uma vez, o código SQL está incluído no final. Para cada alternativa insiro numa tabela temporária o tempo inicial e final. No final executo uma instrução que fornece a comparação de tempos para cada alternativa vs a instrução SQL com os NVL() encadeados.

Executei o procedimento várias vezes para verificar se os resultados eram semelhantes, o que se confirmou. E os tempos obtidos são semelhantes aos seguintes (dois exemplos):

using  Nested NVL:        0 00:00:35.48369 100.00%
using  Nested NVL in SPL: 0 00:02:47.96074 473.34%
using  IF based in SPL:   0 00:03:27.54880 584.91%
using  COALESCE_UDR:      0 00:00:47.85661 134.86%


using  Nested NVL:        0 00:00:35.31295 100.00%
using  Nested NVL in SPL: 0 00:02:45.31578 468.14%
using  IF based in SPL:   0 00:03:26.77770 585.55%
using  COALESCE_UDR:      0 00:00:47.78794 135.32%


Conclusões

As conclusões são bastante óbvias, e os tempos falam por si, mas vou enumerá-las:
  1. Não há alternativa mais rápida que os NVL() encadeados. Portanto se a única preocupação é a performance é esta que deve usar. O obstáculo óbvio é que o código parecerá "estranho" para utilizadores de outras bases de dados e não será uma boa solução se está a portar uma aplicação ou query.
  2. NVL() encadeados dentro de um procedimento em SPL tem um enorme impacto de performance. Note que o código é semelhante ao anterior, mas corre dentro de uma SPL, por isso terá de haver chamadas à função
  3. O procedimento SPL com IFs é ainda mais lento que o anterior
  4. A performance da UDR C que criámos não é de todo muito má... Sim, vemos algum impacto, entre 30 a 40%, mas será isso significativo? Note-se que no caso de teste estamos a chamar a função 10M de vezes. E o impacto foi de 13s. Note-se ainda que o teste foi pensado para aceder a dados que estão em memória, e que não estamos a avaliar nenhuma cláusula WHERE. Ou seja, parece um bom compromisso entre flexibilidade e performance. Como é hábito a UDR em C é bastante rápida






1    /*
2    ------------------------------------------
3     include section
4    ------------------------------------------
5    */
6    #include <stdio.h>
7    #include <milib.h>
8    #include <sqlhdr.h>
9
10   // Define as much i* as the maximum number of arguments wanted
11   mi_lvarchar * ix_coalesce(
12      mi_lvarchar *i0,mi_lvarchar *i1,mi_lvarchar *i2,
13      mi_lvarchar *i3,mi_lvarchar *i4,mi_lvarchar *i5,
14      mi_lvarchar *i6,mi_lvarchar *i7,mi_lvarchar *i8,
15      mi_lvarchar *i9, MI_FPARAM *fParam)
16   {
17   int arg_count,a;
18
19      /* Adjust the code to the number of parameters */
20      arg_count = mi_fp_nargs(fParam);
21
22      // loop through the arguments...
23      for(a=0;a<arg_count ;a++)
24      {
25              if ( mi_fp_argisnull(fParam, a) == MI_FALSE )
26              {
27                      switch(a)
28                      {
29                              case 0:
30                                      return(i0);
31                                      ;;
32                              case 1:
33                                      return(i1);
34                                      ;;
35                              case 2:
36                                      return(i2);
37                                      ;;
38                              case 3:
39                                      return(i3);
40                                      ;;
41                              case 4:
42                                      return(i4);
43                                      ;;
44                              case 5:
45                                      return(i5);
46                                      ;;
47                              case 6:
48                                      return(i6);
49                                      ;;
50                              case 7:
51                                      return(i7);
52                                      ;;
53                              case 8:
54                                      return(i8);
55                                      ;;
56                              case 9:
57                                      return(i9);
58                                      ;;
59                      }
60              }
61      }
62      mi_fp_setreturnisnull(fParam, 0, MI_TRUE);
63      return NULL;
64   }
 
AWK script (echo "10000000 10 test_coalesce.unl" | awk -f gen_data.awk):
 
#-------------------------------------------------------
BEGIN { cycle = 1 }
{
        TOP_LIMIT=$1;
        NUM_FIELDS=$2;
        FILE=$3;

        for (a=1;a<=TOP_LIMIT;a++)
        {
                LINE="";
                for (b=1;b<=NUM_FIELDS;b++)
                        if ( b == cycle)
                                LINE=LINE a "|";
                        else
                                LINE=LINE "|";
                cycle++;
                if ( cycle > NUM_FIELDS )
                        cycle=1;
                print LINE >> FILE
        }
}
#-------------------------------------------------------

Functions SQL 
SQL de criação das funções:

DROP FUNCTION IF EXISTS coalesce_basic;
CREATE FUNCTION coalesce_basic (
        i1 INTEGER DEFAULT NULL, i2 INTEGER DEFAULT NULL,
        i3 INTEGER DEFAULT NULL, i4 INTEGER DEFAULT NULL,
        i5 INTEGER DEFAULT NULL, i6 INTEGER DEFAULT NULL,
        i7 INTEGER DEFAULT NULL, i8 INTEGER DEFAULT NULL,
        i9 INTEGER DEFAULT NULL, i10 INTEGER DEFAULT NULL
) RETURNING INTEGER;

        RETURN NVL(NVL(NVL(NVL(NVL(NVL(NVL(NVL(NVL(i1,i2),i3),i4),i5),i6),i7),i8),i9),i10);
END FUNCTION;


DROP FUNCTION IF EXISTS coalesce_if;
CREATE FUNCTION coalesce_if (
        i1 INTEGER DEFAULT NULL, i2 INTEGER DEFAULT NULL,
        i3 INTEGER DEFAULT NULL, i4 INTEGER DEFAULT NULL,
        i5 INTEGER DEFAULT NULL, i6 INTEGER DEFAULT NULL,
        i7 INTEGER DEFAULT NULL, i8 INTEGER DEFAULT NULL,
        i9 INTEGER DEFAULT NULL, i10 INTEGER DEFAULT NULL
) RETURNING INTEGER;

        IF ( i1 IS NOT NULL) THEN
                RETURN i1;
        END IF;
        IF ( i2 IS NOT NULL) THEN
                RETURN i2;
        END IF;
        IF ( i3 IS NOT NULL) THEN
                RETURN i3;
        END IF;
        IF ( i4 IS NOT NULL) THEN
                RETURN i4;
        END IF;
        IF ( i5 IS NOT NULL) THEN
                RETURN i5;
        END IF;
        IF ( i6 IS NOT NULL) THEN
                RETURN i6;
        END IF;
        IF ( i7 IS NOT NULL) THEN
                RETURN i7;
        END IF;
        IF ( i8 IS NOT NULL) THEN
                RETURN i8;
        END IF;
        IF ( i9 IS NOT NULL) THEN
                RETURN i9;
        END IF;
        RETURN i10;
END FUNCTION;


Test run (coalesce.sql)
Execução do teste (coalesce.sql):
 
DROP TABLE IF EXISTS test_coalesce;
CREATE RAW TABLE test_coalesce
(
        col1 INTEGER,
        col2 INTEGER,
        col3 INTEGER,
        col4 INTEGER,
        col5 INTEGER,
        col6 INTEGER,
        col7 INTEGER,
        col8 INTEGER,
        col9 INTEGER,
        col10 INTEGER
) EXTENT SIZE 50000 NEXT SIZE 50000 LOCK MODE ROW;

-- Load the test data / Carregar os dados de teste
LOAD FROM test_coalesce.unl INSERT INTO test_coalesce;
ALTER TABLE test_coalesce TYPE (standard);



-- Force data to cache / colocar os dados em memória
UNLOAD TO /dev/null SELECT * FROM test_coalesce;

-- Create execution times table / Criar tabela de recolha de tempos
DROP TABLE IF EXISTS timmings;
CREATE TABLE timmings
(
        method VARCHAR(30),
        start DATETIME YEAR TO FRACTION(5),
        end DATETIME YEAR TO FRACTION(5)
);

-- Using nested NVL() / Utilizacao de NVL() encadeados
INSERT INTO timmings (method, start) VALUES ( 'Nested NVL', CURRENT YEAR TO FRACTION(5));
UNLOAD TO /dev/null
SELECT
nvl(nvl(nvl(nvl(nvl(nvl(nvl(nvl(nvl(col1,col2),col3),col4),col5),col6),col7),col8),col9),col10)
FROM test_coalesce;
UPDATE timmings SET end = CURRENT YEAR TO FRACTION(5) WHERE method = 'Nested NVL';

-- Using the nested NVL procedure / Utilizacao do procedimento com os NVL() encadeados
INSERT INTO timmings (method, start) VALUES ( 'Nested NVL in SPL', CURRENT YEAR TO FRACTION(5));
UNLOAD TO /dev/null
SELECT
COALESCE_BASIC(col1,col2,col3,col4,col5,col6,col7,col8,col9,col10)
FROM test_coalesce;
UPDATE timmings SET end = CURRENT YEAR TO FRACTION(5) WHERE method = 'Nested NVL in SPL';

-- Using the IF based SPL / Utilizacao da SPL baseada em IFs
INSERT INTO timmings (method, start) VALUES ( 'IF based in SPL', CURRENT YEAR TO FRACTION(5));
UNLOAD TO /dev/null
SELECT
COALESCE_IF(col1,col2,col3,col4,col5,col6,col7,col8,col9,col10)
FROM test_coalesce;
UPDATE timmings SET end = CURRENT YEAR TO FRACTION(5) WHERE method = 'IF based in SPL';

-- Using this article COALESCE UDR / Utilizacao da funcao UDR criada no artigo 
INSERT INTO timmings (method, start) VALUES ( 'COALESCE_UDR', CURRENT YEAR TO FRACTION(5));
UNLOAD TO /dev/null
SELECT
COALESCE_UDR(col1,col2,col3,col4,col5,col6,col7,col8,col9,col10)
FROM test_coalesce;
UPDATE timmings SET end = CURRENT YEAR TO FRACTION(5) WHERE method = 'COALESCE_UDR';


-- Auxiliary function to calculate the relative percentage / Funcao auxiliar para calculo das percentagens relativas
DROP FUNCTION IF EXISTS interval_to_second;
CREATE FUNCTION interval_to_second(i INTERVAL MINUTE TO FRACTION(5)) RETURNING DECIMAL(10,5);

DEFINE v_hours, v_minutes, v_seconds SMALLINT;
DEFINE v_fraction DECIMAL(10,5);
LET v_hours = 0;
LET v_minutes = SUBSTR(i::CHAR(12), CHARINDEX(':',i::CHAR(12))-2,2);
LET v_seconds = SUBSTR(i::CHAR(12), CHARINDEX(':',i::CHAR(12))+1, 2);
LET v_fraction = "0." || SUBSTR(i::CHAR(12), CHARINDEX(':',i::CHAR(12))+4, 5);

RETURN v_hours * 3600 + v_minutes * 60 + v_seconds + v_fraction;
END FUNCTION;

SELECT
        method || ': ' || (end - start) || ' ' || TRUNC(
                (
                        interval_to_second((end - start)::INTERVAL MINUTE TO FRACTION(5))
                        /
                        (SELECT interval_to_second((t1.end - t1.start)::INTERVAL MINUTE TO FRACTION(5)) FROM timmings t1 where t1.method = 'Nested NVL')
                        )*100
                ,2) ||'%' as Using
FROM
        timmings; 



Thursday, December 22, 2011

Small query performance analysis / Pequena análise de performance de querys

This article is written in English and Portuguese
Este artigo está escrito em Inglês e Português

English version:

The need...

The end of the year is typically a critical time for IT people. Following up on last article's I'm still working with performance issues. "Performance issues on Informix?!" I hear you say... Well yes, but to give you an idea of the kind of system I'm talking about I can say that recently we noticed three small tables (between 3 and 60 rows) that between mid-night and 11AM were scanned 33M times. To save you the math, that's around 833 scans/queries for each of these tables per second. And this started to happen recently, on top of the normal load that nearly 3000 sessions can generate...
So, the point is: every bit of performance matters. And in most cases, on this system there are no long running queries. It's mostly very short requests made an incredible number of times. And yes, this makes the DBA life harder... If you have long running queries with bad query plans they're usually easy to spot. But if you have a large number of very quick queries, but with questionable query plans, than it's much more difficult to find.

Just recently I had one of this situations. I've found a query with a questionable query plan. The query plan varies with the arguments and both possible options have immediate response times (fraction of a second). That's not the first time I've found something similar, and most of the times I face the same situation twice I usually decide I need to have some tool to help me on that.

The idea!

The purpose was to see the difference in the work the engine does between two query plans. And when I say "tool" I'm thinking about a script. Last time I remember having this situation, I used a trick in dbaccess to obtain the performance counters for both the session, and the tables involved. Some of you probably know, others may not, but when dbaccess parses an SQL script file it can recognize a line starting with "!" as an instruction to execute the rest of the line as a SHELL command. So basically what I did previously was to customize the SQL script containing the query like this:

!onstat -z
SELECT .... FROM .... WHERE ...
!some_shell_scritpt

where some_shell_script had the ability to find the session and run an onstat -g tpf and also an onstat -g ppf. These two onstat commands show us a lot of performance counters respectively from the threads (tpf) and from the partitions (ppf). The output looks like:


IBM Informix Dynamic Server Version 11.70.UC4 -- On-Line -- Up 7 days 23:42:15 -- 411500 Kbytes

Thread profiles
tid lkreqs lkw dl to lgrs isrd iswr isrw isdl isct isrb lx bfr bfw lsus lsmx seq
24  0      0   0  0  0    0    0    0    0    0    0    0  0   0   0    0    0  
26  0      0   0  0  0    0    0    0    0    0    0    0  95  95  0    0    0  
51  32917  0   0  0  21101 13060 3512 57   532  3795 0    0  91215 29964 0    125008 4226
52  39036  0   0  0  9099 11356 2648 80   1372 265  0    0  45549 9312 0    244900 21 
49  705    0   0  0  574  8938 0    139  0    139  0    0  22252 148 0    5656 541
2444 706    0   0  0  14   344  0    4    0    0    3    0  819 7   136  224  0  

This tells us the thread Id, lock requests, lock waits, deadlocks, timeouts, logical log records, isam calls (read, write, rewrite, delete, commit and rollback), long transactions, buffer reads and writes, logical log space used, logical log space maximum and sequential scans.
And this:

panther@pacman.onlinedomus.com:informix-> onstat -g ppf | grep -v "0     0     0     0     0     0     0     0     0     0     0     0"

IBM Informix Dynamic Server Version 11.70.UC4 -- On-Line -- Up 7 days 23:43:41 -- 411500 Kbytes

Partition profiles
partnum    lkrqs lkwts dlks  touts isrd  iswrt isrwt isdel bfrd  bfwrt seqsc rhitratio
0x100001   0     0     0     0     0     0     0     0     13697 0     0     100
0x100002   993   0     0     0     445   0     0     0     1460  0     2     100
0x10002d   6769  0     0     0     2379  34    340   34    9094  581   2     100
0x10002e   164   0     0     0     166   0     0     0     472   0     2     100
0x10002f   2122  0     0     0     2750  0     0     0     5288  0     0     100
0x100030   0     0     0     0     4     0     0     0     700   0     4     100
0x100034   14192 0     0     0     5922  192   80    192   15566 1274  0     100
0x100035   2260  0     0     0     188   80    0     80    2766  655   4     100
0x100036   1350  0     0     0     548   34    0     34    1872  249   0     100
0x100037   80    0     0     0     16    4     0     4     346   28    0     100
0x100038   4720  0     0     0     738   360   0     360   3734  1557  0     100

which tells us some of the above, but for each partition.
Note that I reset the counters, run the query and then obtain the profile counters. Ideally, nothing else should be running on the instance (better to do it on a test instance)

Sharing it

But I decided to make this a bit easier and I created a script for doing it. I'm also using this article to announce that starting today, I'll try to keep my collection of scripts on a publicly available site:

http://onlinedomus.com/informix/viewvc.cgi

This repository contains a reasonable amount of scripts for several purposes. Ideally I should create proper documentation and use cases for each one of them, but I currently don't have that. It's possible I'll cover some of them here in the blog, but probably only integrated in a wider article (like this one).

These scripts were created by me (with one exception - setinfx was created by Eric Vercelleto when we were colleagues in Informix Portugal and we should thank him for allowing the distribution), during my free time and should all contain license info (GPL 2.0). This means you can use them, copy them, change them etc. Some of them are very old and may not contain this info.
Some fixes and improvements were done during project engagements. Many of them were based on ideas I got from some scripts available in IIUG's software repository or from colleagues ideas, problems and suggestions (Thanks specially to António Lima and Adelino Silva)

It's important to notice that the scripts are available "as-is", no guarantees are made and I cannot be held responsible for any problem that it's use may cause.
Having said that, I've been using most of them on several customers for years without problems.
Any comments and/or suggestions are very welcome, and if I find the suggestions interesting and they don't break the script's ideas and usage, I'll be glad to incorporate them on future versions.

Many of the scripts have two option switches that provide basic help (-h) and version info (-V).
If by any chance you are using any of these scripts I suggest you check the site periodically to find any updates. I try my best to maintain retro-compatibility and old behavior when I make changes on them.

Back to the problem

So, this article focus on analyzing and comparing the effects of running a query with two (or more) different query plans. The script created for this was ixprofiling. If you run it with -h (help) option it will print:


panther@pacman.onlinedomus.com:fnunes-> ./ixprofiling -h
ixprofiling [ -h | -V ]
            -s SID database
            [-z|-Z|-n] database sql_script
     -h             : Get this help
     -V             : Get script version
     -s SID database: Get stats for session (SID) and database
     -n             : Do NOT reset engine stats
     -z             : Reset engine stats using onstat (default - needs local database)
     -Z             : Reset engine stats using SQL Admin API (can work remotely )

Let's see what the options do:
  • -s SID database
    Shows the info similar to onstat -g tpf (for the specified session id) and onstat -g ppf (for the specified database)
    It will show information for all the partition objects in the specified database for which any of the profile counters is different from zero. Note that when I write partition, it can be a table, a table's partition or a table's index.
  • database sql_script
    Runs the specified SQL script after making some changes that will (by default) reset the engine profile counters (-z option). See more information about the SQL script below
  • -n
    Prevents the reset of profile counters (if you're not a system database administrator you'll need to specify this to avoid errors)
  • -z
    Resets the profile counters using onstat -z. This is the quickest and most simple way to do it but will need local database access.
  • -Z
    Resets the counters using SQL admin API, so it can be used on remote databases


And now let's see an usage example. The script has some particularities that need to be detailed.
First, since the idea is to compare two or more query plans we can put all the variations inside the SQL script, separating them by a line like:

-- QUERY

when the script finds these lines, it will automatically get the stats (from the previous query) and reset the counters to prepare for the next query. If you use just one query you don't need this, since by default it will reset the counters at the beginning and show the stats at the end.
If you put two or more queries on the script don't forget to end each query with ";" or it will break the functionality.
Let's see a practical example. I have a table with the following structure:

create table ibm_test_case 
  (
    col1 integer,
    col2 smallint not null ,
    col3 integer,
    col4 integer,
[... irrelevant bits... ]
    col13 datetime year to second,
[... more irrelevant bits... ]
  );

create index ix_col3_col13 on ibm_test_case (col3,col13) using btree ;
create index ix_col4 on ibm_test_case (col4) using btree ;

and a query like:

select c.col1
from ibm_test_case c
where
        c.col3 = 123456789 and
        c.col4 = 1234567 and
        c.col13 = ( select max ( c2.col13 ) from ibm_test_case c2
                where
                        c2.col3 = c.col3 and
                        c2.col4 = c.col4
                );


The problem is the query plan for the sub-query. It can choose between an index headed by col3 and another on col4. So I create a test_case.sql with:


unload to /dev/null select c.col1
from ibm_test_case c
where
        c.col3 = 123456789 and
        c.col4 = 1234567 and
        c.col13 = ( select max ( c2.col13 ) from ibm_test_case c2
                where
                        c2.col3 = c.col3 and
                        c2.col4 = c.col4
                );

-- QUERY
unload to /dev/null select c.col1
from ibm_test_case c
where
        c.col3 = 123456789 and
        c.col4 = 1234567 and
        c.col13 = ( select --+ INDEX ( c2 ix_col3_col13 )
                max ( c2.col13 ) from ibm_test_case c2
                where
                        c2.col3 = c.col3 and
                        c2.col4 = c.col4
                );

Note that on the second query I'm forcing the use of a particular index.
Then we run:

ixprofiling stores test_case.sql


and we get the following output:


Database selected.

Engine statistics RESETed. Query results:

Explain set.


1 row(s) unloaded.


Thread profiles (SID: 2690)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
5224  0     0     0     0     2611  0     0     0     2646  0     0     0     0     0     0     0     2170  0.051671256 sqlexec     

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
6     0     0     0     0     0     2     0     0     0     10    0     0     systables
2609  0     0     0     1933  0     2607  0     0     0     2609  0     0     ibm_test_case
1     0     0     0     2     0     1     0     0     0     6     0     0     ibm_test_case#ix_col3_col13
2608  0     0     0     3     0     1     0     0     0     21    0     0     ibm_test_case#ix_col4
Engine statistics RESETed. Query results:

1 row(s) unloaded.


Thread profiles (SID: 2690)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
17    0     0     0     0     6     0     0     0     31    0     0     0     0     0     0     0     188   0.003161049 sqlexec     

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
6     0     0     0     0     0     2     0     0     0     10    0     0     systables
4     0     0     0     2     0     1     0     0     0     4     0     0     ibm_test_case
7     0     0     0     0     0     6     0     0     0     17    0     0     ibm_test_case#ix_col3_col13

So, we can now analyze the differences. As you can see the output is more friendly than the output from onstat. On the session section we can see the usual counters, plus the number of times the engine scheduled the thread(s) to run, the CPU time consumed and the name of the threads.
On the tables/partitions section, we can find the partition, table or index name in a friendly nomenclature (instead of the partnum).
As for the comparison, you can spot a big difference. Much more buffer reads and ISAM reads for the first query plan and also a bigger CPU time. Be aware however that for very fast queries the CPU times may show very big variance so don't assume a lower CPU time is always associated with the better query plan. You should repeat the test many times to see the oscillations.
Also note that the meaning of ISAM calls is many times misunderstood. Some people think it's the number of "SELECTs", others the number of rows returned... In reality it's the number of internal functions calls. Some engine settings like BATCHEDREAD_TABLE and BATCHEDREAD_INDEX may influence the number of calls for the same query and query result.

That's all for now. I leave you with the repository and hopefully future articles will focus on some of these scripts. Feel free to use them and send me you suggestions.

Versão Portuguesa:


A necessidade...

O fina do ano é tipicamente uma altura critica para os informáticos. Continuando no mesmo tema do último artigo, continuo a trabalhar com problemas de performance. "Problemas de performance em Informix?!" poderão estar a pensar... Bem, sim, mas para vos dar uma ideia do sistema sobre o qual estou a falar, posso dizer que recentemente notámos três pequenas tabelas (entre 3 e 60 linhas) que entre a meia-noite e as onze da manhã eram varridas (sequential scan) 33M de vezes. Para poupar nas contas posso já dizer que dá cerca de 833 scans/queries por segundo para cada uma das tabelas. E isto começou a acontecer recentemente sobre a carga "normal" que perto de 3000 sessões podem criar.
Portanto, a ideia é que cada bocadinho de performance tem impacto. Na maioria dos casos, este sistema não tem queries longas. Na maior parte das vezes os problemas são pedidos com curta duração mas feitos um imenso número de vezes. E sim, isto torna a vida dos DBAs mais dicfícil... Se tivermos queries longas com maus planos de execução são normalmente fáceis de identificar. Mas se tivermos um grande número de queries muito curtas, com um plano de execução questionável, isso é muito mais difícil de encontrar.

Ainda recentemente tive uma dessas situações. Detectei uma query com um plano de execução duvidoso. O plano de execução varia com os parâmetros usados e ambas as alternativas têm um tempo de resposta "imediato" (fracção de segundo). Não foi a primeira vez que encontrei algo semelhante, e na maioria dos casos em que enfrento uma situação duas vezes, normalmente decido que preciso de alguma ferramenta que me ajude no futuro.

A ideia!


O objectivo era evidenciar a diferença no trabalho feito pelo motor entre dois planos de execução. E quando refiro "ferramenta" estou a pensar num script. A última vez que me lembro de ter tido uma situação destas  usei um truque no dbaccess para obter os indicadores de performance tanto para a sessão como para as tabelas envolvidas.
Alguns de vós saberão, outros não, mas quando o dbaccess lê um scritpt SQL pode reconhecer uma linha começada com "!" como uma instrução para executar o resto da linha como um comando SHELL. Assim, o que fiz em situações anteriores foi alterar o script SQL que continha a query para algo do género:

!onstat -z
SELECT .... FROM .... WHERE ...
!um_shell_scritpt

onde um_shell_script tem a capacidade de encontrar a sessão e correr um onstat -g tpf e também um onstat -g ppf. Ests dois comandos mostram-nos uma série de contadores de performance respectivamente da sessão/thread (tpf) e das partições (ppf). O output é semelhante a isto:

IBM Informix Dynamic Server Version 11.70.UC4 -- On-Line -- Up 7 days 23:42:15 -- 411500 Kbytes

Thread profiles
tid lkreqs lkw dl to lgrs isrd iswr isrw isdl isct isrb lx bfr bfw lsus lsmx seq
24  0      0   0  0  0    0    0    0    0    0    0    0  0   0   0    0    0  
26  0      0   0  0  0    0    0    0    0    0    0    0  95  95  0    0    0  
51  32917  0   0  0  21101 13060 3512 57   532  3795 0    0  91215 29964 0    125008 4226
52  39036  0   0  0  9099 11356 2648 80   1372 265  0    0  45549 9312 0    244900 21 
49  705    0   0  0  574  8938 0    139  0    139  0    0  22252 148 0    5656 541
2444 706    0   0  0  14   344  0    4    0    0    3    0  819 7   136  224  0  

É-nos mostrado o ID da thread, número de pedidos de lock, esperas em locks, deadlocks, lock timeouts, chamadas ISAM (leitura, escrita, re-escrita, apagar, commit e rollback), transacções longas, leituras e escritas de buffers, espaço usado em logical logs e máximo espaço usado em logical logs e número de sequential scans. E isto:

panther@pacman.onlinedomus.com:informix-> onstat -g ppf | grep -v "0     0     0     0     0     0     0     0     0     0     0     0"

IBM Informix Dynamic Server Version 11.70.UC4 -- On-Line -- Up 7 days 23:43:41 -- 411500 Kbytes

Partition profiles
partnum    lkrqs lkwts dlks  touts isrd  iswrt isrwt isdel bfrd  bfwrt seqsc rhitratio
0x100001   0     0     0     0     0     0     0     0     13697 0     0     100
0x100002   993   0     0     0     445   0     0     0     1460  0     2     100
0x10002d   6769  0     0     0     2379  34    340   34    9094  581   2     100
0x10002e   164   0     0     0     166   0     0     0     472   0     2     100
0x10002f   2122  0     0     0     2750  0     0     0     5288  0     0     100
0x100030   0     0     0     0     4     0     0     0     700   0     4     100
0x100034   14192 0     0     0     5922  192   80    192   15566 1274  0     100
0x100035   2260  0     0     0     188   80    0     80    2766  655   4     100
0x100036   1350  0     0     0     548   34    0     34    1872  249   0     100
0x100037   80    0     0     0     16    4     0     4     346   28    0     100
0x100038   4720  0     0     0     738   360   0     360   3734  1557  0     100

que nos mostra alguns dos contadores anteriores, mas por partição.
Note-se que re-inicializo os contadores, corro a query e depois obtenho os outputs. Idealmente não deverá estar mais nada a correr na instância (é preferível usar uma instância de teste).

Partilha


Mas decidi tornar isto um pouco mais fácil e criei um script para o fazer. Estou também a usar este artigo para anunciar que a partir de hoje, tentarei manter a minha colecção de scripts disponível num site público:

http://onlinedomus.com/informix/viewvc.cgi

Este repositório contém uma quantidade razoável de scripts e outras ferramentas úteis para várias tareafas. Idealmente eu deveria criar documentação e casos de uso para cada um deles, mas de momento isso não está feito. É possível que vá descrevendo alguns destes scripts em futuros artigos, mas sempre integrados em assuntos mais vastos (como este)

Estes scripts foram criados por mim (com uma excepção - setinfx foi criado por Eric Vercelletto quando éramos colegas na Informix Portugal e devemos agradecer-lhe por permitir a distribuição), durante os meus tempos livres e devem conter informação de licenciamento (GPL 2.0). Isto quer dizer que podem ser usados, distribuidos, alterados etc.). Alguns podem não ter esta informação por serem muito antigos.
Naturalmente algumas correcções e melhorias foram feitas durante projectos em clientes, sempre que detecto algum erro ou hipótese de melhoria no seu uso. Muitos deles foram baseados em ideias que obtive de scripts existents no repositório do IIUG, ou de ideias, problemas e sugestões de colegas (agradecimento especial ao António Lima e ao Adelino Silva)

É importante avisar que os scripts são disponiblizados "como são", sem qualquer tipo de garantia implicita ou explicita e eu não posso ser responsabilizado por qualquer problema que advenha do seu uso. Posto isto, convém também dizer que a maioria dos scripts têm sido usados por mim em clientes ao longo de anos, sem problemas.

Quaisquer comentários e/ou sugestões são bem vindas, e se os achar interessantes terei todo o prazer em os incorportar em futuras versões (desde que não fujam à lógica e utilização do script)
Muitos destes scripts disponibilizam duas opções que fornecem ajuda básica (-h) e informação sobre a versão (-V).
Se utilizar algum destes scripts no seu ambiente, sugiro que verifique periodicamente se houve correcções ou melhorias, consultando o site com alguma regularidade. Sempre que possível evito que novas funcionalidades alterem o comportamento do script.

De volta ao problema

Este artigo foca a análise e comparação dos efeitos de executar uma query com dois (ou mais) planos de execução. O script criado para isso chama-se ixprofiling. Se corrido com a opção -h (help) mostra-nos:

panther@pacman.onlinedomus.com:fnunes-> ./ixprofiling -h
ixprofiling [ -h | -V ]
            -s SID database
            [-z|-Z|-n] database sql_script
     -h             : Get this help
     -V             : Get script version
     -s SID database: Get stats for session (SID) and database
     -n             : Do NOT reset engine stats
     -z             : Reset engine stats using onstat (default - needs local database)
     -Z             : Reset engine stats using SQL Admin API (can work remotely )

Vejamos o que fazem as opções:
  • -s SID base_dados
    Mostra informação semelhante ao onstat -g tpf (para a sessão indicada por SID) e onstat -g ppf (para a base de dados indicada)
    Irá mostrar informação para todas as partições na base de dados escolhida, para as quais exista pelo menos um dos contadores com valor diferente de zero. Note-se que quando refiro partição estou a referir-me a uma tabela, a um fragmento de tabela fragmentada (ou se preferir particionada) ou a um indíce.
  • base_dados script_sql
    Corre o script SQL indicado, fazendo alterações que irão (por omissão), re-inicializar os contadores de performance do motor (opção -z). Veja mais informação sobre o script SQL abaixo
  • -n
    Evita a re-inicialização dos contadores de performance (se não fôr administrador do sistema de base de dados terá de usar esta opção para evitar erros)
  • -z
    Faz a re-inicialização dos contadores do motor usando o comando onstat -z. Esta é a forma mais simples e rápida de o fazer, mas requer que a base de dados seja local
  • -Z
    Faz a re-inicialização dos contadores utilizando a SQL Admin API, de forma que possa ser feito com bases de dados remotas

E agora vejamos um exemplo de uso. O script tem algumas particularidades que merecem ser datalhadas.
Primeiro e porque a ideia é comparar dois ou mais planos de execução, podemos colocar todas as variantes de plano de execução  dentro do mesmo script SQL usando uma linha como esta para separar as queries:

-- QUERY

Estas linhas são automaticamente substituídas por comandos que obtêm os contadores actuais (da query anterior) e que re-inicializam os mesmos contadores preparando a execução seguinte. Se usar apenas uma query não é necessário isto, pois por omissão a re-inicialização dos contadores é feita no início, e após a última query são automaticamente mostrados os contadores.
Se colocar duas ou mais queries no script não se esqueça de terminar cada uma com ";" ou o script não funcionará como esperado.
Vamos ver um exemplo prático. Tenho uma tabela com a seguinte estrutura:

create table ibm_test_case 
  (
    col1 integer,
    col2 smallint not null ,
    col3 integer,
    col4 integer,
[... parte irrelevante ... ]
    col13 datetime year to second,
[... mais colunas irrelevantes ... ]
  );

create index ix_col3_col13 on ibm_test_case (col3,col13) using btree ;
create index ix_col4 on ibm_test_case (col4) using btree ;

e uma query com:

select c.col1
from ibm_test_case c
where
        c.col3 = 123456789 and
        c.col4 = 1234567 and
        c.col13 = ( select max ( c2.col13 ) from ibm_test_case c2
                where
                        c2.col3 = c.col3 and
                        c2.col4 = c.col4
                );


O problem é o plano de execução da sub-query. Pode  escolher entre um índice começado pela coluna col3 e outro pela coluna col4. Por isso crio um ficheiro, caso_teste.sql com:


unload to /dev/null select c.col1
from ibm_test_case c
where
        c.col3 = 123456789 and
        c.col4 = 1234567 and
        c.col13 = ( select max ( c2.col13 ) from ibm_test_case c2
                where
                        c2.col3 = c.col3 and
                        c2.col4 = c.col4
                );

-- QUERY
unload to /dev/null select c.col1
from ibm_test_case c
where
        c.col3 = 123456789 and
        c.col4 = 1234567 and
        c.col13 = ( select --+ INDEX ( c2 ix_col3_col13 )
                max ( c2.col13 ) from ibm_test_case c2
                where
                        c2.col3 = c.col3 and
                        c2.col4 = c.col4
                );

Repare  que na segunda query estou a forçar o uso de um determinado índice:
Depois corro:

ixprofiling stores caso_teste.sql


e obtemos o seguinte:


Database selected.

Engine statistics RESETed. Query results:

Explain set.


1 row(s) unloaded.


Thread profiles (SID: 2690)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
5224  0     0     0     0     2611  0     0     0     2646  0     0     0     0     0     0     0     2170  0.051671256 sqlexec     

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
6     0     0     0     0     0     2     0     0     0     10    0     0     systables
2609  0     0     0     1933  0     2607  0     0     0     2609  0     0     ibm_test_case
1     0     0     0     2     0     1     0     0     0     6     0     0     ibm_test_case#ix_col3_col13
2608  0     0     0     3     0     1     0     0     0     21    0     0     ibm_test_case#ix_col4
Engine statistics RESETed. Query results:

1 row(s) unloaded.


Thread profiles (SID: 2690)
LkReq LkWai DLks  TOuts LgRec IsRd  IsWrt IsRWr IsDel BfRd  BfWrt LgUse LgMax SeqSc Srts  DskSr SrtMx Sched CPU Time    Name        
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----------- ------------ 
17    0     0     0     0     6     0     0     0     31    0     0     0     0     0     0     0     188   0.003161049 sqlexec     

Partitions profiles (Database: stores)
LkReq LkWai DLks  TOuts DskRd DskWr IsRd  IsWrt IsRWr IsDel BfRd  BfWrt SeqSc Object name                                           
----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ----- ------------------------------------------------------
6     0     0     0     0     0     2     0     0     0     10    0     0     systables
4     0     0     0     2     0     1     0     0     0     4     0     0     ibm_test_case
7     0     0     0     0     0     6     0     0     0     17    0     0     ibm_test_case#ix_col3_col13

Então, podemos agora analisar as diferenças. Como se pode ver o resultado é mais simpático que o do onstat. Na secção relativa à sessão, podemos ver os contadores habituais, mais o número de vezes que o motor escalonou a thread para correr, e o tempo de CPU consumido, bem como o nome das threads
Na secção destinada às partições podemos encontrar os nomes das tabelas, partições ou indíces numa nomenclatura fácil de entender (em vez do partnum).
Sobre a comparação, podemos ver uma grande diferença. Muitos mais leituras de buffers e chamadas ISAM para o primeiro plano de execução e também mais consumo de CPU. Mas atenção que para queries muito rápidas os tempos de CPU podem apresentar uma variação muito grande. Por isso convém não assumir imediatamente que um plano de execução é melhor porque se vê um tempo de CPU menor na primeira interação. Deve repetir-se o teste muitas vezes para se verificar as oscilações.
Chamo também a atenção para o significado das chamadas ISAM. Muitas vezes vejo confusões sobre este tema. Algumas pessoas pensam que são o número de SELECTs (para os ISAM reads), ou que serão o número de linhas retornadas... Na realidade é o número de chamadas a funções internas. Algumas configurações do motor como BATCHEDREAD_TABLE e BATCHEDREAD_INDEX podem influenciar o número destas chamadas, para a mesma query e mesmo conjunto de resultados.


É tudo por agora. Deixo-lhe o repositório e a esperança que artigos futuros se foquem em alguns destes scripts. Use-os à vontade e envie quaisquer sugestões.