Company names are never as clean as they look in a spec. The same business gets registered with a legal Ltd/PLC name (Tesco Stores Ltd), traded under a completely different short or brand name (Tesco), referred to by an abbreviation (RAC), and shows up on invoices, emails, and API payloads under whatever spelling the sender happened to type — ERAC UK LTD, Enterprise Rent-A-Car, Enterprise Rent-A Car. A single name column and a LIKE clause will miss most of those, because you’d need to know in advance which variant the user typed.
The fix doesn’t need a separate aliases table or a search engine. MySQL’s JSON_SEARCH function, combined with a JSON column, gets you there with one extra OR in the query — store every known variant (legal name, short name, abbreviation, previous trading name) as a JSON array next to the canonical name, and match against all of them at once.
1. The Schema
Store the canonical name as normal, and keep every known variant — Ltd name, short name, abbreviation, old trading name, whatever the business is known by — in a JSON array column:
CREATE TABLE companies (
id INT PRIMARY KEY AUTO_INCREMENT,
name VARCHAR(255) NOT NULL,
aliases JSON NULL
);
Which gives you rows like:
| id | name | aliases |
|---|---|---|
| 1 | RAC | ["ERAC UK LTD", "Enterprise Rent-A-Car", "Enterprise Rent-A Car"] |
| 2 | Tesco | ["Tesco Stores Ltd", "Tesco PLC", "TESCO"] |
RAC shows the abbreviation-and-trading-name case — a short, everyday name with the fuller legal and marketing names as aliases. Tesco shows the mirror case — a well-known short name as the canonical entry, with the registered Ltd name, the PLC name, and an all-caps variant as aliases. Same column, same query, either direction.
2. Writing the Aliases
JSON_ARRAY() builds the array directly in SQL — no need to hand-encode JSON in application code:
UPDATE companies
SET aliases = JSON_ARRAY('ERAC UK LTD', 'Enterprise Rent-A-Car', 'Enterprise Rent-A Car')
WHERE id = 1;
UPDATE companies
SET aliases = JSON_ARRAY('Tesco Stores Ltd', 'Tesco PLC', 'TESCO')
WHERE id = 2;
3. Searching Both Columns at Once
This is the part that actually solves the problem — match on the canonical name or any alias, in a single query:
SELECT * FROM companies
WHERE name LIKE '%ERAC UK LTD%' OR JSON_SEARCH(aliases, 'one', '%ERAC UK LTD%') IS NOT NULL;
That query finds row 1 even though name is just RAC — ERAC UK LTD only exists in aliases. It works the other way round too, against row 2:
-- Someone searches the full legal name, but the canonical `name` is just "Tesco"
SELECT * FROM companies
WHERE name LIKE '%Tesco Stores Ltd%' OR JSON_SEARCH(aliases, 'one', '%Tesco Stores Ltd%') IS NOT NULL;
It doesn’t matter which variant — Ltd name, short name, abbreviation — ends up in name and which end up in aliases. The OR covers both, so the search works regardless of which name the row was originally created under.
JSON_SEARCH(json_doc, one_or_all, search_str) walks the JSON document looking for a string value matching search_str, and returns the path to it (e.g. "$[0]") — or SQL NULL if nothing matches. Since we only care whether something matched, not where, wrapping it in IS NOT NULL turns it into a plain boolean check that slots neatly into the OR.
In our filter-driven query, that turns into something like:
name LIKE '%{filter[name]}%' OR JSON_SEARCH(aliases, 'one', '%{filter[name]}%') IS NOT NULL
— the same {filter[name]} value reused on both sides, so one search box matches against the canonical name and every alias in one pass.
4. The Gotcha: No Wildcard Means Exact Match
This is the bit that catches people out the first time. search_str in JSON_SEARCH follows LIKE semantics — which means without a %, it’s an exact match, not a substring search.
-- Matches only an alias that is EXACTLY "ERAC UK LTD"
JSON_SEARCH(aliases, 'one', 'ERAC UK LTD')
-- Matches any alias that CONTAINS "ERAC UK LTD"
JSON_SEARCH(aliases, 'one', '%ERAC UK LTD%')
Drop the % and a perfectly reasonable-looking query will silently return nothing for partial matches, with no error to point you at why. If the search term comes from user input — a filter box, an autocomplete field — wrap it in %...% yourself before it reaches the query, exactly as the snippet above does.
One side effect worth knowing: because % and _ are wildcards, a search term that happens to contain a literal % or _ (a company name with an underscore, say) will be interpreted as a wildcard unless you escape it. JSON_SEARCH supports a custom escape character as a fourth argument if that’s a real risk for your data.
5. 'one' vs 'all'
The second argument controls how much work JSON_SEARCH does:
'one'— stops at the first match and returns a single path. Cheaper, and all we need for an existence check.'all'— finds every matching path and returns them as a JSON array. Useful if you actually need to know which alias matched, not just that one did.
For a filter/search query where you only branch on IS NOT NULL, 'one' is the right choice — there’s no reason to make MySQL keep scanning the array after it already knows the row qualifies.
6. A Note on Performance
JSON_SEARCH scans the JSON document row by row — it isn’t index-accelerated the way a plain column comparison is, even with a functional index on the JSON path, because the wildcard search defeats equality-based indexing. For a companies table with a few thousand rows, this is a non-issue. If you’re doing this kind of alias matching against millions of rows, it’s worth reaching for either:
- A proper
company_aliasesjoin table with an indexedaliascolumn, or - MySQL 8’s full-text search on a generated column that flattens the JSON array into text.
For most day-to-day company lookups, though, JSON_SEARCH on a small JSON array is simpler than either of those and fast enough not to matter.
7. Where This Works
JSON_SEARCH and the JSON column type have been available since MySQL 5.7.8, and carry through unchanged in MariaDB, Amazon RDS for MySQL, and Amazon Aurora MySQL — so this pattern works without modification across the MySQL-compatible databases most AWS-based stacks already run on.
Takeaways
- Keep known aliases as a
JSONarray column next to the canonical name — no extra table needed for a handful of aliases per row. - Combine
name LIKE '%...%'withJSON_SEARCH(aliases, 'one', '%...%') IS NOT NULLto search both in one query. - Always wrap your search term in
%...%—JSON_SEARCHwithout wildcards is an exact match, not a substring search, and it fails silently. - Use
'one'for existence checks,'all'only when you need to know which alias matched. - Reach for a proper join table or full-text search once the table — or the aliases per row — gets large enough that a full JSON scan actually shows up in your query times.