O artigo anterior desta série terminou com uma pergunta: se um formato de tabela resolve a persistência e a atualização dos dados, quem responde às consultas com baixa latência?

Uma resposta comum é o Trino. Mas o Trino não opera sozinho. Ao redor dele aparecem um catálogo, armazenamento de objetos, um formato como Iceberg, uma camada de ingestão e alguma ferramenta para organizar transformações. Cada peça tem uma função clara; a plataforma nasce da integração entre elas.

O Apache Doris parte de outra proposta. Em vez de ser apenas a Engine de Consulta, ele reúne catálogo, execução, armazenamento, ingestão e mecanismos de aceleração no mesmo sistema. A comparação relevante, portanto, não é Doris contra Trino. É Doris contra a stack necessária para o Trino cumprir papel de serving — a camada que recebe consultas de ferramentas de BI e aplicações e devolve os dados prontos para consumo.

Da stack aberta ao engine integrado

O Trino é uma Engine de Consulta distribuída. Ele recebe SQL, planeja a consulta e coordena o trabalho necessário para ler e combinar dados. Sua força está justamente em não exigir que esses dados pertençam ao próprio Trino: conectores permitem consultar diferentes sistemas e até cruzá-los na mesma consulta.

Essa flexibilidade resolve a camada de computação. As demais responsabilidades continuam em outros componentes:

O diagrama é um mapa conceitual. Ele mostra responsabilidades, não uma topologia obrigatória de implantação.

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#F7F7F5",
    "primaryTextColor": "#1F2937",
    "primaryBorderColor": "#D6D3D1",
    "secondaryColor": "#EEEDE8",
    "tertiaryColor": "#FFF4DB",
    "lineColor": "#6B7280",
    "clusterBkg": "#EEEDE8",
    "clusterBorder": "#D6D3D1",
    "fontFamily": "Inter, Segoe UI, Helvetica Neue, Arial, sans-serif",
    "fontSize": "15px"
  },
  "flowchart": {"curve": "basis", "nodeSpacing": 24, "rankSpacing": 32, "padding": 8}
}}%%
flowchart TB
    ING["Ingestão (Seatunnel/Airbyte)"]
    DATA["Armazenamento de objetos (S3/GCS)<br/>+ Formato Lakehouse (Iceberg)"]
    CAT["Catálogo (Hive Metastore)"]
    ELT["ELT (dbt)"]
    TRINO["Trino"]
    BI1["BI / análise interna"]
    SERVING["Banco de Application Serving<br/>(PostgreSQL / MySQL)"]
    APP["Aplicação<br/>customer-facing"]

    ING --> DATA
    DATA --> TRINO
    CAT --> TRINO
    ELT --> TRINO
    TRINO --> BI1
    TRINO --> SERVING
    SERVING --> APP

    classDef muted fill:#F7F7F5,stroke:#D6D3D1,color:#4B5563,stroke-width:1px;
    classDef active fill:#FFF4DB,stroke:#B45309,color:#1F2937,stroke-width:2px;
    class ING,DATA,CAT,ELT,BI1,SERVING,APP muted;
    class TRINO active;
  • Ingestão: O primeiro passo é transportar dados de sistemas operacionais, eventos ou arquivos para a camada analítica. Esse trabalho pode ser contínuo ou em lote, mas não é responsabilidade do Trino. Algum pipeline precisa escrever os dados no destino que ele consultará.

  • Armazenamento: Um armazenamento de objetos, como S3 ou MinIO, mantém os arquivos de forma durável e barata. Ele entende objetos e caminhos, mas não sabe sozinho que um conjunto de arquivos representa uma tabela, qual schema está em vigor ou quais arquivos formam a versão atual dos dados.

  • Lakehouse: O Apache Iceberg organiza os arquivos como uma tabela. Ele registra snapshots, evolução de schema, partições e quais arquivos pertencem a cada versão. Isso permite que diferentes motores leiam o mesmo dado com uma interpretação consistente.

  • Catálogo: Antes de ler uma tabela Iceberg, o Trino precisa encontrá-la. O catálogo funciona como um diretório: relaciona o nome usado no SQL com os metadados que descrevem a versão atual da tabela. Esse catálogo pode ser um Hive Metastore, um REST Catalog ou outra implementação compatível.

  • Trino: Com a tabela localizada, o Trino descobre quais arquivos precisa ler, distribui o trabalho e combina os resultados. Ele é a camada que transforma SQL em processamento, mas não é dono do armazenamento nem do formato da tabela. Essa separação é o que permite trocar ou adicionar engines sem mover os dados.

  • ELT: Dados brutos raramente estão prontos para consumo. Uma camada de ELT, frequentemente organizada com dbt, transforma tabelas de origem em modelos de negócio, controla dependências e adiciona práticas como testes e documentação. O dbt não substitui o Trino: ele envia SQL para o Trino executar. O resultado da transformação volta para o armazenamento e passa novamente pelo formato de tabela e pelo catálogo.

  • Banco de serving para aplicações: Uma aplicação customer-facing nem sempre pode executar consultas analíticas complexas a cada requisição. Uma saída comum é consolidar o resultado no Trino e materializá-lo em PostgreSQL ou MySQL, onde a aplicação faz leituras mais simples e previsíveis. Esse banco acrescenta outra cópia dos dados e exige um processo para mantê-la sincronizada com a camada analítica.

O custo da stack não está em uma peça isolada. Está na soma das fronteiras: contratos de integração, versões compatíveis, autenticação, rede, observabilidade e recuperação de falhas entre projetos com ciclos de vida diferentes.

Como o Doris consolida o mesmo caminho

O Doris organiza essas responsabilidades como partes de um mesmo banco analítico. No modo internal storage, dois papéis formam o núcleo da arquitetura:

  • Frontend: recebe conexões, mantém o catálogo, analisa o SQL e cria o plano da consulta.
  • Backend: armazena os dados e executa as partes do plano distribuídas pelo Frontend.

Essa divisão ainda permite escalar o sistema, mas mantém catálogo, armazenamento e execução sob o mesmo produto e o mesmo modelo operacional.

O diagrama agrupa responsabilidades. Uma implantação pode ter vários processos Frontend e Backend.

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#F7F7F5",
    "primaryTextColor": "#1F2937",
    "primaryBorderColor": "#D6D3D1",
    "secondaryColor": "#EEEDE8",
    "tertiaryColor": "#FFF4DB",
    "lineColor": "#6B7280",
    "clusterBkg": "#EEEDE8",
    "clusterBorder": "#D6D3D1",
    "fontFamily": "Inter, Segoe UI, Helvetica Neue, Arial, sans-serif",
    "fontSize": "15px"
  },
  "flowchart": {"curve": "basis", "nodeSpacing": 28, "rankSpacing": 34, "padding": 8}
}}%%
flowchart LR
    ING["Ingestão"]
    BI["BI / análise interna"]
    APP["Aplicação<br/>customer-facing"]

    subgraph DORIS["Apache Doris · internal storage"]
        direction LR
        FRONTEND["Frontend<br/>(catálogo · SQL · planejamento)"]
        BACKEND["Backend<br/>(execução distribuída · armazenamento local<br/>índices · materialized views)"]

        FRONTEND <--> BACKEND
    end

    ING --> FRONTEND
    BI <--> FRONTEND
    APP <--> FRONTEND

    classDef muted fill:#F7F7F5,stroke:#D6D3D1,color:#4B5563,stroke-width:1px;
    classDef active fill:#FFF4DB,stroke:#B45309,color:#1F2937,stroke-width:2px;
    class ING,BI,APP muted;
    class FRONTEND,BACKEND active;

A ingestão chega diretamente às tabelas

O Doris oferece caminhos próprios para cargas contínuas, envios via aplicação e importações em lote. Isso não elimina integrações especializadas em todos os cenários, mas reduz a necessidade de construir um pipeline externo apenas para colocar dados no banco.

Com a tabela bronze_orders já criada, uma ingestão contínua de eventos JSON publicados em Kafka pode ser configurada com um único comando:

CREATE ROUTINE LOAD analytics.load_bronze_orders
ON bronze_orders
COLUMNS(order_id, customer_id, status, total_amount, created_at)
PROPERTIES (
    "format" = "json",
    "jsonpaths" = "[\"$.order_id\",\"$.customer_id\",\"$.status\",\"$.total_amount\",\"$.created_at\"]"
)
FROM KAFKA (
    "kafka_broker_list" = "kafka:9092",
    "kafka_topic" = "orders",
    "property.kafka_default_offsets" = "OFFSET_END"
);

Depois de criado, o Routine Load permanece ativo e transforma as mensagens do tópico em escritas na tabela. Não é necessário manter um agendador externo apenas para repetir a carga.

Tabelas Unique Key também aceitam atualização por chave. Em vez de tratar toda mudança como um conjunto de arquivos que será reconciliado durante a leitura, o Doris pode resolver versões da mesma chave durante a escrita.

Materialized views aproximam transformação e serving

Materialized views armazenam antecipadamente o resultado de uma transformação para evitar que cada consulta repita o mesmo trabalho. No Doris, elas fazem parte do próprio mecanismo de otimização e podem ser escolhidas automaticamente quando uma consulta compatível chega.

O exemplo abaixo usa bronze_orders e bronze_order_items como tabelas de entrada e cria uma camada silver. A materialized view mantém apenas pedidos pagos, combina pedidos e itens e calcula o valor bruto de cada item:

CREATE MATERIALIZED VIEW silver_order_items
BUILD IMMEDIATE
REFRESH AUTO ON SCHEDULE EVERY 10 MINUTE
DISTRIBUTED BY HASH(order_id) BUCKETS 8
AS
SELECT
    orders.order_id,
    orders.customer_id,
    DATE(orders.created_at) AS order_date,
    items.product_id,
    items.quantity,
    items.quantity * items.unit_price AS gross_value
FROM bronze_orders AS orders
JOIN bronze_order_items AS items
    ON orders.order_id = items.order_id
WHERE orders.status = 'PAID';

BUILD IMMEDIATE cria a primeira versão com os dados existentes. REFRESH AUTO ON SCHEDULE EVERY 10 MINUTE atualiza a materialized view a cada dez minutos. Isso automatiza a passagem de bronze para silver, mas os dados podem permanecer desatualizados até o próximo ciclo.

Bronze e silver são apenas nomes de camadas neste exemplo. Para o Doris, bronze_orders e bronze_order_items são tabelas internas, enquanto silver_order_items é uma materialized view consultável.

Isso cobre parte do espaço ocupado por modelos materializados em uma camada ELT. Não substitui toda a disciplina de dbt — testes, documentação, linhagem e orquestração continuam relevantes —, mas aproxima transformação e serving dentro do mesmo sistema.

O catálogo e o planejamento já fazem parte do banco

Tabelas internas são conhecidas diretamente pelo Doris. Não é necessário implantar um catálogo separado apenas para que a própria Engine de Consulta encontre seus dados.

Quando há fontes externas, o Multi-Catalog permite consultá-las sem copiar tudo previamente. Nesse cenário, os sistemas externos continuam existindo, mas o Doris pode assumir também parte do trabalho de federação que normalmente justificaria o Trino.

O armazenamento conhece o engine que fará a leitura

O Doris grava suas tabelas internas em um formato colunar próprio nos dois modos de implantação. O que muda é onde esses arquivos ficam: no internal storage, segmentos e índices permanecem nos discos dos processos Backend; no decoupled, os mesmos tipos de arquivo ficam no armazenamento compartilhado, enquanto os processos Backend mantêm um cache local.

Nos dois casos, o Doris controla a organização física, os índices e as estratégias de leitura. Esse acoplamento reduz abertura, mas também remove etapas intermediárias: a Engine de Consulta não precisa interpretar um formato genérico mantido para vários motores.

O protocolo MySQL reduz a fricção de consumo

Clientes e ferramentas de BI podem se conectar usando o protocolo MySQL. Essa compatibilidade não torna o SQL do Doris idêntico ao MySQL em todos os detalhes, mas reduz a necessidade de introduzir um protocolo exclusivo na ponta de consumo.

O próprio Doris pode atender aplicações customer-facing

Quando os dados consolidados e as materialized views já estão no Doris, uma aplicação pode consultá-los diretamente pelo protocolo MySQL. Nesse desenho, o mesmo sistema que recebe e transforma os dados também responde às leituras do produto, sem exigir uma cópia adicional em PostgreSQL ou MySQL apenas para formar a camada de serving.

É esse caminho que aproxima o Doris de customer-facing analytics. O projeto apresenta dashboards incorporados, portais para clientes e lojistas e produtos de dados como casos de uso próprios, apoiados por baixa latência e alta concorrência.

Isso não transforma o Doris em um banco transacional de propósito geral. Se a aplicação precisa de transações operacionais, integridade referencial complexa ou escrita registro a registro como parte do fluxo principal do produto, PostgreSQL ou MySQL continuam tendo um papel próprio. A consolidação vale para o serving analítico.

O trade-off entre federação e serving integrado

Na consulta federada, o problema principal é alcançar dados que continuam distribuídos entre sistemas diferentes. No serving, o problema passa a ser manter modelos analíticos prontos para leituras repetidas, concorrentes e com latência previsível.

Trino e Doris participam dos dois cenários. A escolha não é entre arquiteturas “certas” e “erradas”, mas entre lugares diferentes para carregar a complexidade: distribuí-la entre componentes especializados ou concentrar mais controle em um único projeto.

Trino + Iceberg: levar a consulta até os dados

O Trino oferece uma camada SQL comum sobre fontes heterogêneas sem exigir que todos os dados sejam movidos para um único banco. Com o Iceberg, os arquivos permanecem em um formato compartilhável e podem ser acessados por diferentes motores.

Essa autonomia favorece exploração, cruzamento e migração de dados. Em contrapartida, ingestão, catálogo, transformações e disponibilidade das fontes permanecem distribuídos. Se uma aplicação exigir outro perfil de leitura, o resultado ainda pode precisar ser materializado em uma camada de serving.

Doris: trazer o caminho crítico para dentro

No Doris, os dados essenciais ao serving podem ser ingeridos, transformados em materialized views e consultados pela aplicação no mesmo sistema. O caminho crítico permanece em representações preparadas para responder repetidamente às perguntas do produto.

O Multi-Catalog ainda permite consultar fontes externas. A vantagem de integração aparece quando os dados que sustentam o serving são persistidos ou materializados no Doris, que passa a controlar armazenamento, transformação e leitura.

O nível de consolidação depende do modo de implantação.

Doris internal storage

É a configuração com maior consolidação. Catálogo, execução e armazenamento pertencem ao Doris, e o número de processos necessários para formar o núcleo do banco é menor.

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#F7F7F5",
    "primaryTextColor": "#1F2937",
    "primaryBorderColor": "#D6D3D1",
    "secondaryColor": "#EEEDE8",
    "tertiaryColor": "#FFF4DB",
    "lineColor": "#6B7280",
    "clusterBkg": "#EEEDE8",
    "clusterBorder": "#D6D3D1",
    "fontFamily": "Inter, Segoe UI, Helvetica Neue, Arial, sans-serif",
    "fontSize": "15px"
  },
  "flowchart": {"curve": "linear", "nodeSpacing": 28, "rankSpacing": 32, "padding": 8}
}}%%
flowchart LR
    FRONTEND_INTERNAL["Frontend<br/>catálogo · SQL · planejamento"]
    BACKEND_INTERNAL["Backends<br/>execução + segmentos e índices locais"]

    FRONTEND_INTERNAL <--> BACKEND_INTERNAL

    classDef integrated fill:#FFF4DB,stroke:#B45309,color:#1F2937,stroke-width:2px;
    class FRONTEND_INTERNAL,BACKEND_INTERNAL integrated;

É a opção mais coerente quando a prioridade é colocar uma camada de serving no ar com poucas dependências e não há necessidade de escalar armazenamento separadamente.

Doris decoupled

O modo decoupled separa processamento e armazenamento. Para isso, adiciona serviços de metadados e um armazenamento compartilhado. Há mais componentes do que no internal storage.

%%{init: {
  "theme": "base",
  "themeVariables": {
    "primaryColor": "#F7F7F5",
    "primaryTextColor": "#1F2937",
    "primaryBorderColor": "#D6D3D1",
    "secondaryColor": "#EEEDE8",
    "tertiaryColor": "#FFF4DB",
    "lineColor": "#6B7280",
    "clusterBkg": "#EEEDE8",
    "clusterBorder": "#D6D3D1",
    "fontFamily": "Inter, Segoe UI, Helvetica Neue, Arial, sans-serif",
    "fontSize": "15px"
  },
  "flowchart": {"curve": "linear", "nodeSpacing": 36, "rankSpacing": 42, "padding": 10}
}}%%
flowchart TB
    FRONTEND_DECOUPLED["Frontend<br/>catálogo · SQL · planejamento"]
    BACKEND_DECOUPLED["Backends<br/>execução + cache local"]
    OBJ["Armazenamento compartilhado<br/>segmentos + índices do Doris"]
    MDB["Base de metadados"]
    MS["Meta Service"]

    FRONTEND_DECOUPLED --> BACKEND_DECOUPLED
    MDB --> MS
    MS --> BACKEND_DECOUPLED
    BACKEND_DECOUPLED --> OBJ

    classDef integrated fill:#FFF4DB,stroke:#B45309,color:#1F2937,stroke-width:2px;
    classDef external fill:#F7F7F5,stroke:#D6D3D1,color:#4B5563,stroke-width:1px;
    class FRONTEND_DECOUPLED,BACKEND_DECOUPLED,MS integrated;
    class MDB,OBJ external;

A diferença em relação à stack do Trino é que essa composição faz parte da arquitetura oficial do Doris. Frontend, Backend e Meta Service pertencem ao mesmo projeto; armazenamento compartilhado e base de metadados continuam sendo dependências externas. Mesmo assim, a topologia e os contratos entre essas peças são definidos pelo Doris. Não é necessário desenhar a integração entre uma Engine de Consulta, um formato de tabela e um catálogo independentes para formar o caminho principal.

Portanto, decoupled é mais complexo que internal, mas ainda preserva uma vantagem de coesão sobre uma stack montada com projetos distintos.

A diferença está na fronteira de controle

As capacidades não formam uma divisão rígida. O Trino também pode atender consultas de alta escala e baixa latência quando as fontes e a arquitetura estão preparadas para isso. O Doris também pode federar consultas sobre sistemas externos. A distinção está na responsabilidade assumida por padrão:

Pergunta arquiteturalTrino + IcebergApache Doris
Qual é o produto central?Uma Engine SQL integrada a uma stack abertaUm Data Warehouse analítico integrado
Qual problema orienta o desenho?Consultar e combinar dados onde eles já estãoIngerir, organizar e servir dados analíticos
Onde os dados ficam?No Data Lake ou nas fontes acessadas por conectoresEm tabelas internas ou em fontes externas
Quem controla o formato físico?As fontes ou os componentes de armazenamento da stackO Doris, para tabelas internas
Onde ficam transformações persistidas?Em componentes e destinos definidos pela stackPodem ficar em materialized views do próprio Doris
Como o serving entra no caminho?Pode exigir uma camada persistida adicionalPode consultar diretamente os modelos mantidos no Doris
Qual abertura recebe prioridade?Interoperabilidade entre fontes, formatos e motoresIntegração do caminho crítico, sem perder acesso a fontes externas

Se o desafio central é oferecer uma interface SQL sobre dados heterogêneos e preservar a independência entre armazenamento e Engine de Consulta, Trino + Iceberg parte de uma posição mais natural. Se o desafio é manter um caminho contínuo entre ingestão, transformação e serving analítico, com menos contratos entre sistemas, o Doris assume mais dessas responsabilidades.

Se a prioridade é…A opção mais coerente tende a ser…
Menos componentes e operação diretaDoris internal storage
Escalar armazenamento e processamento separadamenteDoris decoupled
Compartilhar tabelas abertas entre vários motoresTrino + Iceberg
Federar muitas fontes sem centralizar os dadosTrino

Teste de carga

A comparação arquitetural mostra por que o Doris pode ser mais simples. Falta verificar se essa consolidação preserva a capacidade de servir consultas analíticas com baixa latência.

Três arquiteturas sobre os mesmos dados

O teste usou o mesmo dataset nas três configurações:

ConfiguraçãoOrganização dos dados
Doris internal storageTabelas no armazenamento local controlado pelo Doris
Doris decoupledTabelas no formato do Doris sobre armazenamento compartilhado
Trino + IcebergTabelas Iceberg consultadas pelo Trino

As três receberam os mesmos dados e executaram consultas equivalentes. O objetivo não era simular toda a variedade de uma plataforma de produção, mas manter as entradas iguais para observar como cada arquitetura respondia ao mesmo trabalho.

TabelaLinhasParticipação no dataset
users131.0964,77%
addresses197.0007,16%
products65.4512,38%
orders655.06623,83%
order_items1.047.46038,10%
payments653.41623,76%
Total2.749.489100%

Três padrões de consulta

As consultas foram escolhidas para exercitar padrões analíticos diferentes.

1. Agregação de receita

A primeira consulta percorre pedidos e itens não cancelados, associa cada item à categoria do produto e calcula volume e receita por mês:

Ver SQL da agregação de receita
SELECT
    date_trunc(o.created_at, 'month') AS month,
    p.category,
    COUNT(DISTINCT o.id) AS orders_count,
    SUM(oi.quantity) AS items_sold,
    SUM(oi.quantity * oi.unit_price) AS revenue
FROM bronze.orders AS o
JOIN bronze.order_items AS oi
    ON oi.order_id = o.id
JOIN bronze.products AS p
    ON p.id = oi.product_id
WHERE o.status <> 'cancelled'
GROUP BY 1, 2
ORDER BY month, revenue DESC;

2. Visão 360 do pedido

A segunda consulta constrói uma visão analítica do pedido. Ela seleciona o endereço mais recente de cada usuário e combina seis tabelas para devolver cliente, pedido, itens, produtos e pagamento:

Ver SQL da visão 360 do pedido
WITH primary_address AS (
    SELECT user_id, city, state
    FROM (
        SELECT
            user_id,
            city,
            state,
            ROW_NUMBER() OVER (
                PARTITION BY user_id
                ORDER BY created_at DESC
            ) AS rn
        FROM bronze.addresses
    ) AS ranked
    WHERE rn = 1
)
SELECT
    u.id AS user_id,
    u.name AS user_name,
    a.city,
    a.state,
    o.id AS order_id,
    o.created_at AS order_created_at,
    o.status AS order_status,
    p.sku,
    p.category,
    oi.quantity,
    oi.unit_price,
    pay.method AS payment_method,
    pay.status AS payment_status
FROM bronze.orders AS o
JOIN bronze.users AS u
    ON u.id = o.user_id
LEFT JOIN primary_address AS a
    ON a.user_id = u.id
JOIN bronze.order_items AS oi
    ON oi.order_id = o.id
JOIN bronze.products AS p
    ON p.id = oi.product_id
LEFT JOIN bronze.payments AS pay
    ON pay.order_id = o.id
WHERE o.created_at >= CURRENT_DATE - INTERVAL '30' DAY
ORDER BY o.created_at DESC, o.id, oi.id
LIMIT 500;

3. Ranking de produtos

A terceira consulta agrega unidades e receita por produto. Em seguida, uma função de janela cria um ranking dentro de cada categoria e mantém as cinco primeiras posições:

Ver SQL do ranking de produtos
WITH product_revenue AS (
    SELECT
        p.id AS product_id,
        p.sku,
        p.name,
        p.category,
        SUM(oi.quantity) AS units_sold,
        SUM(oi.quantity * oi.unit_price) AS revenue
    FROM bronze.order_items AS oi
    JOIN bronze.products AS p
        ON p.id = oi.product_id
    JOIN bronze.orders AS o
        ON o.id = oi.order_id
    WHERE o.status <> 'cancelled'
    GROUP BY 1, 2, 3, 4
),
ranked AS (
    SELECT
        *,
        RANK() OVER (
            PARTITION BY category
            ORDER BY revenue DESC
        ) AS category_rank
    FROM product_revenue
)
SELECT
    category,
    category_rank,
    sku,
    name,
    units_sold,
    revenue
FROM ranked
WHERE category_rank <= 5
ORDER BY category, category_rank;

Cenário 1: uma consulta por vez

Cada consulta foi executada isoladamente, com um usuário e 29 execuções por configuração. Esse cenário remove a disputa entre usuários e mostra a latência de cada tipo de trabalho.

{
  "type": "bar",
  "title": "Latência mediana por consulta sem concorrência",
  "subtitle": "1 usuário · 29 execuções por consulta e configuração",
  "unit": "ms",
  "categories": ["Agregação", "Visão 360", "Ranking"],
  "series": [
    { "key": "doris-internal", "name": "Doris internal storage", "values": [59, 160, 53] },
    { "key": "doris-decoupled", "name": "Doris decoupled storage", "values": [53, 160, 53] },
    { "key": "trino", "name": "Trino + Iceberg", "values": [560, 2100, 490] }
  ],
  "note": "Os valores exatos aparecem sobre as barras."
}

Cada célula apresenta média / mediana / p95, em milissegundos:

ConsultaDoris internalDoris decoupledTrino + Iceberg
Agregação de receita75,7 / 59 / 7257,1 / 53 / 81558,5 / 560 / 580
Visão 360 do pedido170,3 / 160 / 210170,6 / 160 / 1702.149,1 / 2.100 / 2.300
Ranking de produtos54,5 / 53 / 6255,7 / 53 / 61494,1 / 490 / 520

Pelas medianas, o Trino levou de 9,2 a 13,1 vezes mais tempo que o Doris, dependendo da consulta.

O p95 mostra o tempo em que 95% das execuções foram concluídas e acrescenta uma leitura da cauda sem concentrar a análise apenas na chamada mais lenta. Com 29 execuções por combinação, ele ainda deve ser tratado como um retrato da rodada, não como uma projeção estável para produção.

Cenário 2: dez usuários concorrentes

No segundo cenário, dez usuários executaram uma mistura das três consultas.

Cada configuração recebeu exatamente 500 requisições.

Com requisições contínuas, a diferença aumentou:

{
  "type": "bar",
  "title": "Latência mediana por consulta sob carga concorrente",
  "subtitle": "10 usuários · 500 requisições por configuração",
  "unit": "ms",
  "categories": ["Agregação", "Visão 360", "Ranking"],
  "series": [
    { "key": "doris-internal", "name": "Doris internal storage", "values": [66, 230, 64] },
    { "key": "doris-decoupled", "name": "Doris decoupled storage", "values": [64, 270, 66] },
    { "key": "trino", "name": "Trino + Iceberg", "values": [1800, 3000, 1800] }
  ],
  "note": "Os valores exatos aparecem sobre as barras."
}

Cada célula apresenta média / mediana / p95, em milissegundos:

ConsultaDoris internalDoris decoupledTrino + Iceberg
Agregação de receita83,6 / 66 / 20088,9 / 64 / 2201.812,8 / 1.800 / 2.400
Visão 360 do pedido261,0 / 230 / 480301,9 / 270 / 6803.059,4 / 3.000 / 3.600
Ranking de produtos88,2 / 64 / 21099,3 / 66 / 2901.870,5 / 1.800 / 2.400

Pelas medianas, o Trino levou de 11,1 a 28,1 vezes mais tempo que o Doris, dependendo da consulta e do modo de armazenamento.

O resultado das 500 requisições

Somar as três consultas mostra o desempenho da configuração durante as 500 requisições:

ConfiguraçãoDuraçãoRequisições/sFalhas
Doris internal~40 s12,70
Doris decoupled~40 s12,40
Trino + Iceberg~2 min 15 s3,70

Para concluir o mesmo trabalho, o Trino ocupou cerca de 3,38 vezes o tempo total e entregou aproximadamente 29% do throughput do internal. Nenhuma das três configurações registrou falha.

O que os resultados sustentam

Latência e consumo de recursos completam a comparação. A última coluna usa o Doris internal como referência:

MétricaDoris internalDoris decoupledTrino + IcebergTrino / Doris internal
Latência média108,5 ms123,5 ms2.005,8 ms18,49× mais lento
Mediana73 ms75 ms1.800 ms24,66× mais lento
p95320 ms360 ms3.100 ms9,69× mais lento
p99460 ms590 ms3.600 ms7,83× mais lento
CPU média89,8%140,3%24,3%27% da CPU
Memória média~3,1 GB~3,7 GB~9,1 GB2,91× a memória

O Doris usou mais CPU e concluiu as mesmas 500 requisições em menos tempo. O Trino usou aproximadamente 2,5 a 2,9 vezes mais memória, mas permaneceu abaixo de um quarto de núcleo em média.

Trino + Iceberg em relação ao Doris internal

A mediana do Trino foi 24,66 vezes a do Doris internal. Pela média, a distância ficou em 18,49 vezes. A cauda reduziu essa proporção, mas não inverteu o resultado: no p99, o Trino ficou 7,83 vezes acima.

A stack Trino + Iceberg usou cerca de 2,91 vezes a memória do Doris internal. O teste não isolou qual componente ou etapa explica cada diferença, mas mostra que, nessa carga, a abertura da stack não veio acompanhada de um serving equivalente ao Doris.

Doris decoupled em relação ao internal storage

Entre os modos do Doris, a distância foi bem menor. O decoupled ficou 14% acima do internal na latência média, 3% na mediana, 13% no p95 e 28% no p99. O throughput permaneceu próximo, em 98% do internal, enquanto a memória ficou 18% acima.

O teste não prova que Doris será sempre mais rápido do que Trino. Ele não cobre escala de bilhões de linhas, outros tipos de carga, ajustes avançados ou uma implantação distribuída de produção. O que sustenta é mais específico: nas condições avaliadas, os dois modos do Doris entregaram uma camada de serving mais rápida e com menor uso de memória, e a configuração mais consolidada também apresentou o melhor resultado geral.

Do serving ao tempo real

O teste começa quando os dados já estão disponíveis para consulta. Ele mostra que o Doris consegue responder rapidamente sobre um estado materializado, mas não mede quanto tempo uma mudança levou para chegar até esse estado.

Essa diferença coloca o Doris mais perto do tempo real sem torná-lo, por si só, uma arquitetura de streaming. No caminho apresentado neste artigo, a ingestão grava novos dados nas tabelas e a materialized view assíncrona publica outra representação a cada dez minutos. O intervalo é menor do que o de um batch tradicional, mas o controle continua baseado em ciclos: acumular mudanças, processar e publicar um novo estado.

Um batch menor continua sendo um batch.

Essa fronteira recupera as dores discutidas em Lakehouse não é a Solução. Uma mudança ainda pode esperar o próximo ciclo para se tornar visível; reduzir o intervalo aumenta a frequência de coordenação e processamento; e cada materialização precisa produzir um novo estado a partir dos dados de origem. O Doris muda o formato, reduz a quantidade de componentes e acelera o serving, mas não elimina a espera introduzida por uma estratégia baseada em micro-batches.

Uma consulta de 64 ms, portanto, ainda pode responder sobre um estado com quase dez minutos de atraso. Diminuir o refresh para um minuto encurta a espera, mas não altera o modelo: apenas executa o mesmo ciclo com mais frequência.

Uma arquitetura voltada ao tempo real precisa mudar a unidade de processamento. Em vez de esperar um lote fechar para produzir outra versão do estado, cada mudança deve percorrer transformações contínuas, atualizar o estado consultável de forma incremental e seguir para os próximos consumidores sem depender do fechamento do próximo lote.

Onde cada arquitetura faz sentido

  • Doris internal storage faz sentido quando o objetivo é colocar no ar uma camada analítica de baixa latência com poucas peças.
  • Doris decoupled faz sentido quando a elasticidade do armazenamento justifica mais componentes e algum overhead.
  • Trino + Iceberg continua sendo a escolha mais coerente quando abertura, interoperabilidade entre motores e federação pesam mais do que a consolidação.

O critério principal não é qual Engine de Consulta venceu um gráfico. É decidir onde a plataforma deve carregar sua complexidade: nas integrações de uma stack aberta ou dentro de um sistema que controla o caminho completo de serving.

Fontes principais