- Add database indexes for common query patterns (path, callsign, device, weather) - Implement packet batching in LiveView to reduce client updates - Create PreparedQueries module for frequently executed database queries - Add KNN (K-nearest neighbors) spatial queries using PostGIS - Implement configurable debouncing for map bounds and hover events - Integrate with existing Redis cache for device lookups - Optimize cleanup worker with batch processing (already implemented) These optimizations significantly improve query performance and reduce database load, especially for high-traffic scenarios. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
145 lines
4.5 KiB
Elixir
145 lines
4.5 KiB
Elixir
defmodule Aprsme.Packets.PreparedQueries do
|
|
@moduledoc """
|
|
High-performance queries using prepared statements and efficient patterns.
|
|
These queries are designed for frequently executed operations.
|
|
"""
|
|
|
|
import Ecto.Query
|
|
|
|
alias Aprsme.Packet
|
|
alias Aprsme.Repo
|
|
|
|
@doc """
|
|
Get the latest packet for a callsign using a prepared statement.
|
|
This is one of the most frequently called queries.
|
|
"""
|
|
@spec get_latest_packet_for_callsign(String.t()) :: Packet.t() | nil
|
|
def get_latest_packet_for_callsign(callsign) when is_binary(callsign) do
|
|
normalized = String.upcase(String.trim(callsign))
|
|
|
|
Repo.one(
|
|
from(p in Packet,
|
|
where: fragment("upper(?)", p.sender) == ^normalized,
|
|
order_by: [desc: p.received_at],
|
|
limit: 1,
|
|
select: %{p | lat: fragment("ST_Y(?)", p.location), lon: fragment("ST_X(?)", p.location)}
|
|
)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Get recent packets within bounds using prepared statement.
|
|
"""
|
|
@spec get_recent_packets_in_bounds(list(), integer()) :: [Packet.t()]
|
|
def get_recent_packets_in_bounds([north, south, east, west] = _bounds, hours_back \\ 1) do
|
|
time_ago = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)
|
|
|
|
Repo.all(
|
|
from(p in Packet,
|
|
where: p.has_position == true,
|
|
where: p.received_at >= ^time_ago,
|
|
where: fragment("ST_Y(?) BETWEEN ? AND ?", p.location, ^south, ^north),
|
|
where: fragment("ST_X(?) BETWEEN ? AND ?", p.location, ^west, ^east),
|
|
order_by: [desc: p.received_at],
|
|
limit: 500,
|
|
select: %{p | lat: fragment("ST_Y(?)", p.location), lon: fragment("ST_X(?)", p.location)}
|
|
)
|
|
)
|
|
end
|
|
|
|
@doc """
|
|
Check if callsign has weather packets using prepared statement.
|
|
"""
|
|
@spec has_weather_packets?(String.t()) :: boolean()
|
|
def has_weather_packets?(callsign) when is_binary(callsign) do
|
|
normalized = String.upcase(String.trim(callsign))
|
|
|
|
query =
|
|
from(p in Packet,
|
|
where: fragment("upper(?)", p.sender) == ^normalized,
|
|
where:
|
|
not is_nil(p.temperature) or
|
|
not is_nil(p.humidity) or
|
|
not is_nil(p.pressure) or
|
|
not is_nil(p.wind_speed),
|
|
limit: 1,
|
|
select: true
|
|
)
|
|
|
|
Repo.exists?(query)
|
|
end
|
|
|
|
@doc """
|
|
Get nearby stations using KNN (K-nearest neighbors) search.
|
|
Uses the <-> operator for efficient spatial queries.
|
|
"""
|
|
@spec get_nearby_stations_knn(float(), float(), String.t() | nil, map()) :: [map()]
|
|
def get_nearby_stations_knn(lat, lon, exclude_callsign \\ nil, opts \\ %{}) do
|
|
limit = Map.get(opts, :limit, 10)
|
|
hours_back = Map.get(opts, :hours_back, 1)
|
|
cutoff_time = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)
|
|
|
|
# Build point for KNN search
|
|
point = %Geo.Point{coordinates: {lon, lat}, srid: 4326}
|
|
|
|
base_query =
|
|
from(p in Packet,
|
|
where: p.has_position == true,
|
|
where: p.received_at >= ^cutoff_time,
|
|
where: not is_nil(p.location),
|
|
distinct: p.base_callsign,
|
|
order_by: [
|
|
asc: p.base_callsign,
|
|
desc: p.received_at
|
|
]
|
|
)
|
|
|
|
query =
|
|
if exclude_callsign do
|
|
from(p in base_query,
|
|
where: p.sender != ^exclude_callsign
|
|
)
|
|
else
|
|
base_query
|
|
end
|
|
|
|
# Subquery to get most recent packet per callsign, then order by distance
|
|
subquery =
|
|
from(p in subquery(query),
|
|
order_by: fragment("? <-> ?", p.location, ^point),
|
|
limit: ^limit,
|
|
select: %{
|
|
callsign: p.sender,
|
|
base_callsign: p.base_callsign,
|
|
lat: fragment("ST_Y(?)", p.location),
|
|
lon: fragment("ST_X(?)", p.location),
|
|
distance: fragment("ST_Distance(?::geography, ?::geography)", p.location, ^point),
|
|
received_at: p.received_at,
|
|
symbol_table_id: p.symbol_table_id,
|
|
symbol_code: p.symbol_code,
|
|
comment: p.comment
|
|
}
|
|
)
|
|
|
|
Repo.all(subquery)
|
|
end
|
|
|
|
@doc """
|
|
Get packet count for an area using prepared statement.
|
|
"""
|
|
@spec get_packet_count_in_area(list(), integer()) :: non_neg_integer()
|
|
def get_packet_count_in_area([north, south, east, west], hours_back \\ 24) do
|
|
time_ago = DateTime.add(DateTime.utc_now(), -hours_back * 3600, :second)
|
|
|
|
query =
|
|
from(p in Packet,
|
|
where: p.has_position == true,
|
|
where: p.received_at >= ^time_ago,
|
|
where: fragment("ST_Y(?) BETWEEN ? AND ?", p.location, ^south, ^north),
|
|
where: fragment("ST_X(?) BETWEEN ? AND ?", p.location, ^west, ^east),
|
|
select: count(p.id)
|
|
)
|
|
|
|
Repo.one(query) || 0
|
|
end
|
|
end
|