跪拜 Guibai
← All articles
Database

Nine SQL Pitfalls That Break Silently During Database Migration

By 倔强的石头_ ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

A migration that passes every syntax check can still corrupt business data silently. These nine failure modes are deterministic and reproducible across Oracle-to-KES and MySQL-to-KES moves, and the three-layer detection method catches them before production.

Summary

Database migrations to platforms like KingbaseES often pass syntax checks but produce wrong row counts, missing data, or shifted values. The root cause falls into three categories: optimizer behavior differences (outer join elimination, predicate pushdown boundaries, subquery unnesting), SQL semantic standard differences (NULL vs. empty string equivalence, three-valued logic short-circuiting, NULL sort position), and function/expression behavior differences (side-effect ordering in WHERE clauses, implicit string-number conversion, time zone parsing).

Each pitfall is silent — no error, no warning, just a successful execution with subtly wrong results. An outer join gets rewritten to an inner join when a WHERE condition rejects NULLs on the nullable side. A NOT IN subquery returns an empty set the moment a NULL appears in production. An ORDER BY clause places NULLs differently across Oracle, MySQL, and KES, breaking pagination.

The fix is a three-layer methodology: static code scanning with regex or SQL parsers to flag suspicious patterns, execution plan and result-set comparison between old and new systems using EXPLAIN and EXCEPT, and production canary releases with continuous monitoring of slow queries, reconciliation metrics, and UDF call patterns. Rewrites favor explicit CTEs, NOT EXISTS over NOT IN, manual NULLS FIRST/LAST declarations, and removing stateful UDFs from WHERE clauses.

Takeaways
Outer joins get silently rewritten to inner joins when a WHERE condition rejects NULLs on the nullable side; move such conditions into the ON clause instead.
Predicate pushdown can shift a WHERE filter inside a subquery that contains LIMIT or ORDER BY, changing which rows are sampled before filtering; use MATERIALIZED CTEs to block the pushdown.
NOT IN with a nullable subquery column returns an empty set the moment a NULL appears; rewrite to NOT EXISTS or LEFT JOIN with IS NULL.
Oracle treats empty strings as NULL, but KES treats them as distinct zero-length values; always use IS NULL for emptiness checks.
Three-valued logic short-circuiting varies across databases, so a UDF with side effects in a WHERE clause may or may not execute depending on the optimizer; never put stateful UDFs in WHERE.
NULL sort position differs: MySQL puts NULLs first on ASC, Oracle and KES put them last; always declare NULLS FIRST or NULLS LAST explicitly.
Implicit string-to-number conversion in KES can disable indexes and throw errors on non-numeric characters; use explicit CAST or quoted literals.
SYSDATE semantics differ between Oracle (OS time zone) and KES (session time zone); use CURRENT_TIMESTAMP or explicitly SET TIME ZONE at session start.
A three-layer detection workflow — static scan, EXPLAIN + EXCEPT comparison, canary monitoring — catches silent logic bugs that syntax checks miss.
Conclusions

High syntax compatibility percentages are misleading: the real migration cost lives in the 5% of SQL that runs without error but produces wrong results, and that 5% is concentrated in optimizer decisions and semantic edge cases, not syntax.

The optimizer is not a neutral executor — it actively rewrites query semantics when it believes an optimization is safe, and the definition of 'safe' differs across database engines. This makes EXPLAIN comparison the single highest-signal diagnostic during migration.

CTEs with MATERIALIZED are an underused defense against optimizer-induced semantic drift; they act as optimization fences that preserve the original intent of subquery boundaries.

NULL handling is the most cross-cutting source of silent corruption, appearing in joins, subqueries, sorting, string equivalence, and three-valued logic — and no two databases agree on all of them.

UDF side effects in WHERE clauses are a time bomb in any database, but migration surfaces the bomb because the optimizer's reordering freedom changes; the fix is architectural, not syntactic — move state changes outside SQL.

Concepts & terms
Outer Join Elimination
An optimizer rewrite that converts a LEFT or RIGHT JOIN into an inner join when a WHERE condition filters out NULLs on the nullable side, because the NULL-padded rows would be discarded anyway. This changes row counts when the intent was to preserve all rows from the preserved table.
Predicate Pushdown
An optimization where a filter condition from an outer query block is moved into a subquery or view to reduce rows early. When the subquery contains LIMIT, ORDER BY, DISTINCT, or aggregates, pushing the filter down changes which rows are sampled before the limiting operation, altering results.
Three-Valued Logic
SQL's logic system where a condition can evaluate to TRUE, FALSE, or UNKNOWN (when NULL is involved). Expressions like NOT IN (NULL, ...) always yield UNKNOWN, which is treated as not TRUE in WHERE clauses, causing empty result sets.
NULL-Empty String Equivalence
Oracle treats a zero-length string ('') as identical to NULL, while standards-compliant databases like PostgreSQL and KES treat them as distinct values. Code relying on Oracle's behavior will silently miss or include wrong rows after migration.
MATERIALIZED CTE
A Common Table Expression explicitly marked MATERIALIZED, which forces the database to compute the CTE result once and store it, preventing the optimizer from pushing outer filters into the CTE or inlining it. Acts as an optimization fence.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗