Back to Indexing
Q2Question 2 of 18

CAST / CONVERT, implicit type conversion, and index defeat

Key Concepts
  • `CAST(expr AS type)` and vendor-specific `CONVERT` explicitly change a value's data type, and you should always cast literals, never columns.
  • Implicit type conversion is when the database silently coerces one operand to match the other in a comparison, following type-precedence rules.
  • Implicit conversion is dangerous because the engine may apply the cast to the indexed column, making the predicate non-sargable and forcing a full scan.
  • The fix is to match the literal's type to the column's type: compare VARCHAR to string literals, BIGINT to integer literals.
  • In JDBC, bind parameters with the correct type (`setString` for VARCHAR, `setLong` for BIGINT) to avoid implicit casts.
  • Implicit conversion bugs hide until a non-numeric value or data growth makes the scan visibly slow; check EXPLAIN for unexpected casts.

Definition

Match the literal's type to the column's type – an implicit conversion on the column silently kills the index.
  • CAST(expr AS type) – Explicitly changes a value's data type; use it on literals and parameters, never on indexed columns.
  • CONVERT – A vendor-specific function (SQL Server, MySQL) for type conversion, often with style codes for formatting.
  • Implicit type conversion – The database silently coerces one operand to match the other based on type-precedence rules when the types differ in a comparison.
  • Index defeat – When the engine applies the implicit cast to the indexed column instead of the literal, the B-tree ordering on the raw column becomes useless and the query falls back to a full scan.
  • JDBC binding – Always use setString() for VARCHAR columns and setLong() for BIGINT columns to send the correct type to the database.

National Bank of India stores account numbers as text like "SAV2001". If a query accidentally compares that text column to a number, the database quietly tries to turn each stored account number into a number to compare. Doing that to every row means it cannot use the account-number index – it must read the whole table, and worse, the conversion can fail or match the wrong rows.

Understanding the Concepts

What is CAST and CONVERT?

Explicit type conversion changes a value's data type deliberately. Use CAST for standard SQL and CONVERT for vendor-specific features.

SQL
-- Standard SQL: CAST
SELECT CAST('2026-10-25' AS DATE);           -- string to date
SELECT CAST(50000.00 AS VARCHAR);            -- number to string
SELECT CAST('12345' AS INTEGER);             -- string to integer

-- SQL Server: CONVERT with style codes
SELECT CONVERT(DATE, '2026-10-25', 23);      -- ISO format
SELECT CONVERT(VARCHAR, GETDATE(), 120);     -- 'YYYY-MM-DD HH:MI:SS'

-- MySQL: CONVERT or CAST
SELECT CAST('2026-10-25' AS DATE);
SELECT CONVERT('12345', SIGNED INTEGER);

The rule: always cast the literal or parameter, never the indexed column.

SQL
-- Good: cast the literal to match the column type
SELECT * FROM transactions
WHERE created_at >= CAST('2026-10-01' AS TIMESTAMP);

-- Bad: cast the indexed column (defeats the index)
SELECT * FROM transactions
WHERE CAST(created_at AS DATE) = '2026-10-25';
-- This is a function on the column -> Seq Scan

What is implicit type conversion?

Implicit conversion happens when the database silently coerces one side of a comparison to match the other, following type-precedence rules. In most engines, numeric types outrank character types.

SQL
-- account_id is VARCHAR(20)
-- Comparing to a number triggers implicit conversion
SELECT * FROM accounts WHERE account_id = 2001;

-- The database sees: VARCHAR vs INTEGER
-- Type precedence: INTEGER > VARCHAR
-- So the database casts the COLUMN: CAST(account_id AS INTEGER) = 2001
-- This is a function on the column -> index is defeated

Why does implicit conversion defeat the index?

When the engine applies the cast to the indexed column, it must compute that function on every row. The B-tree is sorted by the raw column value, not the cast value, so there is no contiguous range to seek.

SQL
-- Index on accounts(account_id) (VARCHAR)
-- Implicit cast on the column defeats it
EXPLAIN ANALYZE
SELECT * FROM accounts WHERE account_id = 2001;
TEXT
Seq Scan on accounts
  Filter: ((account_id)::integer = 2001)
  Rows Removed by Filter: 9,999,999
  Execution Time: 3,200 ms  -- full table scan
SQL
-- Fix: compare string to string
EXPLAIN ANALYZE
SELECT * FROM accounts WHERE account_id = '2001';
TEXT
Index Scan using accounts_pkey on accounts
  Index Cond: (account_id = '2001')
  Rows: 1
  Execution Time: 0.05 ms  -- 64,000x faster

What problems can implicit conversion cause beyond performance?

Implicit conversion can also cause runtime errors and incorrect matches.

SQL
-- Runtime error: non-numeric value in a VARCHAR column
-- account_id contains 'SAV2001' (has letters)
SELECT * FROM accounts WHERE account_id = 2001;
-- The database tries CAST('SAV2001' AS INTEGER)
-- ERROR: invalid input syntax for type integer: "SAV2001"

-- Incorrect matches: leading zeros, spaces
-- '0042' as VARCHAR vs 42 as INTEGER
-- CAST('0042' AS INTEGER) = 42 -> matches, but '0042' != '42' as strings
-- This can return unexpected rows

How do you prevent implicit conversion in JDBC?

Always bind parameters with the correct type.

JAVA
// com.bank.service.AccountLookupService
package com.bank.service;

import java.math.BigDecimal;
import java.sql.*;

public class AccountLookupService {

    /**
     * Wrong: binding an int when the column is VARCHAR.
     * This causes implicit CAST on the column, defeating the index.
     */
    public void lookupWrong(Connection conn, int id)
            throws SQLException {
        String sql = "SELECT * FROM accounts WHERE account_id = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setInt(1, id);  // int sent for VARCHAR column!
            // Database: CAST(account_id AS INT) = 2001 -> Seq Scan
        }
    }

    /**
     * Correct: binding a String for a VARCHAR column.
     * Types match, index is used.
     */
    public BigDecimal lookupCorrect(Connection conn, String accountId)
            throws SQLException {
        String sql = "SELECT balance FROM accounts WHERE account_id = ?";
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, accountId);  // String for VARCHAR -> Index Scan
            try (ResultSet rs = ps.executeQuery()) {
                if (rs.next()) {
                    return rs.getBigDecimal("balance");
                }
                return null;
            }
        }
    }

    /**
     * Timestamp filter: bind as Timestamp, not String.
     */
    public int countTransactionsAfter(
            Connection conn, String accountId, Timestamp after)
            throws SQLException {
        String sql = """
            SELECT COUNT(*)
            FROM   transactions
            WHERE  account_id = ?
              AND  created_at >= ?
            """;
        try (PreparedStatement ps = conn.prepareStatement(sql)) {
            ps.setString(1, accountId);      // VARCHAR -> setString
            ps.setTimestamp(2, after);        // TIMESTAMP -> setTimestamp
            try (ResultSet rs = ps.executeQuery()) {
                rs.next();
                return rs.getInt(1);
            }
        }
    }
}

What are the type-precedence rules?

Different engines have different type-precedence hierarchies, but the general pattern is that numeric types outrank character types.

TEXT
General type precedence (highest to lowest):
  1. DATETIME / TIMESTAMP
  2. NUMERIC / DECIMAL / FLOAT
  3. INTEGER / BIGINT
  4. VARCHAR / CHAR / TEXT

When comparing VARCHAR vs INTEGER:
  - INTEGER outranks VARCHAR
  - So the VARCHAR column is cast to INTEGER
  - This defeats the index on the VARCHAR column

| Left Side | Right Side | Who Gets Cast | Index Impact |
|---|---|---|---|
| VARCHAR column | INT literal | Column cast to INT | Index defeated |
| VARCHAR column | VARCHAR literal | No cast needed | Index used |
| INT column | VARCHAR literal | Literal cast to INT | Index used |
| TIMESTAMP column | VARCHAR literal | Literal cast to TS | Index used |

How do you detect implicit conversions?

Check EXPLAIN ANALYZE for unexpected casts or filter functions.

SQL
-- Look for a cast in the filter
EXPLAIN ANALYZE
SELECT * FROM accounts WHERE account_id = 2001;
-- Filter: ((account_id)::integer = 2001)
-- The ::integer cast is the implicit conversion
-- If you see this -> fix the literal type

-- After fix:
EXPLAIN ANALYZE
SELECT * FROM accounts WHERE account_id = '2001';
-- Index Cond: (account_id = '2001')
-- No cast in the condition -> index is used

What should a senior engineer know?

  • Implicit conversion bugs often hide in production for months until a non-numeric value is inserted or table growth makes the scan slow enough to notice.
  • ORMs can generate implicit conversions if the entity field type does not match the column type.
  • Enable SQL logging in development to catch mismatched parameter types early.
  • Always test new queries with EXPLAIN ANALYZE on production-size data.

What are common interview follow-up questions?

  • What is a sargable predicate and how does implicit conversion make it non-sargable?
  • How do type-precedence rules differ between PostgreSQL, MySQL, and SQL Server?
  • Why should you cast the literal instead of the column?
  • How can you detect implicit conversions in EXPLAIN plans?

Visual Overview

DIAGRAM
┌─────────────────────────────────────────────────┐
│   Implicit Type Conversion and Index Defeat      │
├─────────────────────────────────────────────────┤
│                                                 │
│  Query: WHERE account_id = 2001                 │
│  Column type: VARCHAR(20)                       │
│  Literal type: INTEGER                          │
│                                                 │
│  Type precedence: INTEGER > VARCHAR             │
│                                                 │
│  Database applies:                              │
│  CAST(account_id AS INTEGER) = 2001             │
│       ▲                                         │
│       │ function on the column!                  │
│       │                                         │
│  B-tree sorted by VARCHAR values:               │
│  '1001' | '2001' | 'SAV2001' | 'SAV3001'       │
│  Cannot seek by INTEGER value ──► Seq Scan      │
│                                                 │
│  Fix: WHERE account_id = '2001'                 │
│  No cast needed ──► Index Scan                  │
│                                                 │
│  JDBC rule:                                     │
│  VARCHAR column ──► ps.setString()              │
│  BIGINT column  ──► ps.setLong()                │
│  TIMESTAMP col  ──► ps.setTimestamp()           │
└─────────────────────────────────────────────────┘

Key Takeaway

CAST with the AS type form, and the vendor-specific CONVERT, explicitly change a value's data type, and you use them on literals and bound parameters when you need a specific type. Implicit type conversion is when the database silently coerces one side of a comparison to match the other based on type-precedence rules, and the danger is that to make the types line up the engine often applies the conversion to the indexed column rather than to the literal. Once a function wraps the indexed column, the B-tree can no longer be traversed, so the query degrades to a full scan, and it can also throw at runtime or match the wrong rows.

In the National Bank of India lookup service, account numbers are stored as text like SAV2001, and a developer passed the id as an integer, which made the database implicitly convert every stored account number to a number – skipping the primary-key index and even failing to parse the alphabetic prefix. The fix was simply to compare text to text by passing a string literal and binding the JDBC parameter as a string with setString, which restored the index seek. Rajesh also made a rule to only ever cast the literal, never the column, so a timestamp filter casts the string literal to a timestamp rather than casting the column to text.

The habit Pankaj enforces is that every new query is tested with EXPLAIN ANALYZE on production-size data to catch implicit conversions early. Implicit conversion bugs often hide for months until a non-numeric value is inserted or data growth makes the scan slow enough to notice, and by then the damage is done.

Remember one sentence: match the literal's type to the column's type – an implicit conversion on the column silently kills the index.

---

Share
Use ← → arrow keys to navigate