Skip to content

mssql get_indexes() does not preserve column order #12894

@CaselIT

Description

@CaselIT

Discussed in #12892

Originally posted by e2ne0 October 1, 2025
Hi everyone,

While using alembic, I noticed that indexes which were already correctly created kept showing up as changes and got re-created every time I ran autogenerate.
Looking into it, I found that when reflecting indexes from mssql, the columns are returned in the order of sys.columns.column_id rather than the actual order defined in the index sys.index_columns.key_ordinal.

I'm posting this in the SQLAlchemy repo because the issue comes from how SQLAlchemy reflects indexes, not from Alembic itself.

Example

Database index definition:

CREATE NONCLUSTERED INDEX [Index_Example]
    ON [Table1]([Column3] DESC, [Column1] ASC, [Column2] DESC);

What get_indexes() returns:

{
    'name': 'Index_Example',
    'column_names': ['Column1', 'Column2', 'Column3']
    # sorted by column_id
}

What it should return to make alembic autogenerate correctly:

{
    'name': 'Index_Example', 
    'column_names': ['Column3', 'Column1', 'Column2']
    # sorted by key_ordinal
}

Reproducible test case

from sqlalchemy import create_engine, inspect, MetaData, Table, Column, Integer, Index

engine = create_engine('mssql+pyodbc://...')

metadata = MetaData()
test_table = Table(
    'Table1',
    metadata,
    Column('Column1', Integer, primary_key=True),
    Column('Column2', Integer),
    Column('Column3', Integer),
)

# create test index
idx = Index(
    'Index_Example',
    test_table.c.Column3.desc(),
    test_table.c.Column1.asc(),
    test_table.c.Column2.desc(),
)

metadata.create_all(engine)

inspector = inspect(engine)
indexes = inspector.get_indexes('Index_Example')
print(indexes[0]['column_names'])
# output shows wrong order

metadata.drop_all(engine)

MSSQL Verification

Running this query shows the correct order:

SELECT 
    COL_NAME(ic.object_id, ic.column_id) AS ColumnName,
    ic.key_ordinal
FROM sys.indexes i
INNER JOIN sys.index_columns ic ON i.object_id = ic.object_id AND i.index_id = ic.index_id
WHERE i.object_id = OBJECT_ID('Table1') AND i.name = 'Index_Example'
ORDER BY ic.key_ordinal;  -- This ORDER BY is missing in get_indexes()

Root cause

In sqlalchemy/dialects/mssql/base.py, the get_indexes() method's second query doesn't order by key_ordinal:

# Current query
rp = connection.execution_options(future_result=True).execute(
    sql.text(
        """
select
    ind_col.index_id,
    col.name,
    ind_col.is_included_column
from
    sys.columns as col
join sys.tables as tab on tab.object_id = col.object_id
join sys.index_columns as ind_col on 
    ind_col.column_id = col.column_id
    and ind_col.object_id = tab.object_id
join sys.schemas as sch on sch.schema_id = tab.schema_id
where
    tab.name = :tabname
    and sch.name = :schname
        """
    )
    # missing: ORDER BY ind_col.index_id, ind_col.key_ordinal
)

Proposed fix

Add ORDER BY ind_col.index_id, ind_col.key_ordinal to ensure columns are returned in the correct order.

My questions

  1. Is this considered a bug?
  2. If yes, would you accept a PR with the above fix?
  3. Any known backward compatibility concerns?

Environment

  • SQLAlchemy: 2.0.x
  • db: Microsoft SQL Server 2016+
  • This affects Alembic autogenerate significantly

Metadata

Metadata

Assignees

No one assigned

    Labels

    PRs (with tests!) welcomea fix or feature which is appropriate to be implemented by volunteersSQL ServerMicrosoft SQL Server, e.g. mssqlbugSomething isn't workingreflectionreflection of tables, columns, constraints, defaults, sequences, views, everything else

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions