Visão geral
O principal objetivo deste artigo é mostrar como os ANSI JOINs do Couchbase são testados. Em Couchbase versão 5.5Agora, você pode executar uniões usando a sintaxe padrão ANSI. Esse acréscimo às uniões as torna muito mais flexíveis, mas também muito mais complexas. Essa complexidade dificulta o teste em nossa estrutura normal, pois há possibilidades demais para serem escritas manualmente para automação. Felizmente, uma de nossas estruturas de teste nos permite definir os parâmetros de uma consulta e, em seguida, gerar conjuntos aleatórios de consultas com base nesses parâmetros. Essa estrutura é conhecida como Gerador de consultas aleatórias (RQG). Neste artigo, discutiremos como o RQG é usado para testar a funcionalidade ANSI JOIN que foi implementada. Primeiro, vamos dar uma breve olhada no que há de diferente nos ANSI JOINs.
Neste artigo, abordaremos os seguintes tópicos:
1. Introdução aos ANSI JOINs
2. Visão geral do gerador de consultas aleatórias (RQG)
3. USANDO o RQG para testar ANSI JOINs
4. Pontos principais
5. Lista de bugs adicionais do ANSI JOIN encontrados
6. Apêndice
Introdução aos ANSI JOINs
Anteriormente, no Couchbase, você só podia executar uniões de pesquisa e uniões de índices. Isso era bom para consultas em que um lado da união pode produzir uma chave de documento do outro lado da união. No entanto, o problema com isso é que as uniões não podiam ser executadas em outros campos de um espaço-chave. As uniões ANSI corrigem esse problema e vão além: agora as uniões podem ser executadas em campos arbitrários ou expressões que resultam em campos, ou em várias condições de união.
Sintaxe antiga do Join:
Lhs-keyspace [join-type] JOIN rhs-keyspace
ON KEYS [expressão que resulta em um meta().id de um dos espaços-chave]
Sintaxe ANSI JOIN:
lhs-expression [join-type] JOIN rhs-keyspace ON [join-condition]
Consulta de sintaxe de junção antiga:
|
1 2 3 4 5 |
SELECIONAR t_3.decimal_campo1 , t_3.chave_primária_id , t_3.varchar_campo1 , t_3.char_field1 DE multiple_table_db_87317526_simple_table_4 t_2 ESQUERDA JUNTAR multiple_table_db_87317526_simple_table_1 t_3 ON CHAVES [ t_2.chave_primária_id ] ONDE (t_2.bool_field1 != verdadeiro E t_3.decimal_field1 <= 4980) |
Mesma consulta com ANSI JOINs Sintaxe:
|
1 2 3 4 5 |
SELECIONAR t_3.decimal_campo1 , t_3.chave_primária_id , t_3.varchar_campo1 , t_3.char_field1 DE multiple_table_db_87317526_simple_table_4 t_2 ESQUERDA JUNTAR multiple_table_db_87317526_simple_table_1 t_3 ON t_2.primary_key_id = t_3.primary_key_id ONDE (t_2.bool_field1 != verdadeiro E t_3.decimal_field1 <= 4980) |
Os tipos de junção compatíveis com o N1QL são INNER JOIN ou LEFT/RIGHT OUTER JOIN. Se você simplesmente colocar JOIN, isso será interpretado como INNER JOIN, e LEFT/RIGHT JOIN será interpretado como OUTER JOIN. Na nova sintaxe de união, o lado esquerdo da união pode ser um espaço-chave, uma consulta n1ql aninhada, uma expressão n1ql ou a própria união. O lado direito da união deve ser um espaço de chave na implementação atual de ANSI JOINs dentro do Couchbase. A cláusula ON contém as condições que a união deve atender. Essas condições podem ser qualquer expressão de predicado, desde que haja um índice adequado no espaço de chave do lado direito. Portanto, você pode fazer comparações de igualdade e comparações de não igualdade (b1.field = b2.field ou b1.field/=/!=/b2.field ou b2.field is NOT NULL ou expressão complexa = b2.field). Esta é uma breve análise do novo recurso. Para se aprofundar no recurso, consulte esta postagem do blog escrita por Bingjie Miao (o desenvolvedor que implementou o recurso): https://www.couchbase.com/blog/ansi-join-support-n1ql/
Agora começamos a entender que as ANSI JOINs têm pouquíssimas limitações em comparação com nossas antigas junções. O lado esquerdo de uma união ANSI pode ser muito complexo, podendo até ser várias uniões, ou uma consulta aninhada que resulta em alguns dados json, ou várias consultas aninhadas. Devido a esse nível de complexidade, é melhor usar algo que chamamos de RQG, em vez de criar consultas individuais para testar. Aqui está uma breve visão geral de como o RQG funciona.
Visão geral do gerador de consultas aleatórias (RQG)
O RQG é uma estrutura que foi escrita pelo Couchbase e que aproveita outra estrutura chamada Randgen. Basicamente, especificamos um conjunto de parâmetros em um arquivo chamado gramática e, a partir desses parâmetros, o Randgen criará modelos de consulta SQL (consultas com espaços reservados para os nomes de campo e de tabela) e, em seguida, nossa estrutura processa esses modelos e os converte em consultas formatadas em N1QL com base no conjunto de dados carregado no couchbase.
Exemplo de modelo de consulta:
|
1 2 3 4 5 |
SELECIONAR LISTA_DE_CAMPOS_NUMÉRICOS DE BUCKET_NAME INNER JUNTAR BUCKET_NAME ON ( TABELA ANTERIOR.CAMPO = CURRENT_TABLE.CAMPO ) ONDE NÃO (BOOL_FIELD != falso); |
Esse modelo é gerado a partir do arquivo de gramática que contém os parâmetros. Basicamente, o arquivo de gramática informa ao randgen para gerar uma consulta que seleciona uma lista de dados de tipo numérico de uma união de dois compartimentos arbitrários em uma comparação de igualdade. A gramática terá vários tipos de campos a serem selecionados e várias condições de união que podem ser inseridas nos predicados ON/ WHERE. Depois que o modelo for gerado, conforme mostrado acima, nossa estrutura irá substituir esses valores arbitrários por valores baseados nos dados carregados. Para NUMERIC_FIELD_LIST, ele selecionará um campo que seja uma lista de ints/bigints/floats/decimals/etc, dependendo do que estiver disponível. A estrutura substituirá BUCKET_NAME pelo nome específico de um bucket existente no servidor. PREVIOUS_TABLE.FIELD será um campo do primeiro bucket e CURRENT_TABLE.FIELD será o mesmo campo do segundo bucket. Depois que tudo isso for feito, ele analisará a consulta e criará uma cópia da consulta, na qual todas as diferenças entre o N1QL e o SQL serão eliminadas. Por exemplo, na antiga sintaxe de junção, não podíamos usar a palavra-chave ON, então, em vez disso, o RQG converteria ON em ON KEYS para as consultas n1ql.
Exemplo de consulta convertida:
|
1 2 3 4 5 |
SELECIONAR t_3.decimal_campo1 , t_3.int_field1 DE multiple_table_db_87317649_simple_table_4 t_2 INNER JUNTAR multiple_table_db_87317649_simple_table_1 t_3 ON CHAVES [ t_2.chave_primária_id ] ONDE (t_2.bool_field1 != falso) |
Em seguida, tanto as consultas n1ql quanto as consultas sql são executadas no servidor couchbase e em um servidor MySQL equivalente. Os resultados das consultas N1QL são comparados com os resultados das consultas MySQL equivalentes. Nosso objetivo é fazer com que nossos resultados correspondam aos resultados do MySQL, pois queremos que o N1QL seja funcionalmente semelhante ao SQL. Se você quiser saber mais sobre como o RQG é organizado ou como ele gera modelos e os converte em n1ql, consulte esta série de 5 partes sobre o RQG: https://dzone.com/articles/testing-n1ql-sql-for-json
USANDO o RQG para testar ANSI JOINs
A maneira como o RQG é configurado nos permite gerar uma variedade de consultas que, de outra forma, talvez não tivéssemos pensado. Aqui está um modelo extremamente complicado gerado pelo RQG:
|
1 2 3 4 5 |
SELECIONAR LISTA_DE_CAMPOS_NUMÉRICOS, STRING_FIELD_LIST DE BUCKET_NAME INNER JUNTAR BUCKET_NAME ON ( TABELA ANTERIOR.CAMPO = CURRENT_TABLE.CAMPO ) ONDE (((((CAMPO_STRING != STRING_VALUES E (NUMERIC_FIELD ENTRE VALOR_LIMITE_INFERIOR e VALOR_LIMITE_SUPERIOR) OU (NUMERIC_FIELD ENTRE VALOR_LIMITE_INFERIOR e VALOR_LIMITE_SUPERIOR))) E (NUMERIC_FIELD IS NÃO NULL)) E ((NÃO (((BOOL_FIELD E (NUMERIC_FIELD IS NULL) OU (NUMERIC_FIELD >= NUMERIC_VALUE))) E ((BOOL_FIELD != falso E NUMERIC_FIELD = NUMERIC_VALUE)) OU ((BOOL_FIELD != verdadeiro E NUMERIC_FIELD = NUMERIC_VALUE)) E (NÃO (BOOL_FIELD)))) E (CAMPO_STRING IS NULL))) E (((BOOL_FIELD E NUMERIC_FIELD != NUMERIC_VALUE)) E ((BOOL_FIELD = verdadeiro E NUMERIC_FIELD IS NULL)))) OU (((NÃO ((NUMERIC_FIELD < NUMERIC_VALUE) E ((BOOL_FIELD != falso OU CAMPO_STRING < STRING_VALUES)))) E (NÃO (((CAMPO_STRING IN ( LISTA )) E (CAMPO_STRING GOSTO STRING_VALUES) E NUMERIC_FIELD IS NULL)))) OU (((BOOL_FIELD E (NÃO ((NUMERIC_FIELD = NUMERIC_VALUE) E (NUMERIC_FIELD ENTRE VALOR_LIMITE_INFERIOR e VALOR_LIMITE_SUPERIOR))) OU (NUMERIC_FIELD <= NUMERIC_VALUE))) OU (((NÃO (((NÃO ((BOOL_FIELD != verdadeiro E NUMERIC_FIELD > NUMERIC_VALUE))) E ((((BOOL_FIELD != falso OU NUMERIC_FIELD != NUMERIC_VALUE)) E ((BOOL_FIELD != verdadeiro E NUMERIC_FIELD ENTRE VALOR_LIMITE_INFERIOR e VALOR_LIMITE_SUPERIOR)) OU ((CAMPO_STRING IS NÃO NULL E NUMERIC_FIELD > NUMERIC_VALUE)) E ((NUMERIC_FIELD >= NUMERIC_VALUE E CAMPO_STRING = STRING_VALUES E NÃO (BOOL_FIELD)))) E ((NÃO (BOOL_FIELD) OU NUMERIC_FIELD IS NULL)))) E (((NÃO ((CAMPO_STRING >= STRING_VALUES E NUMERIC_FIELD IS NULL))) OU (NÃO ((((BOOL_FIELD E NUMERIC_FIELD != NUMERIC_VALUE)) E ((BOOL_FIELD != falso OU NUMERIC_FIELD IS NULL))) E (((CAMPO_STRING IN ( LISTA )) E ((CAMPO_STRING <= STRING_VALUES E NUMERIC_FIELD <= NUMERIC_VALUE))) E ((NUMERIC_FIELD ENTRE VALOR_LIMITE_INFERIOR e VALOR_LIMITE_SUPERIOR OU CAMPO_STRING != STRING_VALUES)))))) OU ((CAMPO_STRING <= STRING_VALUES E NUMERIC_FIELD >= NUMERIC_VALUE))))) OU (NÃO ((NÃO ((BOOL_FIELD = falso E NUMERIC_FIELD IN ( LISTA )))) OU (NÃO ((BOOL_FIELD = verdadeiro E NUMERIC_FIELD < NUMERIC_VALUE)))))) E ((((BOOL_FIELD != verdadeiro E CAMPO_STRING NÃO ENTRE VALOR_LIMITE_INFERIOR e VALOR_LIMITE_SUPERIOR)) OU (((BOOL_FIELD = falso OU (CAMPO_STRING < STRING_VALUES) OU ((CAMPO_STRING <= STRING_VALUES) OU (CAMPO_STRING >= STRING_VALUES)))) E ((NÃO (BOOL_FIELD) OU NUMERIC_FIELD IS NÃO NULL)))) E (((NÃO (BOOL_FIELD) OU (CAMPO_STRING <= STRING_VALUES) OU (CAMPO_STRING != STRING_VALUES))) OU ((NUMERIC_FIELD < NUMERIC_VALUE OU CAMPO_STRING IS NULL))))))); |
Como você pode ver pelo tamanho dessa consulta, teria sido muito difícil criá-la por conta própria, e muitas consultas desse tipo são geradas e executadas por meio do RQG. Essa capacidade de criar consultas altamente complexas o torna perfeitamente adequado para lidar com a complexidade arbitrária dos ANSI JOINs. A gramática que definimos para testar ANSI JOINs tenta capturar a complexidade do lado esquerdo de uma junção, bem como a complexidade da cláusula ON. Dizemos ao RQG para gerar a cláusula lado esquerdo de uma união como uma de três coisas: um espaço-chave normal, uma subconsulta n1ql ou várias uniões aninhadas. Dizemos ao RQG para gerar o lado direito de uma união como um espaço-chave. Em seguida, dizemos ao RQG para gerar o Cláusula ON de uma junção como predicados de igualdade entre dois campos, um do espaço-chave à esquerda ou alias à esquerdae um do espaço de teclas do lado direito. As outras coisas que podem ser geradas na cláusula ON podem ser comparações de não igualdade (/=/!=) ou simplesmente verificar os atributos do campo do espaço-chave à direita (b2.field não é nulo, b2.field não está ausente, b2.field está em uma lista etc.). Para ANSI JOINs, há uma pequena diferença entre o n1ql e o sql. Essa diferença é que o N1QL oferece suporte à comparação IS (NOT) MISSING, o que se deve ao modelo de dados não relacional. Alguns compartimentos podem ter documentos que possuem um campo que outros documentos nesse compartimento não possuem. Portanto, IS (NOT) MISSING significa que o campo não aparece no documento (ou, no caso de IS NOT MISSING, significa que o campo aparece no documento). Tecnicamente, se quiséssemos, também poderíamos dizer ao RQG para gerar subconsultas n1ql dentro da cláusula ON e compará-las com os valores do espaço-chave à direita; no entanto, isso aumentaria exponencialmente o tempo de execução de nossas consultas e não é um caso de uso comum. Aqui estão alguns exemplos de consultas ANSI JOIN geradas a partir do nosso conjunto de parâmetros acima:
|
1 2 3 4 5 |
SELECIONAR NOME_DO_BUCKET_EXTERNO.* DE BUCKET_NAME INNER JUNTAR BUCKET_NAME ON ( TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.STRING_CAMPO ) ONDE NUMERIC_FIELD = NUMERIC_VALUE; |
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
SELECIONAR NOME_DO_BUCKET_EXTERNO.* DE BUCKET_NAME INNER JUNTAR BUCKET_NAME ON ( (TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING E TABELA ANTERIOR.NUMERIC_FIELD = CURRENT_TABLE.NUMERIC_FIELD) ) ESQUERDA JUNTAR BUCKET_NAME ON ( (TABELA ANTERIOR.NUMERIC_FIELD = CURRENT_TABLE.NUMERIC_FIELD OU TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING) ) INNER JUNTAR BUCKET_NAME ON ( NÃO ((TABELA ANTERIOR.NUMERIC_FIELD < CURRENT_TABLE.NUMERIC_FIELD E TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING)) ) INNER JUNTAR BUCKET_NAME ON ( (TABELA ANTERIOR.NUMERIC_FIELD = CURRENT_TABLE.NUMERIC_FIELD E TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING E TABELA ANTERIOR.NUMERIC_FIELD = CURRENT_TABLE.NUMERIC_FIELD) ) INNER JUNTAR BUCKET_NAME ON ( (TABELA ANTERIOR.NUMERIC_FIELD = CURRENT_TABLE.NUMERIC_FIELD E TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING) ) INNER JUNTAR BUCKET_NAME ON ( (TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING E TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING) ) INNER JUNTAR BUCKET_NAME ON ( NÃO ((TABELA ANTERIOR.NUMERIC_FIELD != CURRENT_TABLE.NUMERIC_FIELD OU TABELA ANTERIOR.CAMPO_STRING = CURRENT_TABLE.CAMPO_STRING)) ) INNER JUNTAR BUCKET_NAME ON ( NÃO ((TABELA ANTERIOR.CAMPO_STRING != CURRENT_TABLE.CAMPO_STRING E TABELA ANTERIOR.NUMERIC_FIELD = CURRENT_TABLE.NUMERIC_FIELD)) ) ONDE NULL_NUM_FIELD IS NULL ou NULL_NUM_FIELD IS FALTANDO; |
Como você pode ver nesses exemplos, a cláusula ON pode ser vários predicados encadeados, encadeados por AND/OR. Também podemos observar que várias junções podem ser executadas em uma consulta e que cada junção pode ter sua própria cláusula ON.
Agora que já entendemos como o RQG gera os modelos e como são esses modelos, vamos dar uma olhada em um exemplo de bug que encontramos na funcionalidade ANSI JOIN e que provavelmente não teríamos encontrado de outra forma. MB-27483, Nesse bug, tivemos a seguinte consulta:
|
1 2 3 4 5 |
SELECIONAR t_2.* DE multiple_table_db_492_simple_table_10 t_4 INNER JUNTAR tabela_múltipla_db_492_tabela_simples_4 t_2 ON ( NÃO ((t_4.char_field1 = t_2.char_field1 E t_4.int_field1 = t_2.int_field1))) ONDE (NÃO ((t_2.bool_field1 = verdadeiro E t_2.decimal_field1 < 4878))) OU ((((t_2.bool_field1 != verdadeiro E t_4.int_field1 IN [ 0 , 12 , 21 , 32 , 39 ])) OU ((t_2.decimal_field1 != 4878) OU (t_4.decimal_field1 >= 4821))) E (NÃO (((NÃO (t_4.bool_field1) OU t_2.char_field1 GOSTO "N")) E ((t_2.decimal_field1 ENTRE 5 e 9985 E t_2.varchar_field1 IS NULL E t_2.bool_field1 = falso))))) LIMITE 100 |
Essa consulta deveria ter sido executada com êxito, dada a forma como o nosso servidor estava configurado; no entanto, em vez de ser executada, ela estava gerando um erro de que o índice adequado não existia no espaço de chave do lado direito. O problema aqui era que a cláusula WHERE estava interferindo na seleção do índice da cláusula ON, fazendo com que a consulta pensasse que não tinha o índice de que precisava, quando esse índice de fato existia. Se analisarmos a cláusula WHERE, veremos que ela é muito complexa, e esse não é o nível de complexidade que normalmente teríamos no lado funcional do teste. Portanto, não teríamos detectado esse bug sem usar o RQG.
Pontos principais
Os ANSI JOINs são bastante flexíveis em termos do que é possível unir, você pode unir expressões arbitrárias com espaços-chave e a cláusula ON pode conter predicados que comparam expressões arbitrárias a campos. Esse nível de complexidade torna impraticável tentar criar consultas específicas para testar cada cenário por conta própria por meio de testes funcionais. Felizmente, temos uma estrutura chamada RQG que é capaz de gerar consultas com base em um conjunto de regras que podemos definir. Essa funcionalidade é muito poderosa e nos permitiu testar ANSI JOINs de forma satisfatória. Por exemplo, se um grande número de consultas complexas estivesse sendo executado, os resultados do ANSI JOIN teriam um desvio de 1. No entanto, se qualquer uma dessas consultas individuais fosse executada isoladamente, os resultados estariam corretos. É por isso que estamos confiantes de que os ANSI JOINs estão em boa forma e prontos para serem usados pelos clientes.
Lista de bugs adicionais do ANSI JOIN encontrados com o RQG:
| Número do bug | Descrição do bug |
| MB-27834 | Essa consulta não estava usando corretamente a dica de consulta LIMIT; ela executava a consulta inteira antes de aplicar o LIMIT |
| MB-27763 | Essa consulta não estava usando corretamente a dica de consulta LIMIT, semelhante ao bug acima |
| MB-27483 | A consulta pensou que o índice apropriado não existia, o que levou a consulta a lançar um erro. No entanto, o índice estava de fato presente no sistema |
| MB-27230 | O erro errado estava sendo retornado pela consulta |
| MB-27201 | Essa consulta estava apresentando um erro, pois o backfill não estava funcionando corretamente |
| MB-27109 | Outra consulta que achava que o índice de que precisava não estava disponível, embora o índice estivesse presente no sistema |
| MB-27028 | Quando várias consultas foram executadas em sucessão, alguns dos resultados das consultas estavam errados em 1. Quando as consultas foram executadas sozinhas, todos os resultados estavam corretos. |
| MB-26649 | A consulta ficava suspensa de forma intermitente |
Apêndice
- Blog do Couchbase Server 5.5 GA https://www.couchbase.com/blog/announcing-couchbase-server-5-5/
- Uniões de índices https://dzone.com/articles/join-faster-with-couchbase-index-joins
- ANSI JOINs Blog do Couchbase https://www.couchbase.com/blog/ansi-join-support-n1ql/
- Randgen https://launchpad.net/randgen
- Série de artigos do RQG Dzone https://dzone.com/articles/testing-n1ql-sql-for-json