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; 



No comments: