fix(geocoding): index the local place-name prefix search
UPPER("name") LIKE UPPER(?) could never use the plain index on "name",
so every local search did a full table scan over ~170k reference
rows on TaskService's single-threaded DATABASE pool. Add a generated
name_upper column with its own index and query that instead.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016okXfGz39FtQawFWDYpQ5C
This commit is contained in:
@@ -69,9 +69,19 @@ public class GeoReferenceDatabase {
|
||||
)
|
||||
""");
|
||||
jdbc.execute("CREATE INDEX IF NOT EXISTS \"ix_place_lat_lon\" ON \"place\" (\"latitude\", \"longitude\")");
|
||||
// Backs PlaceReferenceQueryService.search's prefix lookup — a plain B-tree still helps a LIKE
|
||||
// 'x%' scan, unlike the trailing-wildcard search this table has no use case for.
|
||||
// No longer what backs PlaceReferenceQueryService's prefix search (see ix_place_name_upper below) —
|
||||
// kept only in case something else ever needs a plain, case-sensitive lookup on "name" itself.
|
||||
jdbc.execute("CREATE INDEX IF NOT EXISTS \"ix_place_name\" ON \"place\" (\"name\")");
|
||||
// H2 cannot use a plain index on "name" to accelerate a predicate wrapped in UPPER("name") — it had
|
||||
// been doing a full table scan over all ~170k rows on every search (confirmed via EXPLAIN), which
|
||||
// is exactly what made typing/deleting in "Add a location" feel like it froze the more you typed:
|
||||
// every settled keystroke re-scanned the whole table on TaskService's single-threaded DATABASE pool.
|
||||
// A generated column actually stores UPPER("name") per row, so a real index can be built on it —
|
||||
// ix_place_name_upper turns that scan into an index range lookup. IF NOT EXISTS on both statements
|
||||
// makes this a one-time cost (computed for every existing row) the first time an already-populated
|
||||
// database is opened after this change, never again after.
|
||||
jdbc.execute("ALTER TABLE \"place\" ADD COLUMN IF NOT EXISTS \"name_upper\" VARCHAR(200) GENERATED ALWAYS AS (UPPER(\"name\"))");
|
||||
jdbc.execute("CREATE INDEX IF NOT EXISTS \"ix_place_name_upper\" ON \"place\" (\"name_upper\")");
|
||||
}
|
||||
|
||||
/** The reference database's own {@link JdbcTemplate} — never a Spring bean, see the class javadoc. */
|
||||
|
||||
@@ -5,6 +5,7 @@ import org.springframework.jdbc.core.RowMapper;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
/**
|
||||
* Read-only access to the reference {@code "place"} table for the Maintenance geocoding panel — the
|
||||
@@ -44,6 +45,13 @@ public class PlaceReferenceQueryService {
|
||||
* Places whose name starts with {@code query} (case-insensitive), most populous first — backs the
|
||||
* "add a place" search in {@code GeoLocationEditView}. Blank input finds nothing rather than every
|
||||
* row: a fresh search field with no text yet must not dump the whole table into a dropdown.
|
||||
*
|
||||
* <p>Filters on {@code "name_upper"} (a generated {@code UPPER("name")} column, see
|
||||
* {@link GeoReferenceDatabase}) rather than wrapping {@code "name"} in {@code UPPER(...)} at query
|
||||
* time — H2 cannot use a plain index on {@code "name"} to accelerate a predicate on a function of it,
|
||||
* so that shape was a full table scan over every one of the ~170k reference rows on every call. The
|
||||
* query string is upper-cased here instead, in Java, so the bind parameter matches the stored column
|
||||
* directly and the index on it actually gets used.
|
||||
*/
|
||||
public List<PlaceRow> searchByNamePrefix(String query, int limit) {
|
||||
if (query.isBlank()) {
|
||||
@@ -53,11 +61,11 @@ public class PlaceReferenceQueryService {
|
||||
"""
|
||||
SELECT "id", "name", "country_code", "admin1_code", "latitude", "longitude", "population"
|
||||
FROM "place"
|
||||
WHERE UPPER("name") LIKE UPPER(?) ESCAPE '\\'
|
||||
WHERE "name_upper" LIKE ? ESCAPE '\\'
|
||||
ORDER BY "population" DESC NULLS LAST
|
||||
LIMIT ?
|
||||
""",
|
||||
ROW_MAPPER, escapeLikeWildcards(query.strip()) + "%", limit);
|
||||
ROW_MAPPER, escapeLikeWildcards(query.strip()).toUpperCase(Locale.ROOT) + "%", limit);
|
||||
}
|
||||
|
||||
/** {@code _} and {@code %} are LIKE wildcards; a place name containing either must match literally. */
|
||||
|
||||
Reference in New Issue
Block a user