Fuzzy Matching in SQL: Native Functions, UDFs, and Why It Breaks at Scale
Fuzzy matching in SQL means comparing strings by similarity rather than exact equality, so that "Jon Smith" and "John Smith" can be treated as the same record.
As of 2025 you can do this with native functions in several engines, most recently SQL Server 2025, without writing your own algorithm. It is a specific form of fuzzy matching applied inside the database, and it is genuinely useful for small jobs.
The question is not whether SQL can fuzzy match. It can. The question is what happens when the row counts grow, because every one of these functions compares strings pairwise, and pairwise cost grows with the square of the table size.
Can you do fuzzy matching in SQL?
Yes. SQL Server 2025 added four native fuzzy string functions (EDIT_DISTANCE, EDIT_DISTANCE_SIMILARITY, JARO_WINKLER_DISTANCE, and JARO_WINKLER_SIMILARITY), PostgreSQL offers fuzzystrmatch and pg_trgm, and most other dialects expose SOUNDEX or an edit-distance function. All of them compare strings pairwise, so a naive self-join on one million rows implies roughly 500 billion comparisons, which is why production matching relies on blocking or a dedicated engine.
The functions are new in SQL Server and long-established in PostgreSQL, but the constraint is the same everywhere. A fuzzy function tells you how similar two strings are; it does not decide which records to compare, and comparing every pair is what gets expensive.
Two shapes of the problem show up in practice. Deduplicating one table means matching it against itself, a self-join, which is the expensive case. Matching two separate tables, an incoming file against a master, is a cross-join bounded by the product of the two row counts rather than the square of one; it also grows fast, and both are why the naive version stops working.
Native fuzzy matching in SQL Server 2025
SQL Server 2025 is the first release with fuzzy string functions built into T-SQL, so you no longer need a CLR assembly or a hand-written UDF for basic similarity. There are four functions, split into two families.
EDIT_DISTANCE returns the number of single-character edits (insert, delete, substitute) needed to turn one string into another, and EDIT_DISTANCE_SIMILARITY returns a 0 to 100 score derived from that distance. JARO_WINKLER_DISTANCE and JARO_WINKLER_SIMILARITY use the Jaro-Winkler algorithm, which weights matching prefixes more heavily and suits short strings like personal names; JARO_WINKLER_SIMILARITY returns a score from 0 to 1.
Choosing between the two families is a practical decision. Edit distance is the better default for general strings, product codes, and SKUs, where any character can differ. Jaro-Winkler is the better default for short personal and company names, where a shared prefix is meaningful and a transposition is common. Running both and comparing scores is a reasonable way to calibrate thresholds on your own data.
Two honest details that most tutorials skip. First, these functions are in preview at release and must be turned on with a database-scoped configuration, and they are tied to compatibility level 170. Second, they do not currently honor collation semantics, which means they are effectively case-sensitive no matter what your database collation says, so you should normalize case before comparing.
Microsoft has stated this collation behavior may change once collation support is added, so a query that relies on the current case-sensitivity could return different results after a later update. That is a reason to pin the behavior you depend on rather than assume it is stable.
There is also a performance ceiling built into how these functions run. You call them in a WHERE clause or a join predicate, and because the result is computed per pair at query time, the optimizer cannot use an index on it. Every candidate pair is scanned. That is fine for a lookup against a small reference table, and it is the root of the scaling problem once both sides of the join are large.
Fuzzy matching in PostgreSQL
PostgreSQL has supported fuzzy matching for years through two extensions. fuzzystrmatch provides levenshtein, soundex, metaphone, and dmetaphone. pg_trgm provides trigram similarity through the similarity function and the percent operator.
The feature that sets Postgres apart is indexing. A trigram GIN or GiST index lets a similarity filter use an index instead of scanning every row, which none of the other engines covered here offer for approximate matching. That does not remove the pairwise problem, but it makes candidate lookup far cheaper than a full scan.
Two details matter when you use pg_trgm in production. The percent operator compares against a session threshold, pg_trgm.similarity_threshold, which defaults to 0.3, so set it to the value your data needs before relying on the operator. word_similarity is the variant to reach for when you are matching a short string inside a longer one, such as a company name embedded in a full legal name.
The index accelerates finding candidates for one record. It does not change the arithmetic of comparing every record against every other record, which is the next problem.
Fuzzy matching across other SQL dialects
Native support varies widely. Oracle and Snowflake ship strong functions, MySQL ships almost nothing beyond SOUNDEX, and BigQuery covers edit distance but not Jaro-Winkler. The table below summarizes where each capability lives.
Function names and availability shift between versions, so treat this as a starting map rather than a permanent one. The companion query set (linked at the end) has a runnable statement for each engine, which should be checked on the target version before it goes into production.
SOUNDEX and phonetic matching
SOUNDEX encodes a word by how it sounds, reducing it to a letter and three digits, so names that sound alike collapse to the same code. It is the one fuzzy technique available in almost every dialect, which is why people reach for it, and it is also the most misunderstood.
Consider three surnames. Smith and Smyth both encode to S530, so SOUNDEX treats them as a match, which is the behavior you want. Schmidt encodes to S253, a different code, so SOUNDEX does not match it to Smith even though a human might consider them related.
SQL Server pairs SOUNDEX with DIFFERENCE, which returns a 0 to 4 score for how close two SOUNDEX codes are, where 4 means the codes are identical. It is a blunt instrument, because it inherits every weakness of SOUNDEX. For non-English names, the double metaphone functions in Postgres (dmetaphone and dmetaphone_alt) handle more variation, though they remain phonetic approximations rather than match decisions.
That pattern is the lesson. SOUNDEX is English-centric, ignores vowels, and produces both false matches (unrelated names that share a code) and false negatives (real variants that diverge). It works well as a blocking key to narrow candidates, and poorly as the final decision about whether two records are the same.
Why SQL fuzzy matching does not scale
A fuzzy function compares two strings. Matching a table against itself means comparing every record with every other record, which is n(n-1)/2 pairs for n rows. That figure grows with the square of the row count, so each tenfold increase in rows is a hundredfold increase in work.
The table below shows the pairwise comparison count at several row counts, with an illustrative time column that assumes one million comparisons per second on a single thread. Real throughput varies with hardware, function, and string length, so read the time column as an order of magnitude, not a promise. This complexity math is the core argument for data matching that does not brute-force every pair.
The pattern is unforgiving. A job that finishes in seconds at ten thousand rows takes days at a million and years at ten million. Somewhere in the low hundreds of thousands of rows, a naive self-join stops being a query you run and becomes a query you wait on, which is why the next section exists.
Hardware does not rescue this. Doubling cores or moving to a bigger box buys a constant factor, while the problem grows with the square of the row count. Ten times the cores is one order of magnitude; ten times the rows is two. The gap widens as the data grows, which is why the fix is fewer comparisons through blocking, not more compute thrown at all of them.
Blocking: how to make it tractable
Blocking is the standard record-linkage technique for cutting the comparison count. Instead of comparing every pair, you partition records into blocks that share a key, such as a ZIP code, the first three letters of a surname, or a SOUNDEX code, and you compare only records inside the same block.
The effect is large. Splitting one million rows into 5,000 blocks of roughly 200 records each drops the comparison count from about 500 billion to about 100 million, a reduction of several thousand times. The comparisons that remain are the ones most likely to be real matches, because the block key already forced some agreement.
A concrete two-pass setup makes the multi-pass idea clear. Pass one blocks on the first three characters of the surname and compares within each block. Pass two blocks on the SOUNDEX code of the surname and compares again, then you union the candidate pairs from both passes. A record with a typo in its first three characters is still caught in pass two if it sounds the same, which is the reason to run more than one key.
Blocking has one cost: a record whose block key contains a typo lands in the wrong block and its true match is never compared. Production systems handle this by blocking on several different keys across multiple passes, so a record has more than one chance to meet its match. That multi-pass logic is straightforward to describe and tedious to maintain by hand in SQL.
What SQL fuzzy matching cannot give you
Scoring string similarity is one step of a matching program, not the whole of it. Once you move from finding similar pairs to producing a clean, trustworthy set of unified records, several requirements appear that a fuzzy function does not address.
- Clustering and transitive closure: if record A matches B and B matches C, grouping A, B, and C into one entity. A pairwise query returns pairs, not groups.
- Survivorship and golden records: deciding which value wins when a group is merged, and producing one canonical record per entity.
- Confidence tiers and review: routing the ambiguous band to a human instead of forcing every pair through a single hard threshold.
- Cross-column matching: comparing a value in one field against a differently named field in another source, which a same-column function cannot express.
- Auditability: showing an examiner or auditor why two records were merged, with the rule and the score that produced the decision.
This is the line where teams move from SQL to a matching engine. MatchLogic’s MatchCore runs the same fuzzy comparison (Jaro-Winkler, Levenshtein, phonetic, plus proprietary logic) under transparent, configurable rules, and it adds the clustering, survivorship, confidence tiers, and audit trail a query cannot. For entity resolution, where records must be unified into a single identity across systems, MatchSense handles the grouping and canonicalization that SQL cannot express.
Because MatchLogic runs on-premise, the data and the matching both stay inside your environment, which matters when the records are regulated. The practical result is that the work you would otherwise script as a growing pile of self-joins and dedupe software queries becomes a configured, reviewable process with a record of every decision.
When to keep fuzzy matching in SQL, and when to move off it
SQL fuzzy matching is the right tool for a real set of jobs, and the wrong tool past a clear line. The checklist below is a quick way to decide which side you are on.
Keep it in SQL when
- The job is one-time or ad hoc, not a recurring production process.
- The data is under roughly 100,000 rows, or you can block it down to that scale per pass.
- You are matching on a single field, with no weighting across fields.
- Both datasets already live in one database, and you have no audit requirement.
Move off SQL when
- Matching is recurring, feeds a production system, or runs across millions of rows.
- You need weighted matching across several fields and confidence tiers, not a single threshold.
- You need survivorship, golden records, cross-column matching, or an audit trail.
- You are matching across systems or databases, or the data is regulated and must stay on-premise.
Making the call on SQL fuzzy matching
SQL fuzzy matching is a legitimate technique with a hard ceiling, and knowing where the ceiling sits is more useful than knowing every function name. Native functions now exist in most major engines, and for one-time work under roughly a hundred thousand rows they are the fastest path to an answer.
The ceiling is arithmetic rather than a limitation of any particular database. Pairwise comparison grows with the square of the row count, so the decision is not which function to use but whether you can reduce the number of comparisons enough for the job to finish. Blocking is the answer inside SQL, and it is also the point where the maintenance burden starts to exceed the value of staying in SQL.
Before optimizing a query further, run the comparison count for your actual row counts. If the number lands in the billions and the job is recurring, the useful next step is scoping a matching engine rather than tuning the join.
Frequently asked questions
Does SQL Server have a built-in fuzzy match function?
Yes. SQL Server 2025 added four native fuzzy functions: EDIT_DISTANCE, EDIT_DISTANCE_SIMILARITY, JARO_WINKLER_DISTANCE, and JARO_WINKLER_SIMILARITY. They are in preview, must be enabled with a database-scoped configuration, and do not yet honor collation, so they behave case-sensitively. Earlier versions have only SOUNDEX and DIFFERENCE.
How do I do a Levenshtein join in Postgres?
Enable the fuzzystrmatch extension and call levenshtein(a.name, b.name) in the join or WHERE clause. A full self-join does not scale, so in practice you filter candidates first with pg_trgm and a GIN or GiST trigram index, then compute Levenshtein only on the survivors.
Is SOUNDEX good enough for name matching?
Not on its own. SOUNDEX is a crude English-phonetic code that ignores vowels, so it both misses real variants (Schmidt and Smith get different codes) and over-groups unrelated names that share a code. Use it as a blocking key to reduce candidate pairs, not as the final match decision.
How many records can SQL fuzzy match before it becomes impractical?
A naive self-join compares n(n-1)/2 pairs, so cost grows with the square of the row count. Around 100,000 rows a self-join is already billions of comparisons and takes hours; at one million rows it is roughly 500 billion comparisons. Past low hundreds of thousands of rows you need blocking or a dedicated engine.
What is blocking in record linkage?
Blocking partitions records into groups that share a key, such as a ZIP code or the first three letters of a surname, and compares only records within the same group. It cuts comparisons from the square of the row count toward linear, at the cost of missing matches that disagree on the block key, which is why production systems block on several keys across passes.
Can you fuzzy match across two databases in SQL?
Not directly. Fuzzy functions operate within one database, so cross-database or cross-server matching requires linked servers, a federated query, or copying both datasets into one place first. This is one of the points where teams move the match out of SQL and into an engine or ETL layer that reads from multiple sources.


