How does a collation or character-set mismatch between two joined VARCHAR keys hurt the join?
answer
- Two string columns, two different declarations
- Equality on text is not byte equality
- One side has to be brought to the other
- Per-row conversion means no index lookup
- Fix it in the column definitions, not the predicate
basics
~20 sComparing two character values requires one agreed collation. When the joined columns declare different collations or character sets, the engine either refuses the comparison or converts one side for every row, which removes that column's index as a way to drive the join.
solid answer
~50 sA string comparison is only defined relative to a collation, so a join predicate between two `VARCHAR` keys needs both sides to agree. Engines resolve disagreement in opposite ways. PostgreSQL treats conflicting collations in a comparison as an error rather than silently choosing one. MySQL may raise `Illegal mix of collations`, or — when the character sets differ, say `latin1` against `utf8mb4` — convert one column to the other's character set. That conversion is applied per row, so the converted side's index can no longer drive the join, and a lookup-driven join degrades into scanning. The durable fix is DDL: give both columns the same type, character set and collation, then re-verify the plan. Patching the predicate with `COLLATE` puts the expression on the column, which keeps the index out of play on that side; it is a diagnosis aid, not a fix.
code
sql · 6 lines-- MySQL: list character columns whose collation disagrees across tables
SELECT table_name, column_name, character_set_name, collation_name
FROM information_schema.columns
WHERE table_schema = DATABASE()
AND column_name = 'customer_ref'
AND data_type IN ('char', 'varchar');go deeper
Know that character columns carry a character set and a collation, and that two columns you join on should declare the same ones. Check the column definitions, not just the types.
Explain what the engine does with a conflict — error in some engines, convert one side in others — and why converting a column per row means that side's index can no longer serve the join lookup.
Show the whole investigation and repair: read the catalog to find the disagreement, confirm with the plan, then fix in DDL and re-verify both the plan and the row count, since collation also decides what counts as equal.
Own the prevention: mismatches originate in migrations and server-default changes across upgrades, never in a query anyone reviews. Decide on a schema-wide collation standard and an automated catalog check that enforces it.
## A string comparison needs one collation Character data carries two properties beyond its length: the character set that says how the bytes encode characters, and the collation that says how two values compare and sort. `=` on strings is a collation-sensitive operation, so a predicate like `o.customer_ref = c.customer_ref` is only well defined once the engine knows which collation to apply. If both columns declare the same one, there is nothing to decide. If they disagree, the engine has to resolve the conflict. ## What engines do when the two sides disagree PostgreSQL is strict about it: when two operands carry conflicting collations, it reports a collation mismatch rather than quietly picking one, so you find out at development time. MySQL takes both routes depending on the case. Two different collations of the same character set with equal coercibility produce `Illegal mix of collations`. Different character sets — the common one being a `latin1` legacy table joined to a `utf8mb4` newer table — are resolved by converting one side to the other's character set, and the statement runs. That is the dangerous outcome, because nothing fails and nothing warns; only the plan and the clock change. Because engines genuinely diverge here, treat the exact behaviour as something to verify against your own engine's documentation rather than something to assume. ## Why the join loses its index A join between two tables typically works by taking a value from one side and looking it up on the other. That lookup uses an index over the second column's stored values, in that column's collation order. Once the engine has to convert or re-collate that column, the predicate is testing a derived value: not the bytes the index stores, and not in the order the index stores them. There is nothing to search, so the engine must produce all the candidate rows and compare them after conversion. On tables where the join was previously a per-row index lookup, this is exactly the "worked in dev, crawls in production" profile — the mismatch costs nothing on a thousand rows and everything on ten million. ## Character set versus collation They fail differently and it is worth keeping them apart. A **character-set** mismatch is a re-encoding of the value and always requires conversion. A **collation** mismatch may leave the bytes alone but changes what counts as equal — a case-insensitive collation matches `'ABC'` with `'abc'`, a case-sensitive one does not. So beyond performance, a collation difference can change the *result*: rows that match under one collation do not match under the other, and a mis-resolved join can silently return the wrong number of rows. That makes "just pick one and cast" a semantic decision, not only a performance one. ## The fix is DDL The repair belongs in the schema. Align the joined columns: same declared type and length, same character set, same collation. `ALTER TABLE` performs it, with the exact syntax varying by engine, and on a large table it rewrites data, so plan it like any other migration — and remember the indexes on that column are rebuilt in the new collation. The tempting shortcut is to write the collation into the query: ```sql -- diagnosis, not a fix: the COLLATE lands on a column, -- so that side's index cannot drive the join SELECT o.id FROM orders o JOIN customers c ON o.customer_ref = c.customer_ref COLLATE utf8mb4_0900_ai_ci; ``` It makes the statement run and it proves the diagnosis, but the expression is on a column, so you have simply chosen which side loses its index. Use it to confirm the cause, then fix the definitions. ## Where mismatches come from They are almost always historical. A table created years earlier under an older server default meets a table created after an upgrade; MySQL 8.0's default collation for `utf8mb4` differs from 5.7's, so tables created on either side of that upgrade disagree by default. Or a column is added by a migration that spells out a character set while its neighbours inherit the database default. Or two systems are merged. Because the divergence is in the DDL and not in any query, code review never sees it. ## Detecting it Read the catalog: the information schema exposes each character column's character set and collation, and one query across the columns you join on will list every disagreement in the schema. Do that as a standing check rather than waiting for a slow join to report it.
- Can a collation mismatch change which rows a join returns, not just how fast it runs?Yes. Collation defines equality as well as ordering, so a case- or accent-insensitive collation matches pairs that a sensitive one rejects. Whichever collation wins the conflict decides the result set, which is why resolving a mismatch is a semantic decision. Confirm the expected row count before and after any change, not just the plan.
- Why is adding COLLATE to the join predicate not a real fix?Because the COLLATE applies to a column expression, so that side is re-collated per row and its index can no longer drive the join. You have chosen which table pays instead of removing the cost. It is useful to prove the diagnosis and to unblock a one-off report, but the durable fix is aligning the column definitions.
- How would you catch these mismatches before they reach production?Query the catalog. The information schema exposes each character column's character set and collation, so a scheduled check can flag any column pair used as a join key whose declarations disagree, and any column that deviates from the database default. Adding that check to CI catches the migration that spells out a character set its neighbours do not.
saying these in an interview costs you the question
- Thinks collation only affects ORDER BY output
- Adds COLLATE to the predicate and calls it fixed
- Assumes both sides convert equally, so nothing is lost
- Ignores that collation changes which rows match
- Believes matching column types is enough without charset