Malloy: Uma nova linguagem para análise de dados

Recentemente descobri uma linguagem que tem me chamado bastante atenção: Malloy. Se você trabalha com dados e já sentiu que o SQL, apesar de poderoso, às vezes parece verboso demais ou difícil de reutilizar, este post é para você.

Vou apresentar o que é Malloy, suas principais diferenças em relação ao SQL tradicional, exemplos práticos e como você pode começar a experimentar essa linguagem no seu fluxo de trabalho.

O Que é Malloy?

Malloy é uma linguagem open source criada por Lloyd Tabb, o mesmo fundador do Looker (posteriormente adquirido pelo Google). A proposta é ambiciosa: ser uma alternativa moderna ao SQL para consultas analíticas.

O que torna Malloy interessante é que ela combina duas coisas que normalmente ficam separadas: uma linguagem de modelagem semântica e uma linguagem de consulta. Na prática, isso significa que você define suas métricas, dimensões e relacionamentos no mesmo lugar onde escreve suas queries.

Malloy não substitui seu banco de dados. Ela compila para SQL otimizado e executa no banco que você já usa. Atualmente, há suporte para BigQuery, Snowflake, PostgreSQL, MySQL, Trino, Presto e, nativamente, DuckDB.

Por Que Malloy Existe?

O SQL foi criado há mais de 40 anos e, embora seja extremamente poderoso, algumas decisões de design não envelheceram bem para o contexto de análise de dados moderna. Os criadores do Malloy identificaram alguns problemas:

  • Falta de reusabilidade: em SQL, é difícil salvar e reutilizar cálculos de forma elegante
  • Verbosidade: consultas simples podem exigir muito código
  • Ambiguidade sintática: o SELECT serve tanto para agregação quanto para seleção simples, dependendo do contexto
  • Ordem invertida: definimos aliases depois da expressão (SUM(valor) AS total), ao contrário de linguagens modernas onde declaramos variável = valor
  • Agregações aninhadas são complexas: criar hierarquias de dados exige múltiplas subqueries ou CTEs
  • Resultados bidimensionais: SQL força tudo em tabelas planas, mesmo quando os dados são naturalmente hierárquicos

Principais Características do Malloy

Modelagem Semântica Integrada

Em Malloy, você define um source que representa sua tabela e, dentro dele, declara dimensões e medidas que podem ser reutilizadas em qualquer query.

O código abaixo mostra como declaramos uma fonte de dados e definimos medidas reutilizáveis:

source: vendas is duckdb.table('vendas.parquet') extend {
  dimension: ano is year(data_pedido)
  dimension: mes is month(data_pedido)
  dimension: trimestre is quarter(data_pedido)
  
  measure: total_vendas is sum(valor)
  measure: qtd_pedidos is count()
  measure: ticket_medio is total_vendas / qtd_pedidos
  measure: qtd_clientes is count(cliente_id)
}

Uma vez definidas, essas medidas ficam disponíveis para qualquer consulta sobre esse source, sem precisar reescrever a lógica.

Sintaxe Mais Intuitiva

Em Malloy, a ordem das operações segue um fluxo mais natural: primeiro definimos a fonte, depois filtramos, agrupamos e agregamos.

Veja a comparação entre SQL e Malloy para a mesma consulta:

SQL tradicional:

SELECT 
  carrier,
  COUNT(*) AS flight_count,
  AVG(flight_time) AS average_flight_time
FROM flights
WHERE origin = 'SFO'
GROUP BY carrier
ORDER BY flight_count DESC

Malloy:

run: flights -> {
  where: origin = 'SFO'
  group_by: carrier
  aggregate: 
    flight_count is count()
    average_flight_time is flight_time.avg()
}

Note que em Malloy não precisamos do ORDER BY — a linguagem automaticamente ordena pelo primeiro agregado. Além disso, a declaração nome is expressão é mais consistente com linguagens de programação modernas.


Exemplos Práticos

Agora vamos ver exemplos mais elaborados que mostram o poder real do Malloy.

Exemplo 1: Queries Aninhadas (Nested Queries)

Um dos recursos mais poderosos do Malloy são as queries aninhadas. Em SQL, para ver dados em múltiplos níveis de granularidade simultaneamente, você precisaria de múltiplas CTEs ou subqueries complexas. Em Malloy, é natural.

O código abaixo cria uma hierarquia de dados onde cada estado contém uma sub-tabela com os aeroportos daquele estado:

run: airports -> {
  group_by: state
  aggregate: airport_count
  nest: by_facility is {
    group_by: fac_type
    aggregate: airport_count
  }
}

O resultado não é uma tabela plana tradicional. Cada linha de estado contém uma sub-tabela com a contagem por tipo de instalação. Isso é muito útil para dashboards e relatórios hierárquicos.

Podemos ir além e criar múltiplos níveis de aninhamento. O exemplo a seguir mostra aeroportos por estado, com detalhamento por condado e tipo de instalação:

run: airports -> {
  where: state ? 'CA' | 'NY' | 'TX'
  group_by: state
  aggregate: airport_count
  nest: top_5_counties is {
    limit: 5
    group_by: county
    aggregate: airport_count
    nest: major_facilities is {
      where: major = 'Y'
      group_by: name is concat(code, ' - ', full_name)
    }
    nest: by_facility is {
      group_by: fac_type
      aggregate: airport_count
    }
  }
}

Essa query retorna, para cada estado, os 5 principais condados, e dentro de cada condado, tanto as instalações principais quanto a distribuição por tipo. Em SQL, isso seria extremamente verboso.

Exemplo 2: Percentual do Total

Calcular percentuais em relação ao total é uma operação comum que em SQL exige window functions ou subqueries. Em Malloy, a função all() torna isso trivial.

O código abaixo calcula o percentual de voos de cada companhia aérea em relação ao total:

source: flights is duckdb.table('flights.parquet') extend {
  join_one: carriers is duckdb.table('carriers.parquet') 
    on carrier = carriers.code
  
  measure: flight_count is count()
}

run: flights -> {
  group_by: carriers.nickname
  aggregate: 
    flight_count
    all_flights is all(flight_count)
    percent_of_flights is flight_count / all(flight_count)
  limit: 10
}

A função all() calcula o agregado considerando todos os dados, ignorando o agrupamento da linha atual. O resultado mostra cada companhia com sua contagem de voos e o percentual que representa do total.

Exemplo 3: Comparações Temporais (Year over Year)

Análises de variação temporal são fundamentais em BI. Em Malloy, podemos usar a cláusula calculate: para window functions como lag().

O código abaixo calcula o crescimento ano a ano de voos:

run: flights -> {
  group_by: dep_year is dep_time.year
  aggregate: flight_count
  calculate: 
    last_year is lag(flight_count, 1)
    growth is (lag(flight_count, 1) - flight_count) / lag(flight_count, 1)
  order_by: dep_year
}

Também podemos criar visualizações comparativas. O exemplo a seguir prepara dados para um gráfico de linhas comparando anos:

run: flights -> {
  group_by: 
    month_of_year is month(dep_time)
    flight_year is dep_time.year
  aggregate: flight_count
}

Isso gera dados prontos para um gráfico onde cada ano é uma linha colorida diferente, plotada por mês.

Exemplo 4: Análise de Coorte

Análises de coorte são essenciais para entender retenção e comportamento de clientes ao longo do tempo. Em Malloy, podemos construir isso de forma elegante.

Primeiro, definimos o modelo com as medidas necessárias:

source: order_items is duckdb.table('order_items.parquet') extend {
  join_one: users is duckdb.table('users.parquet') 
    on user_id = users.id
  
  measure: 
    user_count is count(user_id)
    order_count is count()
    total_sales is sale_price.sum()
}

Agora podemos criar a análise de coorte. O código abaixo mostra, para cada mês de pedido, quantos usuários de cada coorte de cadastro fizeram compras:

run: order_items -> {
  where: 
    (users.created_at.month ? @2022 for 6 months) 
    and created_at ? @2022 for 6 months
  
  group_by: order_month is created_at.month
  aggregate: users_that_ordered is user_count
  
  nest: cohorts is {
    group_by: user_signup_cohort is users.created_at.month
    aggregate: 
      users_in_cohort is user_count
      percent_of_cohort is user_count / all(user_count)
    order_by: user_signup_cohort
  }
}

O resultado mostra, para cada mês de pedido, uma sub-tabela com a distribuição por coorte de cadastro do usuário. Isso seria muito complexo em SQL puro.

Exemplo 5: Joins e Relacionamentos

Malloy simplifica a declaração de joins e, mais importante, previne erros comuns de agregação quando há relacionamentos many-to-many.

O código abaixo mostra como definir um modelo com múltiplos joins:

source: carriers is duckdb.table('carriers.parquet') extend {
  primary_key: code
  measure: carrier_count is count()
}

source: flights is duckdb.table('flights.parquet') extend {
  join_one: carriers with carrier
  
  measure: 
    flight_count is count()
    total_distance is distance.sum()
    avg_distance is distance.avg()
}

source: airports is duckdb.table('airports.parquet') extend {
  join_many: flights on code = flights.origin
  
  measure: 
    airport_count is count()
  dimension: 
    elevation_in_meters is elevation * 0.3048
}

Com esse modelo, posso fazer queries que navegam pelos relacionamentos. O exemplo a seguir mostra os top aeroportos por número de voos, com detalhamento por companhia aérea:

run: airports -> {
  group_by: code, city
  aggregate: 
    airport_count
    total_flights is flights.flight_count
  nest: top_carriers is {
    group_by: flights.carriers.nickname
    aggregate: flights.flight_count
    limit: 3
  }
  limit: 10
}

Exemplo 6: Agregações com Queries como Fonte

Podemos usar o resultado de uma query como fonte para joins, criando tabelas derivadas de forma elegante.

O código abaixo cria uma tabela de fatos sobre aeronaves e a usa para enriquecer queries:

query: aircraft_facts is duckdb.table('flights.parquet') -> {
  group_by: tail_num
  aggregate: 
    lifetime_flights is count()
    lifetime_distance is distance.sum()
}

source: flights_enriched is duckdb.table('flights.parquet') extend {
  join_one: aircraft_facts on tail_num = aircraft_facts.tail_num
}

run: flights_enriched -> {
  group_by: carrier
  nest: top_aircraft is {
    group_by: tail_num, aircraft_facts.lifetime_flights
    limit: 5
    order_by: lifetime_flights desc
  }
}

Exemplo 7: Filtros Dinâmicos com Literais de Tempo

Malloy tem uma sintaxe poderosa para trabalhar com datas. O operador ? (apply) combinado com literais de tempo torna filtros temporais muito legíveis.

O código abaixo demonstra diferentes formas de filtrar por tempo:

run: flights -> {
  // Voos de 2003
  where: dep_time ? @2003
  aggregate: flight_count
}

run: flights -> {
  // Voos do primeiro trimestre de 2003
  where: dep_time ? @2003-Q1
  aggregate: flight_count
}

run: flights -> {
  // Voos de janeiro a março de 2003
  where: dep_time ? @2003-01 to @2003-03
  aggregate: flight_count
}

Também podemos criar filtros relativos ao momento atual:

run: order_items extend {
  measure: 
    orders_last_30_days is count() { where: created_at > now - 30 days }
    orders_last_year is count() { where: created_at > now - 1 year }
} -> {
  aggregate: 
    orders_last_30_days
    orders_last_year
}

Exemplo 8: Expressões Condicionais (Pick)

O pick em Malloy funciona como o CASE do SQL, mas com uma sintaxe mais limpa.

O código abaixo categoriza aeroportos por elevação:

source: airports is duckdb.table('airports.parquet') extend {
  dimension: elevation_category is 
    pick 'Low' when elevation < 1000
    pick 'Medium' when elevation < 5000
    pick 'High' when elevation < 10000
    else 'Very High'
}

run: airports -> {
  group_by: elevation_category
  aggregate: airport_count is count()
}

O pick também pode ser combinado com o operador apply para limpeza de dados:

dimension: status_clean is status ?
  pick 'shipped' when 'will call' | 'shipped' | 'dispatched'
  pick 'cancelled' when 'canceled' | 'cancelled' | 'void'
  else status

Exemplo 9: Views Reutilizáveis

Podemos definir views dentro de um source e reutilizá-las em diferentes contextos.

O código abaixo define várias views que podem ser chamadas por nome:

source: flights is duckdb.table('flights.parquet') extend {
  measure: flight_count is count()
  
  view: by_carrier is {
    group_by: carrier
    aggregate: flight_count
  }
  
  view: by_origin is {
    group_by: origin
    aggregate: flight_count
    limit: 10
  }
  
  view: by_month is {
    group_by: dep_month is dep_time.month
    aggregate: flight_count
  }
  
  view: carrier_dashboard is {
    group_by: carrier
    aggregate: flight_count
    nest: by_origin
    nest: by_month
  }
}

// Executa views por nome
run: flights -> by_carrier
run: flights -> by_origin
run: flights -> carrier_dashboard

A última query (carrier_dashboard) gera um resultado rico: para cada companhia aérea, temos a contagem total, uma sub-tabela com top origens e outra sub-tabela com distribuição mensal. Isso é essencialmente um mini-dashboard em uma única query.

Exemplo 10: Subtotais Aninhados

Para relatórios financeiros e de negócios, frequentemente precisamos de subtotais em múltiplos níveis. Em SQL, isso exigiria GROUP BY ROLLUP ou múltiplas queries. Em Malloy, é natural.

O código abaixo cria um relatório de vendas com subtotais por região e categoria:

run: vendas -> {
  group_by: regiao
  aggregate: total_vendas
  nest: por_categoria is {
    group_by: categoria
    aggregate: total_vendas
    nest: por_produto is {
      group_by: produto
      aggregate: total_vendas
      limit: 5
    }
  }
}

O resultado preserva a hierarquia: região → categoria → produto, cada nível com seus respectivos totais.


Instalação e Primeiros Passos

A forma mais fácil de experimentar Malloy é através da extensão para VS Code. O DuckDB já vem embutido na extensão, então você pode começar imediatamente com arquivos locais.

Passo 1: Instale o Visual Studio Code se ainda não tiver.

Passo 2: Abra o VS Code, clique no ícone de extensões (o quadrado com peças) e busque por “Malloy”.

Passo 3: Instale a extensão oficial da Malloy.

Passo 4: Crie um arquivo com extensão .malloy e comece a escrever.

Para testar com um arquivo CSV local, você pode usar:

source: meus_dados is duckdb.table('caminho/para/arquivo.csv')

run: meus_dados -> {
  group_by: coluna1
  aggregate: total is count()
}

A extensão do VS Code oferece recursos visuais interessantes. Ao clicar em “Run” acima de uma query, você vê os resultados inline, incluindo renderização de tabelas aninhadas e até gráficos básicos.

Conectando com BigQuery

Se você trabalha com BigQuery, precisará configurar a autenticação via gcloud CLI:

gcloud auth login --update-adc
gcloud config set project SEU_PROJECT_ID --installation

Depois, pode referenciar tabelas do BigQuery diretamente:

source: eventos is bigquery.table('meu_projeto.meu_dataset.minha_tabela')

Conectando com PostgreSQL

Para PostgreSQL, configure a conexão nas settings da extensão do VS Code, informando host, porta, usuário, senha e banco de dados.


Aplicações Práticas

Business Intelligence e Dashboards

Malloy brilha na construção de camadas semânticas para BI. Você pode:

  • Definir métricas de negócio uma vez e reutilizá-las em todos os relatórios
  • Criar dashboards hierárquicos com nested queries
  • Garantir consistência nas definições de KPIs

Análise Exploratória

Para cientistas de dados, Malloy oferece:

  • Prototipagem rápida de hipóteses
  • Fácil pivoteamento entre diferentes granularidades
  • Análises de coorte e retenção simplificadas

Data Engineering

Para engenheiros de dados:

  • Documentação viva do modelo de dados
  • Validação de lógica de negócio antes de implementar em pipelines
  • Geração de SQL otimizado para diferentes dialetos

Casos de Uso Ideais

  • E-commerce: análise de funil, coortes de clientes, performance de produtos
  • SaaS: métricas de retenção, MRR, churn por coorte
  • Finanças: relatórios com subtotais hierárquicos, análise temporal
  • Marketing: atribuição, análise de campanhas, segmentação

Limitações e Considerações

Malloy ainda é uma linguagem experimental. Algumas limitações a considerar:

  • Ecossistema pequeno: comunidade e recursos de aprendizado ainda limitados comparados a SQL
  • Sem operações transacionais: Malloy é focado em leitura analítica, não em DDL/DML
  • Curva de aprendizado: embora similar a SQL, há conceitos novos a aprender
  • Integração: nem todas as ferramentas de BI suportam Malloy nativamente

Por outro lado, se você já usa DuckDB ou BigQuery e precisa de análises complexas frequentemente, vale muito a pena experimentar.

Considerações Finais

Malloy representa uma evolução interessante no mundo das linguagens de consulta. Ela não tenta substituir SQL completamente, mas oferece uma alternativa mais expressiva para análises complexas.

O que mais me impressiona é como consultas que seriam verbosas e propensas a erros em SQL ficam concisas e legíveis em Malloy. A capacidade de trabalhar com dados hierárquicos de forma nativa é um diferencial significativo.

Se você trabalha com análise de dados, recomendo dedicar algumas horas para experimentar. A curva de aprendizado é suave para quem já conhece SQL, e os ganhos de produtividade podem ser significativos.

Links úteis:

O que você achou do Malloy? Já conhecia ou é novidade?


Comentários

Deixe um comentário

O seu endereço de email não será publicado. Campos obrigatórios marcados com *