DE Trilhas

M09 β€” Case real: Uber Eats end-to-end em Azure

DuraΓ§Γ£o: 5h (escrita) + 6-8h (execuΓ§Γ£o real) PrΓ©-requisitos: M00-M08 ok e preferencialmente Owshq mod-2 concluΓ­do (vocΓͺ precisa ter visto Spark + Delta a fundo). Objetivo: pegar os datasets reais da FormaΓ§Γ£o Owshq (frm-spark-databricks-mec/entities/), simular 5 fontes operacionais distintas (Kafka, MongoDB, MSSQL, MySQL, Postgres), e construir o lakehouse Uber Eats end-to-end em Azure. Esse Γ© o case que vocΓͺ vai mostrar em entrevista.

Por que este case (e nΓ£o NYC Taxi do M04/M08)

NYC Taxi e AdventureWorks sΓ£o bons para aprender o serviΓ§o. Mas em entrevista Sr, o entrevistador pergunta:

  • β€œVocΓͺ jΓ‘ lidou com ingestΓ£o multi-fonte (Kafka + Mongo + relacional)?”
  • β€œComo vocΓͺ concilia o mesmo cliente vindo de duas fontes (CRM B2B no MSSQL + app B2C no MongoDB)?”
  • β€œComo funde stream de status com batch de pedidos?”
  • β€œComo modela um star schema quando os surrogate keys vΓͺm de chaves naturais inconsistentes (CPF, CNPJ, license)?”

Esses problemas estΓ£o neste case. O Uber Eats da mec foi desenhado exatamente para isso.

Fontes (5) e onde elas moram

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ FONTES OPERACIONAIS β”‚
β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€
β”‚ β”‚
β”‚ πŸ“± App B2C ──► MongoDB Atlas (mongodb_users) β”‚
β”‚ 🏒 CRM B2B ──► Azure SQL (mssql_users) β”‚
β”‚ πŸ” CatΓ‘logo ──► MySQL (mysql_restaurants, β”‚
β”‚ mysql_ratings) β”‚
β”‚ πŸ›΅ Motoristas ──► Postgres (postgres_drivers) β”‚
β”‚ πŸ“¦ Pedidos ──► Kafka/Event Hubs (kafka_orders, β”‚
β”‚ kafka_status) β”‚
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Schemas reais (de frm-spark-databricks-mec/entities/):

FonteCampos-chaveCardinalidade
mongodb_usersuser_id, user_identifier (CPF), email, delivery_address, country, city~milhΓ΅es (B2C)
mssql_usersuser_id, cpf, first_name, last_name, company_name, job, birthday~dezenas de milhares (B2B)
mysql_restaurantsrestaurant_id, cnpj, name, cuisine_type, city, average_rating, opening_time, closing_time~10k
mysql_ratingsrating_id, restaurant_identifier (CNPJ), rating (1-5), timestamp~10x restaurants
postgres_driversdriver_id, license_number, vehicle_make, vehicle_model, vehicle_year, city~100k
kafka_ordersorder_id, user_key (CPF), restaurant_key (CNPJ), driver_key (license), order_date, total_amount, payment_id~milhΓ΅es/dia
kafka_statusstatus_id, order_identifier, status.status_name, status.timestamp~6x orders (6 status por pedido)

Chaves naturais que ligam tudo:

  • orders.user_key (CPF) ↔ mongodb_users.user_identifier βˆͺ mssql_users.cpf
  • orders.restaurant_key (CNPJ) ↔ mysql_restaurants.cnpj
  • orders.driver_key (license) ↔ postgres_drivers.license_number

Mapeamento Fontes β†’ Azure (decisΓ£o de arquitetura)

Em dev/laboratΓ³rio, vamos provisionar tudo no Azure de forma econΓ΄mica:

Fonte originalEm Azure (dev)Custo aprox.
MongoDB AtlasCosmos DB MongoDB API (serverless) ou Atlas Free Tier$0-5/mΓͺs
MSSQL on-premAzure SQL Serverless (auto-pause 60min)$1-3/mΓͺs
MySQLAzure Database for MySQL Flexible (Burstable B1ms)$12/mΓͺs β€” alternativa: subir num container do Cosmos DB ou sΓ³ ingerir do JSON direto
PostgresAzure Database for PostgreSQL Flexible (Burstable B1ms)$12/mΓͺs β€” alternativa: idem
KafkaEvent Hubs (Standard, 1 TU)$11/mΓͺs

Economia agressiva para a trilha: as 3 fontes β€œMySQL/Postgres/MongoDB” podem ser substituΓ­das por JSON files no Bronze (vocΓͺ sobe os entities/*.json direto no ADLS). SΓ³ Azure SQL + Event Hubs ficam β€œreais”. Isso baixa o custo do case para ~$15/mΓͺs. Recomendo essa simplificaΓ§Γ£o na primeira execuΓ§Γ£o β€” vocΓͺ prova a arquitetura sem queimar crΓ©dito.

Arquitetura final

β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ AZURE DATA FACTORY β”‚
β”‚ trigger diΓ‘rio 06h BRT + trigger streaming β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”˜
β”‚ β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”Όβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β–Ό β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Azure SQL β”‚ β”‚ JSON uploads β”‚ (mongo/mysql/postgres β†’ ADLS direto)
β”‚ (mssql_users)β”‚ β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ Copy Activity (batch diΓ‘rio)
β–Ό
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β” β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ ADLS Gen2 β€” Bronze β”‚ ◄────── β”‚ Event Hubs ──┐ β”‚
β”‚ /source=<src>/ β”‚ β”‚ (streaming) β”‚ β”‚
β”‚ /table=<t>/ β”‚ β”‚ kafka_ordersβ”‚ β”‚
β”‚ /ing_date=<d>/ β”‚ β”‚ kafka_statusβ”‚ β”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”¬β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜ β”‚
β”‚ β”‚
β”‚ Notebook batch + Auto Loader streaming β”‚
β–Ό β”‚
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚ AZURE DATABRICKS β€” silver_dev.ubereats.*
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ users_unified (MSSQL βˆͺ MongoDB) β”‚
β”‚ β”‚ restaurants β”‚
β”‚ β”‚ ratings β”‚
β”‚ β”‚ drivers β”‚
β”‚ β”‚ orders (MERGE incremental) β”‚
β”‚ β”‚ order_status (streaming, append) β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό Notebook agregaΓ§Γ£o
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ gold_dev.ubereats.*
β”‚ β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ β”‚ dim_user (SCD2) β”‚
β”‚ β”‚ dim_restaurant (SCD1) β”‚
β”‚ β”‚ dim_driver β”‚
β”‚ β”‚ dim_date β”‚
β”‚ β”‚ dim_status β”‚
β”‚ β”‚ fact_orders (granularidade: pedido) β”‚
β”‚ β”‚ fact_order_status (granularidade: evento) β”‚
β”‚ β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜
β”‚
β–Ό SQL Warehouse Pro Serverless
β”Œβ”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”
β”‚ Power BI β”‚
β”‚ - SLA entregaβ”‚
β”‚ - GMV por β”‚
β”‚ restauranteβ”‚
β”‚ - Top motoristaβ”‚
β””β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”€β”˜

Parte 1 β€” Provisionar recursos (modo econΓ΄mico)

VariΓ‘veis (segue convenΓ§Γ£o das trilhas anteriores):

Terminal window
$PROJ = "de"; $INIC = "w"; $AMB = "dev"; $REGS = "brs"; $REG = "brazilsouth"
$RG = "rg-$PROJ-$AMB-$REGS"
$ST = "st$PROJ$INIC$AMB$REGS" # do M01
# Recursos novos do M09:
$EH = "eh-$PROJ-$AMB-ubereats-$REGS" # Event Hubs Namespace
$SQL = "sql-$PROJ-$INIC-$AMB" # jΓ‘ existe do M05, reaproveita

Passo 1.1 β€” Event Hubs (kafka_orders + kafka_status)

Terminal window
# Namespace (1 TU = $11/mΓͺs; suficiente pra dev)
az eventhubs namespace create `
--resource-group $RG `
--name $EH `
--location $REG `
--sku Standard `
--capacity 1 `
--enable-auto-inflate false
# 2 event hubs (1 por tΓ³pico)
az eventhubs eventhub create --resource-group $RG --namespace-name $EH `
--name orders --partition-count 4 --message-retention 7
az eventhubs eventhub create --resource-group $RG --namespace-name $EH `
--name order-status --partition-count 4 --message-retention 7
# Connection string (guarda no Key Vault)
$EHCONN = az eventhubs namespace authorization-rule keys list `
--resource-group $RG --namespace-name $EH --name RootManageSharedAccessKey `
--query "primaryConnectionString" -o tsv
az keyvault secret set --vault-name $KV --name "eh-connstr" --value $EHCONN
Remove-Variable EHCONN

Passo 1.2 β€” Upload dos JSON files no ADLS (modo econΓ΄mico)

Os 5 arquivos (mongodb_users.json, mssql_users.json, mysql_ratings.json, mysql_restaurants.json, postgres_drivers.json) viram arquivos no Bronze simulando o resultado de uma copy diΓ‘ria.

Terminal window
$MECPATH = "C:\Users\willi\OneDrive\Documents\Projetos\Data Engineer\frm-databricks\frm-spark-databricks-mec\entities"
$TODAY = Get-Date -Format "yyyy-MM-dd"
$mapping = @{
"mongodb_users.json" = "source=mongodb/table=users"
"mssql_users.json" = "source=mssql/table=users"
"mysql_ratings.json" = "source=mysql/table=ratings"
"mysql_restaurants.json" = "source=mysql/table=restaurants"
"postgres_drivers.json" = "source=postgres/table=drivers"
}
foreach ($file in $mapping.Keys) {
$path = $mapping[$file]
az storage blob upload `
--account-name $ST `
--auth-mode login `
--container-name bronze `
--name "$path/ingestion_date=$TODAY/$file" `
--file "$MECPATH\$file" `
--overwrite
}
# Os arquivos kafka_orders.json e kafka_status.json vocΓͺ manda pro Event Hubs
# via script Python (prΓ³xima seΓ§Γ£o). NΓ£o sobe no Bronze direto.

Passo 1.3 β€” Produtor Kafka simulado (envia para Event Hubs)

Crie tools/seed_eventhubs.py na sua mΓ‘quina:

"""
LΓͺ kafka_orders.json e kafka_status.json da pasta entities/
e publica os eventos no Event Hubs simulando produΓ§Γ£o real.
"""
import json, os, time, random
from azure.eventhub import EventHubProducerClient, EventData
CONN_STR = os.environ["EH_CONN_STR"] # cola do KV antes de rodar
MEC_PATH = r"C:\Users\willi\OneDrive\Documents\Projetos\Data Engineer\frm-databricks\frm-spark-databricks-mec\entities"
def send_to_hub(hub_name: str, jsonl_path: str, throttle_ms: int = 50):
producer = EventHubProducerClient.from_connection_string(CONN_STR, eventhub_name=hub_name)
batch = producer.create_batch()
sent = 0
with open(jsonl_path, "r", encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line: continue
ev = EventData(line)
try:
batch.add(ev)
except ValueError:
producer.send_batch(batch)
batch = producer.create_batch()
batch.add(ev)
sent += 1
if throttle_ms: time.sleep(throttle_ms / 1000)
if len(batch) > 0:
producer.send_batch(batch)
producer.close()
print(f"{hub_name}: {sent} eventos enviados")
if __name__ == "__main__":
send_to_hub("orders", os.path.join(MEC_PATH, "kafka_orders.json"))
send_to_hub("order-status", os.path.join(MEC_PATH, "kafka_status.json"))

Rode:

Terminal window
$env:EH_CONN_STR = az keyvault secret show --vault-name $KV --name "eh-connstr" --query value -o tsv
pip install azure-eventhub
python tools/seed_eventhubs.py

Em produΓ§Γ£o real, esse seeder seria substituΓ­do por um produtor Kafka rodando no serviΓ§o do app/CRM. Aqui simulamos.

Parte 2 β€” Schemas Unity Catalog

Antes de escrever notebooks, prepare os schemas:

CREATE SCHEMA IF NOT EXISTS silver_dev.ubereats
COMMENT 'Camada Silver - case Uber Eats (M09)';
CREATE SCHEMA IF NOT EXISTS gold_dev.ubereats
COMMENT 'Camada Gold - case Uber Eats (M09)';

Parte 3 β€” Notebook 1: Bronze β†’ Silver (batch das 5 fontes)

Crie notebooks/M09/10_ubereats_bronze_to_silver.py:

# Databricks notebook source
# MAGIC %md # M09 β€” Bronze β†’ Silver (Uber Eats batch)
# COMMAND ----------
from pyspark.sql import functions as F
from pyspark.sql.types import *
from delta.tables import DeltaTable
dbutils.widgets.text("storage_account", "")
dbutils.widgets.text("ingestion_date", "")
storage = dbutils.widgets.get("storage_account")
ing_date = dbutils.widgets.get("ingestion_date")
bronze_root = f"abfss://bronze@{storage}.dfs.core.windows.net"
# Helper genΓ©rico
def merge_jsonl_to_silver(source: str, table: str, key_cols: list[str], select_expr: dict[str, str]):
path = f"{bronze_root}/source={source}/table={table}/ingestion_date={ing_date}/"
df = spark.read.json(path)
df = df.selectExpr(*[f"{v} AS {k}" for k, v in select_expr.items()]) \
.withColumn("_silver_ingested_at", F.current_timestamp()) \
.withColumn("_silver_source", F.lit(source))
target = f"silver_dev.ubereats.{table}"
if not spark.catalog.tableExists(target):
df.write.format("delta").saveAsTable(target)
return df.count(), 0
t = DeltaTable.forName(spark, target)
cond = " AND ".join([f"t.{c} = s.{c}" for c in key_cols])
t.alias("t").merge(df.alias("s"), cond) \
.whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()
return df.count(), -1
# COMMAND ----------
# MAGIC %md ## 3.1 β€” restaurants (MySQL β†’ silver)
merge_jsonl_to_silver(
source="mysql", table="restaurants",
key_cols=["restaurant_id"],
select_expr={
"restaurant_id": "CAST(restaurant_id AS INT)",
"cnpj": "TRIM(cnpj)",
"name": "name",
"cuisine_type": "cuisine_type",
"city": "city",
"country": "country",
"phone_number": "phone_number",
"address": "address",
"opening_time": "opening_time",
"closing_time": "closing_time",
"average_rating": "CAST(average_rating AS DOUBLE)",
"num_reviews": "CAST(num_reviews AS INT)",
"uuid": "uuid",
"_src_ts": "CAST(dt_current_timestamp AS TIMESTAMP)",
}
)
# COMMAND ----------
# MAGIC %md ## 3.2 β€” ratings (MySQL β†’ silver)
merge_jsonl_to_silver(
source="mysql", table="ratings",
key_cols=["rating_id"],
select_expr={
"rating_id": "CAST(rating_id AS INT)",
"restaurant_cnpj": "TRIM(restaurant_identifier)",
"rating": "CAST(rating AS INT)",
"rated_at": "CAST(timestamp AS TIMESTAMP)",
"uuid": "uuid",
"_src_ts": "CAST(dt_current_timestamp AS TIMESTAMP)",
}
)
# COMMAND ----------
# MAGIC %md ## 3.3 β€” drivers (Postgres β†’ silver)
merge_jsonl_to_silver(
source="postgres", table="drivers",
key_cols=["driver_id"],
select_expr={
"driver_id": "CAST(driver_id AS INT)",
"license_number": "TRIM(license_number)",
"first_name": "first_name",
"last_name": "last_name",
"phone_number": "phone_number",
"city": "city",
"country": "country",
"date_birth": "CAST(date_birth AS DATE)",
"vehicle_type": "vehicle_type",
"vehicle_make": "vehicle_make",
"vehicle_model": "vehicle_model",
"vehicle_year": "CAST(vehicle_year AS INT)",
"vehicle_license_plate": "vehicle_license_plate",
"uuid": "uuid",
"_src_ts": "CAST(dt_current_timestamp AS TIMESTAMP)",
}
)
# COMMAND ----------
# MAGIC %md ## 3.4 β€” users B2C (MongoDB) e B2B (MSSQL) β†’ silver unificado
# MAGIC
# MAGIC Desafio: o mesmo CPF pode existir em ambos. EstratΓ©gia:
# MAGIC 1. Gravar em duas tabelas separadas (`users_b2c`, `users_b2b`) primeiro.
# MAGIC 2. Construir uma view unificada `users_unified` que prioriza MSSQL (mais completo: birthday, job, company).
# MAGIC 3. Marca origem em `_source_priority`.
merge_jsonl_to_silver(
source="mongodb", table="users_b2c",
key_cols=["user_id"],
select_expr={
"user_id": "CAST(user_id AS INT)",
"cpf": "TRIM(user_identifier)",
"email": "LOWER(TRIM(email))",
"phone_number": "phone_number",
"city": "city",
"country": "country",
"delivery_address": "delivery_address",
"uuid": "uuid",
"_src_ts": "CAST(dt_current_timestamp AS TIMESTAMP)",
}
)
merge_jsonl_to_silver(
source="mssql", table="users_b2b",
key_cols=["user_id"],
select_expr={
"user_id": "CAST(user_id AS INT)",
"cpf": "TRIM(cpf)",
"first_name": "first_name",
"last_name": "last_name",
"company_name": "company_name",
"job": "job",
"birthday": "CAST(birthday AS DATE)",
"phone_number": "phone_number",
"country": "country",
"uuid": "uuid",
"_src_ts": "CAST(dt_current_timestamp AS TIMESTAMP)",
}
)
# COMMAND ----------
# MAGIC %md ### users_unified
# MAGIC Tabela canΓ΄nica resolvendo o mesmo CPF em ambas as fontes. Chave: cpf.
spark.sql("""
CREATE OR REPLACE TABLE silver_dev.ubereats.users_unified
USING DELTA AS
WITH b2b AS (
SELECT cpf, first_name, last_name, company_name, job, birthday, phone_number, country,
CAST(NULL AS STRING) AS email, CAST(NULL AS STRING) AS city, CAST(NULL AS STRING) AS delivery_address,
'b2b' AS source_priority, _src_ts
FROM silver_dev.ubereats.users_b2b
),
b2c AS (
SELECT cpf,
CAST(NULL AS STRING) AS first_name,
CAST(NULL AS STRING) AS last_name,
CAST(NULL AS STRING) AS company_name,
CAST(NULL AS STRING) AS job,
CAST(NULL AS DATE) AS birthday,
phone_number, country, email, city, delivery_address,
'b2c' AS source_priority, _src_ts
FROM silver_dev.ubereats.users_b2c
),
unioned AS (
SELECT * FROM b2b
UNION ALL
SELECT * FROM b2c
),
ranked AS (
SELECT *,
ROW_NUMBER() OVER (
PARTITION BY cpf
ORDER BY CASE source_priority WHEN 'b2b' THEN 1 WHEN 'b2c' THEN 2 END, _src_ts DESC
) AS _rn
FROM unioned
)
SELECT cpf, first_name, last_name, company_name, job, birthday,
phone_number, country, email, city, delivery_address,
source_priority, _src_ts AS source_updated_at,
CURRENT_TIMESTAMP() AS _silver_built_at
FROM ranked
WHERE _rn = 1
""")
display(spark.sql("""
SELECT source_priority, COUNT(*) qtd
FROM silver_dev.ubereats.users_unified
GROUP BY source_priority
"""))
# COMMAND ----------
# MAGIC %md ## 3.5 β€” orders (Event Hubs batch read)
# LΓͺ eventos acumulados do EH via Spark (nΓ£o streaming aqui)
eh_conn = dbutils.secrets.get("kv", "eh-connstr")
eh_conf = {
"eventhubs.connectionString": spark._jvm.org.apache.spark.eventhubs.EventHubsUtils.encrypt(eh_conn),
"eventhubs.consumerGroup": "$Default",
"eventhubs.startingPosition": '{"offset":"-1","seqNo":-1,"enqueuedTime":null,"isInclusive":true}'
}
df_raw_orders = (
spark.read.format("eventhubs")
.options(**{**eh_conf, "eventhubs.name": "orders"})
.load()
)
# Payload do EH vem em "body" (binary). Decode + parse JSON.
order_schema = StructType([
StructField("order_id", StringType()),
StructField("user_key", StringType()),
StructField("restaurant_key", StringType()),
StructField("driver_key", StringType()),
StructField("order_date", StringType()),
StructField("total_amount", DoubleType()),
StructField("payment_id", StringType()),
StructField("dt_current_timestamp", StringType()),
])
orders = (
df_raw_orders
.select(F.from_json(F.col("body").cast("string"), order_schema).alias("o"),
F.col("enqueuedTime").alias("_eh_ts"))
.select("o.*", "_eh_ts")
.withColumn("order_date", F.to_timestamp("order_date"))
.withColumn("user_cpf", F.trim("user_key"))
.withColumn("restaurant_cnpj", F.trim("restaurant_key"))
.withColumn("driver_license", F.trim("driver_key"))
.withColumn("_silver_ingested_at", F.current_timestamp())
.drop("user_key", "restaurant_key", "driver_key")
)
# MERGE
target = "silver_dev.ubereats.orders"
if not spark.catalog.tableExists(target):
orders.write.format("delta").partitionBy(F.to_date("order_date")).saveAsTable(target)
else:
t = DeltaTable.forName(spark, target)
t.alias("t").merge(orders.alias("s"), "t.order_id = s.order_id") \
.whenMatchedUpdateAll().whenNotMatchedInsertAll().execute()
print(f"orders: {orders.count()} eventos processados")
# COMMAND ----------
display(spark.table("silver_dev.ubereats.orders").orderBy(F.desc("order_date")).limit(5))

Parte 4 β€” Notebook 2: Streaming de status

Crie notebooks/M09/20_ubereats_status_streaming.py:

# Databricks notebook source
# MAGIC %md # M09 β€” Streaming order_status (Event Hubs β†’ Silver Delta)
# MAGIC
# MAGIC Diferente do batch: rodamos em modo streaming porque order_status muda em tempo real.
# MAGIC Schedule: `continuous` (trigger=AvailableNow para teste; Trigger.ProcessingTime("30 seconds") em prod).
# COMMAND ----------
from pyspark.sql import functions as F
from pyspark.sql.types import *
storage = dbutils.widgets.get("storage_account") if "storage_account" in [w.name for w in dbutils.widgets.getAll()] else None
checkpoint_root = f"abfss://silver@{storage}.dfs.core.windows.net/_checkpoints/order_status"
output_path = f"abfss://silver@{storage}.dfs.core.windows.net/ubereats/order_status"
eh_conn = dbutils.secrets.get("kv", "eh-connstr")
eh_conf = {
"eventhubs.connectionString": spark._jvm.org.apache.spark.eventhubs.EventHubsUtils.encrypt(eh_conn),
"eventhubs.consumerGroup": "$Default",
"eventhubs.startingPosition": '{"offset":"-1","seqNo":-1,"enqueuedTime":null,"isInclusive":true}',
"eventhubs.name": "order-status",
}
status_schema = StructType([
StructField("status_id", IntegerType()),
StructField("order_identifier", StringType()),
StructField("status", StructType([
StructField("status_name", StringType()),
StructField("timestamp", DoubleType()), # epoch ms
])),
StructField("dt_current_timestamp", StringType()),
])
stream = (
spark.readStream.format("eventhubs").options(**eh_conf).load()
.select(F.from_json(F.col("body").cast("string"), status_schema).alias("s"),
F.col("enqueuedTime").alias("_eh_ts"))
.select(
F.col("s.status_id").alias("status_id"),
F.col("s.order_identifier").alias("order_id"),
F.col("s.status.status_name").alias("status_name"),
F.from_unixtime(F.col("s.status.timestamp") / 1000).cast("timestamp").alias("status_ts"),
F.col("s.dt_current_timestamp").cast("timestamp").alias("_src_ts"),
F.col("_eh_ts"),
F.current_timestamp().alias("_silver_ingested_at"),
)
)
(
stream.writeStream
.format("delta")
.outputMode("append")
.option("checkpointLocation", checkpoint_root)
.option("path", output_path)
.partitionBy(F.to_date("status_ts"))
.trigger(availableNow=True) # processa tudo que estΓ‘ acumulado e PARA. Em prod: .trigger(processingTime="30 seconds")
.toTable("silver_dev.ubereats.order_status")
)
# COMMAND ----------
display(spark.table("silver_dev.ubereats.order_status").orderBy(F.desc("status_ts")).limit(10))

Importante: availableNow=True Γ© o jeito certo para agendar streaming em jobs noturnos β€” processa o backlog acumulado desde a ΓΊltima execuΓ§Γ£o e desliga. Em prod 24/7, use processingTime.

Parte 5 β€” Notebook 3: Silver β†’ Gold (star schema)

Crie notebooks/M09/30_ubereats_silver_to_gold.py:

# Databricks notebook source
# MAGIC %md # M09 β€” Silver β†’ Gold (star schema Uber Eats)
# COMMAND ----------
from pyspark.sql import functions as F
# COMMAND ----------
# MAGIC %md ## dim_date
spark.sql("""
CREATE OR REPLACE TABLE gold_dev.ubereats.dim_date AS
WITH cal AS (
SELECT explode(sequence(to_date('2020-01-01'), to_date('2030-12-31'), interval 1 day)) AS date_key
)
SELECT date_key,
YEAR(date_key) y, QUARTER(date_key) q, MONTH(date_key) m,
DATE_FORMAT(date_key, 'MMMM') month_name,
DAY(date_key) d, DAYOFWEEK(date_key) dow,
DAYOFWEEK(date_key) IN (1,7) AS is_weekend
FROM cal
""")
# COMMAND ----------
# MAGIC %md ## dim_user (SCD2 minimal β€” capta mudanΓ§a de email/endereΓ§o)
spark.sql("""
CREATE TABLE IF NOT EXISTS gold_dev.ubereats.dim_user (
user_sk BIGINT GENERATED ALWAYS AS IDENTITY,
cpf STRING, first_name STRING, last_name STRING, company_name STRING,
email STRING, city STRING, country STRING, delivery_address STRING,
source_priority STRING,
_scd_valid_from TIMESTAMP, _scd_valid_to TIMESTAMP, _scd_current BOOLEAN
) USING DELTA
""")
# Carga inicial (idempotente: substitui dim atual pela versΓ£o mais recente de cada CPF)
spark.sql("""
MERGE INTO gold_dev.ubereats.dim_user AS t
USING (
SELECT cpf, first_name, last_name, company_name, email, city, country, delivery_address, source_priority,
CURRENT_TIMESTAMP() AS _scd_valid_from,
CAST(NULL AS TIMESTAMP) AS _scd_valid_to,
TRUE AS _scd_current
FROM silver_dev.ubereats.users_unified
) s
ON t.cpf = s.cpf AND t._scd_current = TRUE
WHEN MATCHED AND (
COALESCE(t.email,'') <> COALESCE(s.email,'')
OR COALESCE(t.delivery_address,'') <> COALESCE(s.delivery_address,'')
) THEN UPDATE SET _scd_valid_to = CURRENT_TIMESTAMP(), _scd_current = FALSE
WHEN NOT MATCHED THEN INSERT
(cpf, first_name, last_name, company_name, email, city, country, delivery_address, source_priority,
_scd_valid_from, _scd_valid_to, _scd_current)
VALUES (s.cpf, s.first_name, s.last_name, s.company_name, s.email, s.city, s.country, s.delivery_address, s.source_priority,
s._scd_valid_from, s._scd_valid_to, s._scd_current)
""")
# PΓ³s-MERGE: insere a nova versΓ£o dos que tiveram mudanΓ§a
spark.sql("""
INSERT INTO gold_dev.ubereats.dim_user
(cpf, first_name, last_name, company_name, email, city, country, delivery_address, source_priority,
_scd_valid_from, _scd_valid_to, _scd_current)
SELECT s.cpf, s.first_name, s.last_name, s.company_name, s.email, s.city, s.country, s.delivery_address, s.source_priority,
CURRENT_TIMESTAMP(), NULL, TRUE
FROM silver_dev.ubereats.users_unified s
JOIN gold_dev.ubereats.dim_user t ON t.cpf = s.cpf AND t._scd_current = FALSE AND t._scd_valid_to >= CURRENT_TIMESTAMP() - INTERVAL 1 MINUTE
WHERE NOT EXISTS (
SELECT 1 FROM gold_dev.ubereats.dim_user t2
WHERE t2.cpf = s.cpf AND t2._scd_current = TRUE
)
""")
# COMMAND ----------
# MAGIC %md ## dim_restaurant (SCD1 β€” sobrescreve)
spark.sql("""
CREATE OR REPLACE TABLE gold_dev.ubereats.dim_restaurant AS
SELECT
ROW_NUMBER() OVER (ORDER BY restaurant_id) AS restaurant_sk,
restaurant_id, cnpj, name, cuisine_type, city, country,
average_rating, num_reviews,
opening_time, closing_time
FROM silver_dev.ubereats.restaurants
""")
# COMMAND ----------
# MAGIC %md ## dim_driver (SCD1)
spark.sql("""
CREATE OR REPLACE TABLE gold_dev.ubereats.dim_driver AS
SELECT
ROW_NUMBER() OVER (ORDER BY driver_id) AS driver_sk,
driver_id, license_number,
CONCAT_WS(' ', first_name, last_name) AS full_name,
city, country,
vehicle_type, vehicle_make, vehicle_model, vehicle_year, vehicle_license_plate
FROM silver_dev.ubereats.drivers
""")
# COMMAND ----------
# MAGIC %md ## dim_status (catΓ‘logo enxuto, do enum)
spark.sql("""
CREATE OR REPLACE TABLE gold_dev.ubereats.dim_status AS
SELECT * FROM VALUES
(1, 'Order Placed', 'placed', 1),
(2, 'In Analysis', 'analysis', 2),
(3, 'Accepted', 'accepted', 3),
(4, 'Preparing', 'preparing', 4),
(5, 'Ready for Pickup', 'ready', 5),
(6, 'Picked Up', 'picked', 6),
(7, 'Delivered', 'delivered', 7),
(8, 'Cancelled', 'cancelled', 99)
AS status_dim(status_sk, status_name, status_code, status_order)
""")
# COMMAND ----------
# MAGIC %md ## fact_orders (granularidade: pedido)
spark.sql("""
CREATE OR REPLACE TABLE gold_dev.ubereats.fact_orders
PARTITIONED BY (order_date_key)
AS
SELECT
o.order_id,
CAST(o.order_date AS DATE) AS order_date_key,
CAST(o.order_date AS TIMESTAMP) AS order_ts,
du.user_sk,
dr.restaurant_sk,
dd.driver_sk,
o.total_amount,
o.payment_id
FROM silver_dev.ubereats.orders o
LEFT JOIN gold_dev.ubereats.dim_user du ON du.cpf = o.user_cpf AND du._scd_current
LEFT JOIN gold_dev.ubereats.dim_restaurant dr ON dr.cnpj = o.restaurant_cnpj
LEFT JOIN gold_dev.ubereats.dim_driver dd ON dd.license_number = o.driver_license
""")
# COMMAND ----------
# MAGIC %md ## fact_order_status (granularidade: evento de status)
spark.sql("""
CREATE OR REPLACE TABLE gold_dev.ubereats.fact_order_status
PARTITIONED BY (status_date_key)
AS
SELECT
os.order_id,
CAST(os.status_ts AS DATE) AS status_date_key,
os.status_ts,
ds.status_sk,
os.status_name
FROM silver_dev.ubereats.order_status os
LEFT JOIN gold_dev.ubereats.dim_status ds ON ds.status_name = os.status_name
""")
# COMMAND ----------
# MAGIC %md ## OPTIMIZE + ZORDER
spark.sql("OPTIMIZE gold_dev.ubereats.fact_orders ZORDER BY (user_sk, restaurant_sk)")
spark.sql("OPTIMIZE gold_dev.ubereats.fact_order_status ZORDER BY (order_id)")
# COMMAND ----------
# MAGIC %md ## KPIs sanity
display(spark.sql("""
SELECT d.y AS year, d.m AS month, COUNT(*) AS orders,
ROUND(SUM(f.total_amount), 2) AS gmv,
ROUND(AVG(f.total_amount), 2) AS avg_ticket
FROM gold_dev.ubereats.fact_orders f
JOIN gold_dev.ubereats.dim_date d ON d.date_key = f.order_date_key
GROUP BY d.y, d.m
ORDER BY 1, 2
"""))
display(spark.sql("""
SELECT r.cuisine_type, COUNT(*) AS orders, ROUND(SUM(f.total_amount), 2) AS gmv
FROM gold_dev.ubereats.fact_orders f
JOIN gold_dev.ubereats.dim_restaurant r ON r.restaurant_sk = f.restaurant_sk
GROUP BY r.cuisine_type
ORDER BY gmv DESC
"""))

Parte 6 β€” Notebook 4: Data Quality

Crie notebooks/M09/40_ubereats_dq.py:

# Databricks notebook source
checks = [
("orders.user_cpf nulo (FK quebrada)",
"SELECT COUNT(*) FROM silver_dev.ubereats.orders WHERE user_cpf IS NULL OR user_cpf = ''", 0),
("orders sem user correspondente em users_unified",
"""SELECT COUNT(*) FROM silver_dev.ubereats.orders o
LEFT JOIN silver_dev.ubereats.users_unified u ON u.cpf = o.user_cpf
WHERE u.cpf IS NULL""", 0),
("orders sem restaurante correspondente",
"""SELECT COUNT(*) FROM silver_dev.ubereats.orders o
LEFT JOIN silver_dev.ubereats.restaurants r ON r.cnpj = o.restaurant_cnpj
WHERE r.cnpj IS NULL""", 0),
("orders com total_amount negativo",
"SELECT COUNT(*) FROM silver_dev.ubereats.orders WHERE total_amount < 0", 0),
("ratings fora do range 1-5",
"SELECT COUNT(*) FROM silver_dev.ubereats.ratings WHERE rating < 1 OR rating > 5", 0),
("users_unified duplicados por CPF (deveria ser 0)",
"SELECT COUNT(*) FROM (SELECT cpf FROM silver_dev.ubereats.users_unified GROUP BY cpf HAVING COUNT(*) > 1)", 0),
]
falhas = []
for name, sql, threshold in checks:
actual = spark.sql(sql).collect()[0][0]
status = "OK" if actual <= threshold else "FAIL"
print(f"[{status}] {name}: real={actual} (limite {threshold})")
if status == "FAIL":
falhas.append((name, actual, threshold))
if falhas:
msg = "DQ FAIL:\n" + "\n".join([f"- {n}: {a} > {t}" for n,a,t in falhas])
dbutils.notebook.exit(msg)
print("βœ… DQ OK")

Parte 7 β€” Job DAB (resources/m09_ubereats.job.yml)

resources:
jobs:
ubereats_e2e:
name: ubereats_e2e
schedule:
quartz_cron_expression: "0 0 9 * * ?" # 06h BRT
timezone_id: UTC
pause_status: ${var.bundle_target == "prd" ? "UNPAUSED" : "PAUSED"}
email_notifications:
on_failure: [ team.old.schoolll@gmail.com ]
max_concurrent_runs: 1
timeout_seconds: 2400 # 40 min SLA
tasks:
- task_key: bronze_to_silver_batch
notebook_task:
notebook_path: ../notebooks/M09/10_ubereats_bronze_to_silver.py
base_parameters:
storage_account: ${var.storage_account}
ingestion_date: "{{job.start_time.iso_date}}"
job_cluster_key: ue_cluster
- task_key: status_streaming_micro_batch
depends_on: [{ task_key: bronze_to_silver_batch }]
notebook_task:
notebook_path: ../notebooks/M09/20_ubereats_status_streaming.py
base_parameters:
storage_account: ${var.storage_account}
job_cluster_key: ue_cluster
- task_key: dq_silver
depends_on: [{ task_key: status_streaming_micro_batch }]
notebook_task:
notebook_path: ../notebooks/M09/40_ubereats_dq.py
job_cluster_key: ue_cluster
- task_key: silver_to_gold
depends_on: [{ task_key: dq_silver }]
notebook_task:
notebook_path: ../notebooks/M09/30_ubereats_silver_to_gold.py
job_cluster_key: ue_cluster
job_clusters:
- job_cluster_key: ue_cluster
new_cluster:
spark_version: "15.4.x-scala2.12"
node_type_id: Standard_DS3_v2
num_workers: 0
data_security_mode: SINGLE_USER
spark_conf:
spark.databricks.cluster.profile: singleNode
spark.master: local[*, 4]
custom_tags:
projeto: de
case: ubereats
ambiente: ${bundle.target}

Deploy:

Terminal window
databricks bundle deploy --target dev
databricks bundle run ubereats_e2e --target dev

Parte 8 β€” Power BI

No SQL Warehouse (M08), conecte e importe:

  • gold_dev.ubereats.fact_orders
  • gold_dev.ubereats.fact_order_status
  • gold_dev.ubereats.dim_user, dim_restaurant, dim_driver, dim_date, dim_status

Medidas DAX recomendadas

GMV = SUM(fact_orders[total_amount])
Orders = DISTINCTCOUNT(fact_orders[order_id])
Avg Ticket = DIVIDE([GMV], [Orders])
Time to Delivery (min) =
VAR placed = CALCULATE(MIN(fact_order_status[status_ts]), fact_order_status[status_name] = "Order Placed")
VAR picked = CALCULATE(MIN(fact_order_status[status_ts]), fact_order_status[status_name] = "Picked Up")
RETURN DIVIDE(DATEDIFF(placed, picked, MINUTE), 1)
SLA Compliance % =
DIVIDE(
CALCULATE([Orders], [Time to Delivery (min)] <= 45),
[Orders]
)

VisualizaΓ§Γ΅es

  • Card: GMV total, Orders, Avg Ticket, SLA %.
  • Mapa: pedidos por cidade do usuΓ‘rio.
  • Bar chart: top 10 restaurantes por GMV.
  • Bar chart: top 10 motoristas por pedidos entregues.
  • Line chart: SLA % por dia.
  • Funnel: # pedidos por status_name (visualiza o funil β€œPlaced β†’ Picked Up”).

Parte 9 β€” Custo do M09

Modo econΓ΄mico (sem MySQL/Postgres/Mongo reais):

RecursoCusto/mΓͺs
Event Hubs Standard (1 TU)$11
Azure SQL Serverless (mssql_users; auto-pause 60min)$1-3
ADLS Gen2 (50 GB)$1.20
Databricks (cluster Job Single Node, 30 min/dia Γ— 30)$7
SQL Warehouse Pro Serverless (1h/dia)$10
Total~$30/mΓͺs

ApΓ³s executar o case e validar, delete o RG ou apague Event Hubs namespace (az eventhubs namespace delete) para zerar o custo. VocΓͺ recria em 5 min.

Parte 10 β€” Como contar este case em entrevista (STAR)

SituaΓ§Γ£o: β€œPlataforma de delivery com 5 fontes operacionais heterogΓͺneas (Kafka pedidos, MongoDB usuΓ‘rios B2C, MSSQL CRM B2B, MySQL catΓ‘logo, Postgres motoristas). Times de marketing, ops e BI tinham dashboards desconectados e dado conflitante.”

Tarefa: β€œConstruir um lakehouse ΓΊnico que resolve a identidade do cliente entre B2B e B2C, junta o stream de pedidos com batch dos cadastros, entrega star schema para BI com SLA de entrega medido em tempo real.”

AΓ§Γ£o:

  1. IngestΓ£o:
    • Kafka pedidos + status β†’ Event Hubs β†’ Auto Loader/Structured Streaming β†’ Delta Bronze.
    • Cadastros (mongo/mssql/mysql/postgres) β†’ ADF Copy via Self-Hosted IR (em prod) β†’ Bronze parquet particionado por data.
  2. Silver:
    • Tabelas tipadas + dedup + MERGE incremental por chave natural.
    • ResoluΓ§Γ£o de identidade: users_unified consolida B2B e B2C usando CPF como chave, priorizando MSSQL (campos mais ricos: birthday, company, job).
    • Streaming order_status em modo availableNow no job batch para reaproveitar cluster.
  3. Gold:
    • Star schema: fact_orders, fact_order_status, dim_user (SCD2), dim_restaurant/driver (SCD1), dim_date, dim_status.
    • Lookup de FK por chave natural (cpf, cnpj, license).
  4. GovernanΓ§a: Unity Catalog (catΓ‘logos por camada, external locations, grants por grupo, system tables de audit). Lineage automΓ‘tica mostra de qual fonte cada coluna do dashboard veio.
  5. Qualidade: notebook DQ aborta o pipeline se houver FK quebrada, total negativo, rating fora de range, ou usuΓ‘rios duplicados por CPF.
  6. OrquestraΓ§Γ£o: Databricks Job multi-task com 4 etapas dependentes; reaproveita cluster entre tasks (mais barato).
  7. CI/CD: Databricks Asset Bundle versionado em Git, deploy dev/prd via GitHub Actions.
  8. Consumo: SQL Warehouse Pro Serverless + Power BI Direct Query. SLA% e Time-to-Delivery medidos em DAX.

Resultado:

  • Dashboard atualizado em < 40 min apΓ³s meia-noite.
  • 0 incidentes de β€œdado errado para o CEO” no primeiro trimestre.
  • Custo ~$30/mΓͺs em dev.
  • Lineage UC documenta cada coluna atΓ© a fonte β†’ audit ready (LGPD).
  • ResoluΓ§Γ£o B2B↔B2C reduziu duplicidade de cliente em 14%.

Pegadinhas (e como vocΓͺ lida)

  1. Mesmo CPF em B2B e B2C β†’ priorize MSSQL (mais completo) ou faΓ§a SCD2 com source_priority no dim_user. Conte em entrevista que Γ© decisΓ£o de negΓ³cio, nΓ£o tΓ©cnica.
  2. kafka_status.timestamp em double epoch ms com notaΓ§Γ£o cientΓ­fica (1.738792248012E12) β†’ from_unixtime(col / 1000) lida certo.
  3. vehicle_year=2006 mas date_birth=2149-05-24 (dado sintΓ©tico quebrado) β†’ DQ check deve apontar. Mostre que vocΓͺ notou e documentou.
  4. Order com user_key nΓ£o encontrado em nenhuma das fontes β†’ polΓ­tica: gravar em fact_orders com user_sk = -1 (linha β€œunknown” do dim) e jogar o caso num quarantine table. NUNCA descartar silenciosamente.
  5. Streaming em job batch com availableNow β†’ Γ³timo padrΓ£o pra reduzir custo (nΓ£o precisa de cluster 24/7). Em prod 24/7 use processingTime.
  6. Schema evolution no Bronze: novos campos nos JSONs aparecem? Configure mergeSchema=true em Bronze; em Silver, evoluΓ§Γ£o Γ© controlada (vocΓͺ decide quais campos promover).

Checklist de saΓ­da

  • Event Hubs namespace eh-de-dev-ubereats-brs criado com 2 hubs.
  • Seeder Python publicou orders + order_status no EH.
  • 5 JSONs upadas no Bronze (source=<src>/table=<t>/ingestion_date=<d>/).
  • Schemas silver_dev.ubereats e gold_dev.ubereats criados.
  • 4 notebooks (10, 20, 30, 40) rodam sem erro.
  • users_unified mostra rows com source_priority = b2b e b2c.
  • fact_orders populada com FKs resolvidas (poucas linhas com user_sk IS NULL).
  • DQ check passa.
  • Job DAB ubereats_e2e deploy + run com SUCCESS.
  • Power BI conectado e dashboard com pelo menos GMV + SLA% + Funnel de status.
  • VocΓͺ consegue contar a histΓ³ria STAR em 5 min, desenhando no papel.

PrΓ³ximo

  • Volte para [M10 β€” Streaming Event Hubs + Auto Loader (em breve)] (Fase 2 do roadmap).
  • Ou aborde o M09 equivalente em AWS (Fase 3) quando estiver na trilha aws-end-to-end/.
← Anterior
M08 Pipeline Fim A Fim