Powered by AppSignal & Oban Pro

QuackDB analytics in Livebook

examples/livebook_analytics.livemd

QuackDB analytics in Livebook

This notebook shows QuackDB as an Elixir-native analytics bridge: DuckDB over Quack, Explorer dataframes, Table.Reader friendly results, telemetry, and native dataframe append.

Setup

Mix.install([
  {:quackdb, path: Path.expand("..", __DIR__)},
  {:explorer, "~> 0.11"},
  {:vega_lite, "~> 0.1"},
  {:kino, "~> 0.14"}
])

By default the notebook starts a local DuckDB Quack server with QuackDB.Server. Set QUACKDB_URI and optional QUACKDB_TOKEN when you want to connect to an existing server instead.

alias Explorer.DataFrame
alias VegaLite, as: Vl

{_server, uri, token} =
  case System.get_env("QUACKDB_URI") do
    nil ->
      token = "super_secret"
      {:ok, server} = QuackDB.Server.start_link(duckdb: :managed, token: token)
      {server, "http://localhost:9494", token}

    uri ->
      {nil, uri, System.get_env("QUACKDB_TOKEN", "")}
  end

{:ok, conn} = QuackDB.start_link(uri: uri, token: token)

Observe queries

:telemetry.attach_many(
  "quackdb-livebook-observer",
  [[:quackdb, :query, :stop], [:quackdb, :append, :stop], [:quackdb, :fetch, :stop]],
  fn event, measurements, metadata, _config ->
    ms = measurements.duration |> System.convert_time_unit(:native, :microsecond) |> Kernel./(1_000)
    IO.inspect(%{event: event, ms: Float.round(ms, 2), metadata: metadata})
  end,
  nil
)

Create sample data

table = "livebook_events_#{System.unique_integer([:positive])}"

QuackDB.query!(
  conn,
  QuackDB.DDL.create_table(table,
    [id: :integer, category: :varchar, score: :double],
    temporary: true
  )
)

df =
  DataFrame.new(
    id: 1..8,
    category: ["alpha", "alpha", "alpha", "beta", "beta", "gamma", "gamma", "gamma"],
    score: [10.0, 20.0, 15.0, 30.0, 25.0, 7.0, 12.0, 18.0]
  )

QuackDB.Explorer.insert_dataframe!(conn, table, df, batch_size: 3)

Query with SQL

result =
  QuackDB.query!(conn, """
  SELECT category, count(*) AS events, avg(score) AS avg_score
  FROM #{table}
  GROUP BY category
  ORDER BY category
  """)

result

QuackDB.Result implements Table.Reader when the optional :table package is available, so Table-aware tools can consume it directly.

Table.Reader.init(result)

Convert to Explorer

summary = QuackDB.Explorer.from_result!(result)

Visualize

summary
|> DataFrame.to_rows()
|> Vl.new(width: 500, height: 300)
|> Vl.mark(:bar)
|> Vl.encode_field(:x, "category", type: :nominal)
|> Vl.encode_field(:y, "avg_score", type: :quantitative)