Complex Query Examples
View SourceThis guide demonstrates how to use LiveTable with custom queries, joins, computed fields, and advanced data transformations.
Order Report with Customer Details
A comprehensive order report joining multiple tables:
# lib/your_app_web/live/order_report_live/index.ex
defmodule YourAppWeb.OrderReportLive.Index do
use YourAppWeb, :live_view
use LiveTable.LiveResource
def mount(_params, _session, socket) do
socket = assign(socket, :data_provider, {YourApp.Reports, :order_details, []})
{:ok, socket}
end
def fields do
[
order_id: %{label: "Order #", sortable: true},
customer_name: %{label: "Customer", sortable: true, searchable: true},
customer_email: %{label: "Email", sortable: true, searchable: true},
product_count: %{label: "Items", sortable: true},
total_amount: %{label: "Total", sortable: true, renderer: &render_currency/1},
order_date: %{label: "Order Date", sortable: true, renderer: &render_date/1},
shipping_method: %{
label: "Shipping",
sortable: true,
assoc: {:shipping, :method}
},
status: %{label: "Status", sortable: true, renderer: &render_status/1},
days_since_order: %{label: "Age (Days)", sortable: true, renderer: &render_days/1}
]
end
def filters do
[
status: Select.new({:orders, :status}, "status", %{
label: "Order Status",
options: [
%{label: "Pending", value: ["pending"]},
%{label: "Processing", value: ["processing"]},
%{label: "Shipped", value: ["shipped"]},
%{label: "Delivered", value: ["delivered"]}
]
}),
order_value: Range.new(:total_amount, "order_value", %{
type: :number,
label: "Order Value",
min: 0,
max: 10000,
step: 100
}),
customer_type: Select.new({:customers, :type}, "customer_type", %{
label: "Customer Type",
options: [
%{label: "Individual", value: ["individual"]},
%{label: "Business", value: ["business"]}
]
}),
recent_orders: Boolean.new(:order_date, "recent", %{
label: "Last 30 Days",
condition: dynamic([o, customers: c, shipping: s], o.inserted_at >= ago(30, "day"))
})
]
end
defp render_currency(amount) do
assigns = %{amount: amount}
~H"""
<span class="font-mono text-green-600">$<%= :erlang.float_to_binary(@amount, decimals: 2) %></span>
"""
end
defp render_date(date) do
assigns = %{date: date}
~H"""
<span class="text-sm"><%= Calendar.strftime(@date, "%b %d, %Y") %></span>
"""
end
defp render_status(status) do
assigns = %{status: status}
~H"""
<span class={[
"px-2 py-1 text-xs font-medium rounded-full",
case @status do
"pending" -> "bg-yellow-100 text-yellow-700"
"processing" -> "bg-blue-100 text-blue-700"
"shipped" -> "bg-purple-100 text-purple-700"
"delivered" -> "bg-green-100 text-green-700"
end
]}>
<%= String.capitalize(@status) %>
</span>
"""
end
defp render_days(days) do
assigns = %{days: days}
~H"""
<span class={[
"text-sm",
if(@days > 30, do: "text-red-600", else: "text-gray-600")
]}>
<%= @days %> days
</span>
"""
end
end
The corresponding context function:
# lib/your_app/reports.ex
defmodule YourApp.Reports do
import Ecto.Query
alias YourApp.Repo
def order_details do
from o in YourApp.Order,
join: c in YourApp.Customer, on: o.customer_id == c.id, as: :customers,
join: s in YourApp.ShippingMethod, on: o.shipping_method_id == s.id, as: :shipping,
left_join: oi in YourApp.OrderItem, on: oi.order_id == o.id,
group_by: [o.id, c.id, s.id],
select: %{
order_id: o.id,
customer_name: fragment("? || ' ' || ?", c.first_name, c.last_name),
customer_email: c.email,
product_count: count(oi.id),
total_amount: o.total_amount,
order_date: o.inserted_at,
shipping_method: s.name,
status: o.status,
days_since_order: fragment("DATE_PART('day', NOW() - ?)", o.inserted_at)
}
end
end
Product Analytics with Sales Data
Advanced product performance analytics:
# lib/your_app_web/live/product_analytics_live/index.ex
defmodule YourAppWeb.ProductAnalyticsLive.Index do
use YourAppWeb, :live_view
use LiveTable.LiveResource
def mount(%{"period" => period}, _session, socket) do
data_provider = {YourApp.Analytics, :product_performance, [period]}
socket = assign(socket, :data_provider, data_provider)
{:ok, socket}
end
def mount(_params, _session, socket) do
mount(%{"period" => "30"}, nil, socket)
end
def fields do
[
product_name: %{label: "Product", sortable: true, searchable: true},
sku: %{label: "SKU", sortable: true, searchable: true},
category_name: %{
label: "Category",
sortable: true,
assoc: {:categories, :name}
},
units_sold: %{label: "Units Sold", sortable: true},
revenue: %{label: "Revenue", sortable: true, renderer: &render_currency/1},
avg_order_value: %{label: "AOV", sortable: true, renderer: &render_currency/1},
profit_margin: %{label: "Margin %", sortable: true, renderer: &render_percentage/1},
conversion_rate: %{label: "Conv. Rate", sortable: true, renderer: &render_percentage/1},
stock_turns: %{label: "Stock Turns", sortable: true, renderer: &render_decimal/1},
performance_score: %{label: "Score", sortable: true, renderer: &render_score/1}
]
end
def filters do
[
category: Select.new({:categories, :name}, "category", %{
label: "Category",
options_source: {YourApp.Catalog, :list_categories, []}
}),
revenue_range: Range.new(:revenue, "revenue_range", %{
type: :number,
label: "Revenue Range",
min: 0,
max: 100000,
step: 1000
}),
high_performers: Boolean.new(:performance_score, "high_performers", %{
label: "High Performers (Score > 80)",
condition: dynamic([p, categories: c], p.performance_score > 80)
}),
low_margin: Boolean.new(:profit_margin, "low_margin", %{
label: "Low Margin (< 20%)",
condition: dynamic([p, categories: c], p.profit_margin < 20)
})
]
end
def table_options do
%{
pagination: %{
enabled: true,
sizes: [25, 50, 100],
default_size: 50
},
sorting: %{
default_sort: [performance_score: :desc, revenue: :desc]
},
exports: %{
enabled: true,
formats: [:csv, :pdf]
}
}
end
defp render_currency(amount) do
assigns = %{amount: amount}
~H"""
<span class="font-mono text-green-600">$<%= :erlang.float_to_binary(@amount, decimals: 2) %></span>
"""
end
defp render_percentage(value) do
assigns = %{value: value}
~H"""
<span class={[
"font-medium",
if(@value >= 20, do: "text-green-600", else: "text-red-600")
]}>
<%= :erlang.float_to_binary(@value, decimals: 1) %>%
</span>
"""
end
defp render_decimal(value) do
assigns = %{value: value}
~H"""
<span class="text-sm"><%= :erlang.float_to_binary(@value, decimals: 2) %></span>
"""
end
defp render_score(score) do
assigns = %{score: score}
~H"""
<div class="flex items-center gap-2">
<div class="w-12 bg-gray-200 rounded-full h-2">
<div
class={[
"h-2 rounded-full",
cond do
@score >= 80 -> "bg-green-500"
@score >= 60 -> "bg-yellow-500"
true -> "bg-red-500"
end
]}
style={"width: #{@score}%"}
></div>
</div>
<span class="text-sm font-medium"><%= round(@score) %></span>
</div>
"""
end
end
Complex analytics context:
# lib/your_app/analytics.ex
defmodule YourApp.Analytics do
import Ecto.Query
alias YourApp.Repo
def product_performance(days \\ 30) do
cutoff_date = Date.add(Date.utc_today(), -days)
from p in YourApp.Product,
join: c in YourApp.Category, on: p.category_id == c.id, as: :categories,
left_join: oi in YourApp.OrderItem, on: oi.product_id == p.id,
left_join: o in YourApp.Order, on: oi.order_id == o.id and o.inserted_at >= ^cutoff_date,
left_join: v in YourApp.ProductView, on: v.product_id == p.id and v.inserted_at >= ^cutoff_date,
group_by: [p.id, c.id],
select: %{
product_name: p.name,
sku: p.sku,
category_name: c.name,
units_sold: coalesce(sum(oi.quantity), 0),
revenue: coalesce(sum(fragment("? * ?", oi.quantity, oi.unit_price)), 0.0),
avg_order_value: fragment("CASE WHEN COUNT(DISTINCT ?) > 0 THEN ? / COUNT(DISTINCT ?) ELSE 0 END",
o.id, coalesce(sum(fragment("? * ?", oi.quantity, oi.unit_price)), 0.0), o.id),
profit_margin: fragment("CASE WHEN ? > 0 THEN ((? - ?) / ?) * 100 ELSE 0 END",
p.price, p.price, p.cost_price, p.price),
conversion_rate: fragment("CASE WHEN COUNT(?) > 0 THEN (COUNT(DISTINCT ?) * 100.0 / COUNT(?)) ELSE 0 END",
v.id, o.id, v.id),
stock_turns: fragment("CASE WHEN ? > 0 THEN ? / ? ELSE 0 END",
p.stock_quantity, coalesce(sum(oi.quantity), 0), p.stock_quantity),
performance_score: fragment("""
LEAST(100, GREATEST(0,
(CASE WHEN ? > 0 THEN ? / ? * 20 ELSE 0 END) +
(CASE WHEN ? > 0 THEN LEAST(20, ? / 1000 * 20) ELSE 0 END) +
(CASE WHEN ? > 0 THEN ? / 50 * 20 ELSE 0 END) +
(CASE WHEN ? > 0 THEN ? / 5 * 20 ELSE 0 END) +
(CASE WHEN ? > 10 THEN 20 ELSE ? / 10 * 20 END)
))
""",
# Stock turns score (0-20)
p.stock_quantity, coalesce(sum(oi.quantity), 0), p.stock_quantity,
# Revenue score (0-20)
coalesce(sum(fragment("? * ?", oi.quantity, oi.unit_price)), 0.0),
coalesce(sum(fragment("? * ?", oi.quantity, oi.unit_price)), 0.0),
# Margin score (0-20)
fragment("((? - ?) / ?) * 100", p.price, p.cost_price, p.price),
fragment("((? - ?) / ?) * 100", p.price, p.cost_price, p.price),
# Conversion score (0-20)
fragment("CASE WHEN COUNT(?) > 0 THEN (COUNT(DISTINCT ?) * 100.0 / COUNT(?)) ELSE 0 END",
v.id, o.id, v.id),
fragment("CASE WHEN COUNT(?) > 0 THEN (COUNT(DISTINCT ?) * 100.0 / COUNT(?)) ELSE 0 END",
v.id, o.id, v.id),
# Units sold score (0-20)
coalesce(sum(oi.quantity), 0), coalesce(sum(oi.quantity), 0))
}
end
end
Multi-Table Inventory Report
Comprehensive inventory analysis across suppliers and warehouses:
# lib/your_app_web/live/inventory_report_live/index.ex
defmodule YourAppWeb.InventoryReportLive.Index do
use YourAppWeb, :live_view
use LiveTable.LiveResource
def mount(_params, _session, socket) do
socket = assign(socket, :data_provider, {YourApp.Inventory, :comprehensive_report, []})
{:ok, socket}
end
def fields do
[
product_name: %{label: "Product", sortable: true, searchable: true},
sku: %{label: "SKU", sortable: true, searchable: true},
supplier_name: %{
label: "Supplier",
sortable: true,
assoc: {:suppliers, :name}
},
warehouse_location: %{
label: "Warehouse",
sortable: true,
assoc: {:warehouses, :location}
},
current_stock: %{label: "Current Stock", sortable: true, renderer: &render_stock/2},
reserved_stock: %{label: "Reserved", sortable: true},
available_stock: %{label: "Available", sortable: true, renderer: &render_available/1},
reorder_point: %{label: "Reorder Point", sortable: true},
max_stock: %{label: "Max Stock", sortable: true},
avg_daily_usage: %{label: "Daily Usage", sortable: true, renderer: &render_usage/1},
days_of_supply: %{label: "Days Supply", sortable: true, renderer: &render_days_supply/2},
last_movement: %{label: "Last Movement", sortable: true, renderer: &render_date/1},
status: %{label: "Status", sortable: false, renderer: &render_inventory_status/2}
]
end
def filters do
[
supplier: Select.new({:suppliers, :name}, "supplier", %{
label: "Supplier",
options_source: {YourApp.Suppliers, :list_active, []}
}),
warehouse: Select.new({:warehouses, :location}, "warehouse", %{
label: "Warehouse",
options_source: {YourApp.Warehouses, :list_locations, []}
}),
stock_status: Select.new(:current_stock, "stock_status", %{
label: "Stock Status",
options: [
%{label: "Out of Stock", value: ["out_of_stock"]},
%{label: "Low Stock", value: ["low_stock"]},
%{label: "Normal", value: ["normal"]},
%{label: "Overstock", value: ["overstock"]}
]
}),
needs_reorder: Boolean.new(:current_stock, "needs_reorder", %{
label: "Needs Reorder",
condition: dynamic([p, suppliers: s, warehouses: w],
p.current_stock <= p.reorder_point and p.current_stock > 0)
}),
slow_moving: Boolean.new(:avg_daily_usage, "slow_moving", %{
label: "Slow Moving (< 1/day)",
condition: dynamic([p, suppliers: s, warehouses: w], p.avg_daily_usage < 1.0)
})
]
end
def table_options do
%{
pagination: %{
enabled: true,
sizes: [50, 100, 200],
default_size: 100
},
sorting: %{
default_sort: [days_of_supply: :asc, current_stock: :asc]
},
exports: %{
enabled: true,
formats: [:csv, :pdf]
}
}
end
defp render_stock(stock, record) do
assigns = %{stock: stock, record: record}
~H"""
<div class="flex items-center gap-2">
<span class={[
"font-medium",
cond do
@stock == 0 -> "text-red-600"
@stock <= @record.reorder_point -> "text-orange-600"
@stock >= @record.max_stock -> "text-purple-600"
true -> "text-green-600"
end
]}>
<%= @stock %>
</span>
</div>
"""
end
defp render_available(available) do
assigns = %{available: available}
~H"""
<span class={[
"font-medium",
if(@available <= 0, do: "text-red-600", else: "text-blue-600")
]}>
<%= @available %>
</span>
"""
end
defp render_usage(usage) do
assigns = %{usage: usage}
~H"""
<span class="text-sm">
<%= :erlang.float_to_binary(@usage, decimals: 1) %>/day
</span>
"""
end
defp render_days_supply(days, record) do
assigns = %{days: days, record: record}
~H"""
<span class={[
"font-medium",
cond do
@days <= 7 -> "text-red-600"
@days <= 14 -> "text-orange-600"
@days >= 90 -> "text-purple-600"
true -> "text-green-600"
end
]}>
<%= round(@days) %> days
</span>
"""
end
defp render_date(nil) do
assigns = %{}
~H"""
<span class="text-gray-400 text-sm">No movement</span>
"""
end
defp render_date(date) do
assigns = %{date: date}
~H"""
<span class="text-sm">
<%= Calendar.strftime(@date, "%b %d") %>
</span>
"""
end
defp render_inventory_status(_value, record) do
assigns = %{record: record}
~H"""
<div class="flex flex-col gap-1">
<%= cond do %>
<% @record.current_stock == 0 -> %>
<span class="bg-red-100 text-red-700 text-xs px-2 py-1 rounded">OUT OF STOCK</span>
<% @record.current_stock <= @record.reorder_point -> %>
<span class="bg-orange-100 text-orange-700 text-xs px-2 py-1 rounded">LOW STOCK</span>
<% @record.current_stock >= @record.max_stock -> %>
<span class="bg-purple-100 text-purple-700 text-xs px-2 py-1 rounded">OVERSTOCK</span>
<% @record.days_of_supply <= 7 -> %>
<span class="bg-yellow-100 text-yellow-700 text-xs px-2 py-1 rounded">URGENT</span>
<% true -> %>
<span class="bg-green-100 text-green-700 text-xs px-2 py-1 rounded">OK</span>
<% end %>
<%= if @record.avg_daily_usage < 1.0 do %>
<span class="bg-gray-100 text-gray-600 text-xs px-2 py-1 rounded">SLOW</span>
<% end %>
</div>
"""
end
end
The comprehensive inventory context:
# lib/your_app/inventory.ex
defmodule YourApp.Inventory do
import Ecto.Query
alias YourApp.Repo
def comprehensive_report do
from p in YourApp.Product,
join: s in YourApp.Supplier, on: p.supplier_id == s.id, as: :suppliers,
join: w in YourApp.Warehouse, on: p.warehouse_id == w.id, as: :warehouses,
left_join: im in YourApp.InventoryMovement,
on: im.product_id == p.id and im.inserted_at >= ago(30, "day"),
group_by: [p.id, s.id, w.id],
select: %{
product_name: p.name,
sku: p.sku,
supplier_name: s.name,
warehouse_location: w.location,
current_stock: p.stock_quantity,
reserved_stock: coalesce(p.reserved_quantity, 0),
available_stock: p.stock_quantity - coalesce(p.reserved_quantity, 0),
reorder_point: p.reorder_point,
max_stock: p.max_stock_level,
avg_daily_usage: fragment("""
CASE
WHEN COUNT(?) > 0 THEN ABS(SUM(CASE WHEN ?.movement_type = 'out' THEN ?.quantity ELSE 0 END)) / 30.0
ELSE 0
END
""", im.id, im, im),
days_of_supply: fragment("""
CASE
WHEN COUNT(?) > 0 AND SUM(CASE WHEN ?.movement_type = 'out' THEN ?.quantity ELSE 0 END) > 0
THEN ? / (ABS(SUM(CASE WHEN ?.movement_type = 'out' THEN ?.quantity ELSE 0 END)) / 30.0)
ELSE 999
END
""", im.id, im, im, p.stock_quantity, im, im),
last_movement: max(im.inserted_at)
}
end
end
Customer Lifetime Value Analysis
Complex customer analytics with cohort analysis:
# lib/your_app_web/live/customer_ltv_live/index.ex
defmodule YourAppWeb.CustomerLtvLive.Index do
use YourAppWeb, :live_view
use LiveTable.LiveResource
def mount(%{"cohort" => cohort}, _session, socket) do
data_provider = {YourApp.Analytics, :customer_ltv_analysis, [cohort]}
socket = assign(socket, :data_provider, data_provider)
{:ok, socket}
end
def mount(_params, _session, socket) do
mount(%{"cohort" => "2024"}, nil, socket)
end
def fields do
[
customer_name: %{label: "Customer", sortable: true, searchable: true},
email: %{label: "Email", sortable: true, searchable: true},
first_order_date: %{label: "First Order", sortable: true, renderer: &render_date/1},
customer_segment: %{
label: "Segment",
sortable: true,
assoc: {:segments, :name}
},
total_orders: %{label: "Orders", sortable: true},
total_spent: %{label: "Total Spent", sortable: true, renderer: &render_currency/1},
avg_order_value: %{label: "AOV", sortable: true, renderer: &render_currency/1},
order_frequency: %{label: "Frequency (days)", sortable: true, renderer: &render_frequency/1},
ltv_score: %{label: "LTV Score", sortable: true, renderer: &render_ltv_score/1},
predicted_ltv: %{label: "Predicted LTV", sortable: true, renderer: &render_currency/1},
churn_risk: %{label: "Churn Risk", sortable: true, renderer: &render_churn_risk/1},
last_order_date: %{label: "Last Order", sortable: true, renderer: &render_last_order/1}
]
end
def filters do
[
segment: Select.new({:segments, :name}, "segment", %{
label: "Customer Segment",
options: [
%{label: "VIP", value: ["vip"]},
%{label: "Regular", value: ["regular"]},
%{label: "New", value: ["new"]},
%{label: "At Risk", value: ["at_risk"]}
]
}),
ltv_range: Range.new(:predicted_ltv, "ltv_range", %{
type: :number,
label: "Predicted LTV",
min: 0,
max: 50000,
step: 1000
}),
high_value: Boolean.new(:total_spent, "high_value", %{
label: "High Value (> $1000)",
condition: dynamic([c, segments: s], c.total_spent > 1000)
}),
churn_risk: Boolean.new(:churn_risk, "churn_risk", %{
label: "High Churn Risk (> 70%)",
condition: dynamic([c, segments: s], c.churn_risk > 70)
}),
active_customers: Boolean.new(:last_order_date, "active", %{
label: "Active (Last 90 Days)",
condition: dynamic([c, segments: s], c.last_order_date >= ago(90, "day"))
})
]
end
def table_options do
%{
pagination: %{
enabled: true,
sizes: [25, 50, 100],
default_size: 50
},
sorting: %{
default_sort: [predicted_ltv: :desc, total_spent: :desc]
},
exports: %{
enabled: true,
formats: [:csv, :pdf]
}
}
end
defp render_currency(amount) do
assigns = %{amount: amount}
~H"""
<span class="font-mono text-green-600">$<%= :erlang.float_to_binary(@amount, decimals: 2) %></span>
"""
end
defp render_date(date) do
assigns = %{date: date}
~H"""
<span class="text-sm"><%= Calendar.strftime(@date, "%b %d, %Y") %></span>
"""
end
defp render_frequency(days) do
assigns = %{days: days}
~H"""
<span class="text-sm"><%= round(@days) %> days</span>
"""
end
defp render_ltv_score(score) do
assigns = %{score: score}
~H"""
<div class="flex items-center gap-2">
<div class="w-16 bg-gray-200 rounded-full h-2">
<div
class={[
"h-2 rounded-full",
cond do
@score >= 80 -> "bg-green-500"
@score >= 60 -> "bg-yellow-500"
@score >= 40 -> "bg-orange-500"
true -> "bg-red-500"
end
]}
style={"width: #{@score}%"}
></div>
</div>
<span class="text-sm font-medium"><%= round(@score) %></span>
</div>
"""
end
defp render_churn_risk(risk) do
assigns = %{risk: risk}
~H"""
<span class={[
"px-2 py-1 text-xs font-medium rounded-full",
cond do
@risk >= 80 -> "bg-red-100 text-red-700"
@risk >= 60 -> "bg-orange-100 text-orange-700"
@risk >= 40 -> "bg-yellow-100 text-yellow-700"
true -> "bg-green-100 text-green-700"
end
]}>
<%= round(@risk) %>%
</span>
"""
end
defp render_last_order(date) do
assigns = %{date: date, days_ago: Date.diff(Date.utc_today(), date)}
~H"""
<div class="flex flex-col">
<span class="text-sm"><%= Calendar.strftime(@date, "%b %d") %></span>
<span class={[
"text-xs",
cond do
@days_ago <= 30 -> "text-green-600"
@days_ago <= 90 -> "text-yellow-600"
true -> "text-red-600"
end
]}>
<%= @days_ago %> days ago
</span>
</div>
"""
end
end
Advanced customer analytics context:
# lib/your_app/analytics.ex (addition to existing module)
def customer_ltv_analysis(cohort_year) do
cohort_start = Date.new!(String.to_integer(cohort_year), 1, 1)
cohort_end = Date.new!(String.to_integer(cohort_year), 12, 31)
from c in YourApp.Customer,
join: s in YourApp.CustomerSegment, on: c.segment_id == s.id, as: :segments,
left_join: o in YourApp.Order, on: o.customer_id == c.id,
where: c.inserted_at >= ^cohort_start and c.inserted_at <= ^cohort_end,
group_by: [c.id, s.id],
select: %{
customer_name: fragment("? || ' ' || ?", c.first_name, c.last_name),
email: c.email,
first_order_date: min(o.inserted_at),
customer_segment: s.name,
total_orders: count(o.id),
total_spent: coalesce(sum(o.total_amount), 0.0),
avg_order_value: fragment("CASE WHEN COUNT(?) > 0 THEN ? / COUNT(?) ELSE 0 END",
o.id, coalesce(sum(o.total_amount), 0.0), o.id),
order_frequency: fragment("""
CASE
WHEN COUNT(?) > 1 THEN
DATE_PART('day', MAX(?) - MIN(?)) / (COUNT(?) - 1)
ELSE 0
END
""", o.id, o.inserted_at, o.inserted_at, o.id),
ltv_score: fragment("""
LEAST(100, GREATEST(0,
(CASE WHEN ? > 0 THEN LEAST(25, ? / 2000 * 25) ELSE 0 END) +
(CASE WHEN COUNT(?) > 0 THEN LEAST(25, COUNT(?) / 10 * 25) ELSE 0 END) +
(CASE WHEN ? > 0 THEN LEAST(25, ? / 500 * 25) ELSE 0 END) +
(CASE WHEN ? <= 60 THEN 25 ELSE GREATEST(0, 25 - (? - 60) / 10) END)
))
""",
# Total spent score (0-25)
coalesce(sum(o.total_amount), 0.0), coalesce(sum(o.total_amount), 0.0),
# Order frequency score (0-25)
o.id, o.id,
# AOV score (0-25)
fragment("CASE WHEN COUNT(?) > 0 THEN ? / COUNT(?) ELSE 0 END",
o.id, coalesce(sum(o.total_amount), 0.0), o.id),
fragment("CASE WHEN COUNT(?) > 0 THEN ? / COUNT(?) ELSE 0 END",
o.id, coalesce(sum(o.total_amount), 0.0), o.id),
# Recency score (0-25, based on days since last order)
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at)),
predicted_ltv: fragment("""
CASE
WHEN COUNT(?) > 0 THEN
? * (? / ?) *
CASE
WHEN ? <= 30 THEN 5.0
WHEN ? <= 90 THEN 3.0
WHEN ? <= 180 THEN 2.0
ELSE 1.5
END
ELSE 0
END
""",
o.id,
coalesce(sum(o.total_amount), 0.0),
fragment("CASE WHEN COUNT(?) > 0 THEN ? / COUNT(?) ELSE 0 END",
o.id, coalesce(sum(o.total_amount), 0.0), o.id),
fragment("CASE WHEN COUNT(?) > 1 THEN DATE_PART('day', MAX(?) - MIN(?)) / (COUNT(?) - 1) ELSE 365 END",
o.id, o.inserted_at, o.inserted_at, o.id),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at)),
churn_risk: fragment("""
CASE
WHEN COUNT(?) = 0 THEN 95
WHEN ? > 365 THEN 90
WHEN ? > 180 THEN 70
WHEN ? > 90 THEN 50
WHEN ? > 60 THEN 30
WHEN ? > 30 THEN 20
ELSE 10
END
""",
o.id,
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at),
fragment("DATE_PART('day', NOW() - MAX(?))", o.inserted_at)),
last_order_date: max(o.inserted_at)
}
end
Key Patterns for Complex Queries
1. Data Provider Assignment
- Assign
:data_provider
inmount/3
orhandle_params/3
- Use
{Module, :function, [args]}
tuple format - Pass parameters dynamically based on route or user input
2. Query Design
- Use meaningful aliases with
as:
keyword - Group by all non-aggregated fields
- Use
fragment/2
for complex calculations - Leverage window functions for advanced analytics
3. Field References
- Use
assoc: {:alias_name, :field}
for joined fields - Ensure field keys match query select keys exactly
- Use computed fields sparingly for performance
4. Complex Renderers
- Use
renderer: &function/2
to access full record - Implement conditional rendering based on multiple fields
- Create reusable renderer functions for common patterns
5. Advanced Filters
- Reference table aliases in dynamic queries
- Use complex conditions with multiple criteria
- Combine filters with AND logic automatically
These examples demonstrate how LiveTable handles sophisticated reporting requirements while maintaining clean, maintainable code.