Entendendo a dependência circular e como evitar esse erro em expressões DAX.

Quando estamos trabalhando com DAX e desejamos construir um modelo de dados para uma análise, existem alguns tópicos que devemos prestar atenção para não cair na falha da criação do modelo.

Uma das falhas mais comuns que ocorrem e mais difíceis de detectar é a que trata da dependência circular (circular dependencies). Como o PBI não nos entrega muita informação quando essa situação ocorre, vamos ver os três tipos mais comuns que causam esse tipo de erro.

ENTENDENDO O CASO

Antes de adentrarmos no assunto, vamos analisar a seguinte situação com estas duas fórmulas para calcular a margem de lucro e a porcentagem do lucro.

As duas são executadas normalmente sem o menor problema. Agora, veja quando tento criar a seguinte coluna calculada alterando o código da minha expressão utilizada chamada SalesMarg.

Para este código, irei utilizar a coluna SalesMrgPct e UnitCost, veja.

Como podemos ver, o PBI retornou o erro de execução acusando de circular depencies.

E afinal de contas o que é essa dependência circular?

A dependência circular é uma prevenção interna que o engine do PBI tem para não entrar em loop infinito.

Por exemplo, a primeira coluna calculada chamada SalesMarg. Para ela ser criada, utilizamos duas colunas de apoio: NetPrice e UnitCost. Estas colunas prendem a coluna calculada SalesMarg em uma relação de dependência, logo, para a última existir, ambas precisam existir.

Tanto que se algum produto atualizar seu preço, independente de qual seja, a coluna SalesMarg irá atualizar também. 

Com esse panorama explicado, somos capazes de entender o motivo da falha de execução causada pela dependência circular. Se voltarmos às duas expressões criadas, perceba que propositalmente criei uma dependência. Reveja.

Expressão criando as colunas de maneira separada

Expressão abaixo utilizando a coluna SalesMargPct (A) dentro da coluna SalesMarg (B). Assim, o PBI entra em loop, pois ele entende que a coluna A depende de B e ao mesmo tempo, a coluna B depende de A.

Esta é a falha de circular depencies.

Normalmente este cenário não acontece e o PBI sabe gerenciar as ordens de execução de maneira correta, evitando o erro. Essa falha ocorre quando estamos trabalhando com dois recursos, ambos envolvendo o uso de Calculate.

Os cenários mais comuns para um erro de dependência circular ocorrem quando:

  1. Calculate alterando o row context  de uma coluna calculada;
  2. Relacionamento de uma tabela com uma tabela calculada;
  3. E  esta relação é feita através de colunas calculadas.

O caso que trata da context transition vimos de maneira breve no post anterior. Neste post eu mostrei que não é possível criar duas colunas calculadas com a função calculate pelo fato dela adicionar todas as colunas da tabela ao filtro.

Quando isso acontece, é como se cada coluna dependesse uma da outra. Então, se temos as colunas A, B, C e D, logo, A depende de B, C e D e o mesmo se aplica para as outras, caindo no loop falho.

É por este motivo que não conseguimos criar duas expressões iguais a esta na mesma tabela.

Porém, embora complexo, este caso possui uma solução que é até bem simples.

Uma delas envolve o uso da função ALLEXCEPT no filtro da calculate. Lembrando que a função ALLEXCEPT ‘remove’ todas as colunas exceto a coluna destacada na função. Veja.

Como ‘deixei’ apenas a coluna ProductKey na tabela Sales, a expressão foi criada.

E a outra forma seria criar essa mesma expressão, sem utilizar a função allexcept, na tabela Produto. Veja esta coluna calculada criada na tabela Product.

Pelo fato da tabela Produto estar no lado 1 da relação com a tabela Sales (N), conseguimos criar essa expressão de forma limpa sem o menor problema. Agora, se quiser criar outra coluna calculada na tabela Produto, certifique-se de utilizar uma coluna que garanta unicidade. Observe.

Criei mais duas colunas na tabela Produto e se perceber, ambas estão com colunas no filtro que garantem a unicidade para a expressão

E aqui, o resultado. Notem que é o mesmo, ainda que com colunas diferentes. Como a expressão vai na tabela Sales, ela retorna a quantidade de relações encontradas nesta e soma ao final.

O processo inverso também pode ser feito pelo simples fato da tabela Product possuir valor único para cada linha, garantido pela coluna ProductKey na tabela.

Como a tabela Sales está no lado N da relação 1:N, ela consegue estabelecer essa coluna calculada em sua tabela.

Agora, se tentarmos criar uma soma da quantidade de produtos na tabela Sales ainda que utilizemos ALLEXCEPT na construção da expressão, não será possível. Mesmo indo na tabela Product.

Há dois fatores para acusar erro nesta construção:

  1. Todas as colunas da tabela Sales estão no filtro implícito criado pela calculate;
  2. ALLEXCEPT age na tabela Product, o que não é o que queremos, visto que estamos trabalhando na tabela Sales.

A solução seria aplicar uma destas duas soluções: 

Como vimos anteriormente, estas foram as soluções apresentadas para solucionar esse incômodo problema quando tratamos de circular dependecies em colunas calculadas que possuem calculate em sua construção.

Agora, vamos ver as nuances do que ocorre quando trabalhamos com tabelas.

DEPENDÊNCIA CIRCULAR EM TABELAS CALCULADAS

Para tabelas calculadas, o formato de solução não se altera muito, mas há algumas questões que se diferem. Principalmente quando estabelecemos relacionamento entre as tabelas.

Uma tabela calculada por definição é uma forma reduzida da tabela fonte, e assim, conseguimos até estabelecer relações entre tais e, tal qual nas colunas calculadas, também existe a possibilidade de erro baseado nas dependências circulares.

Veja esta tabela que contém os melhores produtos de 2008.

Criei duas colunas calculadas com a função calculate utilizando a função ALLEXCEPT para evitar o erro, como vimos na primeira parte deste post.

Veja o resultado destas duas colunas calculadas e percebam as linhas em blank destacadas em vermelho. 

NOTA: sem a função allexcept também funciona, a diferença é no resultado.

Se tentarmos estabelecer um relacionamento da tabela calculada(BestProduct) para a tabela Product, veja o que ocorre:

Essa falha acontece justamente por conta das linhas em branco na tabela calculada. Como na tabela fonte(Product) não há blank rows, diferente do que aconteceu com a tabela BestProduct, essa relação falha.

A única maneira de se estabelecer uma relação BestProduct -> Product, seria:

  1. Criando uma relação 1:N
  2. Criando uma relação 1:1 com o cross filter (both).

Qualquer relacionamento na situação acima, partindo da tabela Product, deve obrigatoriamente ser 1:1

E o que acontece nesta falha, afinal.

  1. Criamos a tabela BestProduct(B) baseada na tabela Product(A), inclusive usando sua key;
  2. Estabelecemos um relacionamento entre elas mas passando para a tabela Product, que ela é dependente da tabela BestProduct (o que não é o ‘correto’);
  3. Considerando o cenário do item 2, se houver qualquer atualização na tabela A, uma ação será disparada para a tabela B, que fará o mesmo para a tabela A. Visto que erroneamente, ela depende da B.
  4. Esta ação descrita no item 3 causará o loop que que também é ampliado devido o tratamento das linhas marcadas como blank que são dependentes mas não possuem correspondentes.

E é justamente por este motivo que quando direcionamos o relacionamento de A > B, temos de marcar na cardinalidade 1:1.

Tanto que se eliminarmos a função allexcept da coluna Test 2, teremos apenas 112 relações. Este número de relações é devido a quantidade de valores distintos na tabela. 111 valores mais um representando a linha marcada como blank.

Para evitar este loop com tabelas calculadas, temos de escolher um tipo de dependência e utilizá-la na criação do código.

  • Ou criamos uma calculatetable em que ela dependa totalmente de sua fonte, incluindo as linhas com blank ou;
  • As linhas com blank marcadas na tabela fonte passam a depender do conteúdo da calculatetable.

Mas como a linguagem DAX é bastante versátil, é possível criar um cenário em que a calculatedtable dependa de algumas linhas da sua fonte, mas que não use blank e assim, seu conteúdo não se altera.

Como o conteúdo não se alterará, ainda que as linhas com blank sejam dependentes da calculatedtable, não haverá o loop da dependência circular, já que não haverá alteração na tabela calculada.

Deste modo, a melhor maneira de construir uma tabela com a fórmula da calculatetable seria utilizando a função allnoblankrows. Com esta função, eliminamos toda a dependência das linhas marcadas como blank.

Construindo uma calculatedtable com esta expressão, é como se disséssemos para o PBI: “Todas as linhas exceto as linhas em branco”.

Reconstruindo o relacionamento entre as tabelas conseguimos criar a relação Product -> BestProduct com a cardinalidade 1:N além de todas as outras opções permitidas.

Outro fator que gostaria de chamar atenção após construir uma calculatedtable utilizando allnoblankrows é quanto ao número de linhas, veja.

Nesta primeira tabela utilizei apenas o all. Veja destacado em azul.

E na segunda, o exemplo envolvendo a função allnoblankrows.

Como estamos puxando todas as linhas e desconsiderando as linhas em branco (blank), todas as linhas que existem na tabela Product, estão sendo relacionadas na minha tabela BestProduct. 

NOTA: o número 2190 na segunda imagem diz respeito a quantidade de linhas que filtrei na seleção sem selecionar as linhas em branco.

Com isso, temos a nossa relação bem estabelecida entre tabela e a tabela calculada.

CONCLUSÃO

Este foi um post que precisei ler e estudar bastante por ser um assunto bem complexo e com bastante detalhe.

Tentei explicar da melhor e mais clara forma possível, abordando os principais aspectos desse tipo de situação que acontece no PBI.

As dependências circulares são um tipo de erro que está mais no código do que no programa, então, se esse tipo de situação acontecer, pode ter certeza que há alguma coisa errada com a expressão que foi criada.

Sempre que for criar uma tabela calculada e queira garantir que não haverá erro de dependência, utilize a função allnoblankrows para quebrar a relação de dependência entre as tabelas envolvidas.

Para colunas calculadas, tente utilizar distinct no lugar de values, também ajudará a evitar problemas, e claro, veja se allexcept funciona e se enquadra no seu cenário.

Não esqueça que calculate e calculatetable são iguais, cuidado com os filtros da função. Muitas vezes, eles estão implícitos e desse modo, podem causar erros no relacionamento.

Para este post, também utilizei leitura de apoio com o intuito de tentar aprender sobre essa situação o máximo possível, assim sendo, deixo o link do post para quem quiser.

Link de apoio, aqui.

Espero que gostem e os ajude. Deus os abençoe!

Se gostou, compartilhe com os amigos!

Seja nosso assinante e fique por dentro dos nossos posts!