2026, Jan 01 09:00
Fixing SQLite ORDER BY When Numeric IDs Are Stored as Text: Use CAST for Correct Sorting (with SQLAlchemy)
SQLite can sort numeric IDs lexically when stored as TEXT, returning wrong rows. Learn to enforce numeric ORDER BY with CAST and SQLAlchemy for correct results
When SQLite stores numeric identifiers as text, ORDER BY stops being numeric and becomes lexical. The result is counterintuitive: an id like 100567 sorts ahead of 15470, because strings are compared alphabetically, not by value. If your selection logic depends on the smallest numeric id, you will fetch the wrong row.
Repro: the model and the query
Consider a minimal ORM mapping and a query that selects a city by name and returns the first row by id. The logic looks correct at a glance.
from sqlalchemy.orm import DeclarativeBase, Mapped, mapped_column
from sqlalchemy import Integer, String, select
class BaseModel(DeclarativeBase):
pass
class TownRecord(BaseModel):
__tablename__ = "city"
uid: Mapped[int] = mapped_column(Integer, primary_key=True, index=True)
name_label: Mapped[str] = mapped_column(String, index=True)
def build_stmt(input_name: str):
stmt = (
select(TownRecord)
.where(TownRecord.name_label == input_name.upper())
.order_by(TownRecord.uid)
.limit(1)
)
return stmtWith data like the following, the query may return the row with the larger textual id instead of the smaller numeric one:
15470 Paris
100567 ParisWhat actually goes wrong
In SQLite, if the id column is stored as TEXT rather than INTEGER, ORDER BY applies string comparison rules. That means '100567' sorts before '15470' because the first character '1' ties, the second character '0' is less than '5', and so on. The declared type in your ORM model does not change how SQLite sorts if the underlying storage ends up being text.
Fix: force numeric ordering
The most direct, database-level fix is to cast the sorting expression to an integer, ensuring numeric ordering regardless of how the data is stored.
SELECT id, city_name
FROM city
WHERE city_name = 'Paris'
ORDER BY CAST(id AS INTEGER)
LIMIT 1;The same idea applies cleanly with SQLAlchemy by wrapping the ORDER BY expression in a cast. The selection logic remains identical; only the ordering expression changes.
from sqlalchemy import cast, Integer, select
def build_stmt_fixed(input_name: str):
stmt = (
select(TownRecord)
.where(TownRecord.name_label == input_name.upper())
.order_by(cast(TownRecord.uid, Integer))
.limit(1)
)
return stmtWhy this detail matters
Sorting semantics control which row you pick when you limit a result set. If your application assumes numeric ordering but the database applies lexical ordering, subtle bugs appear in production: wrong entities fetched, inconsistent pagination, and hard-to-reproduce behavior. The mismatch is especially tricky because the ORM type annotation can suggest one thing, while the actual storage type in SQLite drives the real ordering behavior.
Takeaways
When ordering by identifiers or any numeric-looking field in SQLite, ensure the database treats the sort key as a number. Casting in ORDER BY is a precise fix when the stored values are text. If you see unexpected orderings where '100567' precedes '15470', verify the column’s storage and make the ordering explicit by casting to INTEGER.