Skip to content

Commit 3329bc0

Browse files
committed
Add ORDER BY elimination for a single NOT-NULL leading-key-column sort.
1 parent 287d1e8 commit 3329bc0

6 files changed

Lines changed: 418 additions & 4 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ The `*.Tests` and `*.Tests.EFCore` suites are the authoritative behavior contrac
107107

108108
### Constraints
109109
- `CHECK`: inline single-column and table-level forms; Msg 547 per row on definitely-false predicate. Inline column-level CHECK predicates may only reference their owning column — peer references raise **Msg 8141** at CREATE TABLE (probe-confirmed verbatim wording). The walker is structural via `Expression.VisitColumnReferences` + `BooleanExpression.VisitOperandExpressions`; coverage is currently limited to common container subclasses (`Reference`, `Parenthesized`, `TwoSidedExpression`, `Cast`, `Length`) — peer refs buried in less-common containers (`DATEPART`, `SUBSTRING`, nested `CASE`, etc.) silently escape the CREATE-TABLE check and surface at INSERT instead. Table-level CHECK has no peer restriction.
110-
- `PRIMARY KEY` / `UNIQUE`: linear scan (O(N) per insert); no B-tree. Reads get equality- and range-seek acceleration via an **incrementally-maintained** per-`Heap` index: the first seek builds it from a scan and activates the heap's bounded mutation journal, and later writes append a delta the next seek replays instead of forcing a rebuild (no per-mutation warm-up; rollback / TRUNCATE invalidate it as the safety valve; the write path's locking stays untouched). Equality / IN seek the longest leading prefix; a range bound (`>`/`<`/`BETWEEN`) on a leading key column seeks an ordered view (`SortedSet`) of that column's keys. See indexes.md for leading-prefix matching, range-seek scope, MVCC/lock-plan decline rules, the journal mechanics, and the residual-WHERE correctness invariant.
110+
- `PRIMARY KEY` / `UNIQUE`: linear scan (O(N) per insert); no B-tree. Reads get **incrementally-maintained** per-`Heap` seek acceleration (no per-mutation warm-up): equality / IN on the longest leading prefix, range (`>`/`<`/`BETWEEN`) on a leading key column, and ORDER BY elimination for a single NOT-NULL leading-key-column sort. See [`indexes.md`](docs/claude/indexes.md) for the journal mechanics, decline rules, and the residual-WHERE correctness invariant.
111111
- `FOREIGN KEY`: inline / table-level / named forms; all four referential actions on `ON DELETE` / `ON UPDATE`; enforced at INSERT / UPDATE / DELETE / MERGE; full `sys.foreign_keys` / `sys.foreign_key_columns`. Referential-action, cascade-cycle, PK/UNIQUE-target, and NULL-skip rules + Msg numbers in [`foreign-keys.md`](docs/claude/foreign-keys.md).
112112

113113
### Transactions

SqlServerSimulator.Tests.Internal/IndexSeekTests.cs

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1011,6 +1011,187 @@ public void RangeAfterWarmup_ReplaysDelta_SortedViewStaysCurrent()
10111011
Contains(4, rows);
10121012
}
10131013

1014+
// ---- ORDER BY elimination: a single NOT-NULL leading-key-column sort
1015+
// streams in key order instead of buffering + sorting. Observable if wrong,
1016+
// so these assert the exact output sequence, not just the trace. The setup
1017+
// inserts in scrambled order so heap order != key order — a wrong elimination
1018+
// would surface as heap-order output. ----
1019+
1020+
private const string ScrambledT = """
1021+
create table t (id int not null primary key, val int not null);
1022+
insert t values (3, 30), (1, 10), (2, 20), (5, 50), (4, 40)
1023+
""";
1024+
1025+
private static string Seq(List<object?> rows) => string.Join(",", rows);
1026+
1027+
[TestMethod]
1028+
public void OrderByPrimaryKey_Eliminates_AscendingOrder()
1029+
{
1030+
var (trace, rows) = Run(ScrambledT, "select id from t order by id");
1031+
Contains("OrderedScan(t)", trace);
1032+
AreEqual("1,2,3,4,5", Seq(rows));
1033+
}
1034+
1035+
[TestMethod]
1036+
public void OrderByPrimaryKeyDescending_Eliminates_DescendingOrder()
1037+
{
1038+
var (trace, rows) = Run(ScrambledT, "select id from t order by id desc");
1039+
Contains("OrderedScan(t)", trace);
1040+
AreEqual("5,4,3,2,1", Seq(rows));
1041+
}
1042+
1043+
[TestMethod]
1044+
public void OrderByWithOffsetFetch_Eliminates_StreamsCorrectPage()
1045+
{
1046+
var (trace, rows) = Run(ScrambledT, "select id from t order by id offset 2 rows fetch next 2 rows only");
1047+
Contains("OrderedScan(t)", trace);
1048+
AreEqual("3,4", Seq(rows));
1049+
}
1050+
1051+
[TestMethod]
1052+
public void OrderByWithTop_Eliminates_StreamsTopN()
1053+
{
1054+
var (trace, rows) = Run(ScrambledT, "select top 2 id from t order by id");
1055+
Contains("OrderedScan(t)", trace);
1056+
AreEqual("1,2", Seq(rows));
1057+
}
1058+
1059+
[TestMethod]
1060+
public void OrderByDescWithTop_Eliminates_StreamsTopN()
1061+
{
1062+
var (trace, rows) = Run(ScrambledT, "select top 2 id from t order by id desc");
1063+
Contains("OrderedScan(t)", trace);
1064+
AreEqual("5,4", Seq(rows));
1065+
}
1066+
1067+
[TestMethod]
1068+
public void RangeAndOrderBySameColumn_EliminatesAndNarrows()
1069+
{
1070+
var (trace, rows) = Run(ScrambledT, "select id from t where id >= 2 and id < 5 order by id");
1071+
Contains("OrderedScan(t)", trace);
1072+
AreEqual("2,3,4", Seq(rows));
1073+
}
1074+
1075+
[TestMethod]
1076+
public void ResidualFilterOnOtherColumn_StillEliminatesOrder()
1077+
{
1078+
// val is not indexed, so it doesn't compete with the order column — the
1079+
// ordered scan streams by id and filters val as a residual.
1080+
var (trace, rows) = Run(ScrambledT, "select id from t where val >= 20 order by id");
1081+
Contains("OrderedScan(t)", trace);
1082+
AreEqual("2,3,4,5", Seq(rows));
1083+
}
1084+
1085+
[TestMethod]
1086+
public void OrderByOnSecondaryIndexColumn_Eliminates()
1087+
{
1088+
var (trace, rows) = Run("""
1089+
create table t (id int not null, pid int not null);
1090+
create index ix on t (pid);
1091+
insert t values (1, 30), (2, 10), (3, 20)
1092+
""", "select pid from t order by pid");
1093+
Contains("OrderedScan(t)", trace);
1094+
AreEqual("10,20,30", Seq(rows));
1095+
}
1096+
1097+
// ---- declines (keeps the buffered sort), still correct order ----
1098+
1099+
[TestMethod]
1100+
public void OrderByNullableColumn_Declines_NullsFirst()
1101+
{
1102+
// A nullable column's NULL-key rows aren't in the ordered view, so
1103+
// elimination declines — and the sort puts NULL first (ASC).
1104+
var (trace, rows) = Run("""
1105+
create table t (id int not null, n int null);
1106+
create index ix on t (n);
1107+
insert t values (1, 30), (2, null), (3, 10)
1108+
""", "select n from t order by n");
1109+
DoesNotContain("OrderedScan(t)", trace);
1110+
AreEqual(",10,30", Seq(rows));
1111+
}
1112+
1113+
[TestMethod]
1114+
public void OrderByExpression_Declines()
1115+
{
1116+
var (trace, rows) = Run(ScrambledT, "select id from t order by id + 0");
1117+
DoesNotContain("OrderedScan(t)", trace);
1118+
AreEqual("1,2,3,4,5", Seq(rows));
1119+
}
1120+
1121+
[TestMethod]
1122+
public void MultiColumnOrderBy_Declines()
1123+
{
1124+
var (trace, rows) = Run("""
1125+
create table t (a int not null, b int not null, primary key (a, b));
1126+
insert t values (2, 1), (1, 2), (1, 1)
1127+
""", "select a from t order by a, b");
1128+
DoesNotContain("OrderedScan(t)", trace);
1129+
AreEqual("1,1,2", Seq(rows));
1130+
}
1131+
1132+
[TestMethod]
1133+
public void DistinctOrderBy_Declines()
1134+
{
1135+
var (trace, rows) = Run(ScrambledT, "select distinct id from t order by id");
1136+
DoesNotContain("OrderedScan(t)", trace);
1137+
AreEqual("1,2,3,4,5", Seq(rows));
1138+
}
1139+
1140+
[TestMethod]
1141+
public void EqualityOnOtherIndexedColumn_DeclinesOrderElimination_PrefersSeek()
1142+
{
1143+
// status has its own index — the equality seek on it beats an ordered
1144+
// scan of the whole table, so order elimination declines and the result
1145+
// still comes back correctly ordered.
1146+
var (trace, rows) = Run("""
1147+
create table t (id int not null primary key, status int not null);
1148+
create index ix on t (status);
1149+
insert t values (3, 9), (1, 9), (2, 7)
1150+
""", "select id from t where status = 9 order by id");
1151+
DoesNotContain("OrderedScan(t)", trace);
1152+
Contains("Seek(t)", trace);
1153+
AreEqual("1,3", Seq(rows));
1154+
}
1155+
1156+
[TestMethod]
1157+
public void OrderByEliminationCorrectAfterWarmupAndMutations()
1158+
{
1159+
// The ordered view feeding elimination must stay current under the
1160+
// incremental journal: warm, then insert / delete / key-update, then a
1161+
// later ORDER BY must reflect them in order.
1162+
var c = new Simulation().CreateDbConnection();
1163+
c.Open();
1164+
Exec(c, ScrambledT);
1165+
_ = ReadRows(c, "select id from t order by id");
1166+
Exec(c, "insert t values (10, 100)");
1167+
Exec(c, "delete from t where id = 3");
1168+
Exec(c, "update t set id = 0 where id = 5");
1169+
var rows = ReadRows(c, "select id from t order by id");
1170+
AreEqual("0,1,2,4,10", Seq(rows));
1171+
}
1172+
1173+
[TestMethod]
1174+
public void OrderByEliminationDifferential_MatchesIndependentSort()
1175+
{
1176+
// Cross-check eliminated ORDER BY against a C# sort of a no-WHERE read
1177+
// (which doesn't eliminate) over a non-trivial scrambled dataset.
1178+
var c = new Simulation().CreateDbConnection();
1179+
c.Open();
1180+
Exec(c, "create table t (id int not null primary key, val int not null)");
1181+
Exec(c, "insert t (id, val) select (value * 7919) % 1000 + 1, value from generate_series(1, 400)");
1182+
1183+
var expected = new List<int>();
1184+
foreach (var o in ReadRows(c, "select id from t"))
1185+
expected.Add(Convert.ToInt32(o));
1186+
expected.Sort();
1187+
1188+
var actual = new List<int>();
1189+
foreach (var o in ReadRows(c, "select id from t order by id"))
1190+
actual.Add(Convert.ToInt32(o));
1191+
1192+
AreEqual(string.Join(",", expected), string.Join(",", actual));
1193+
}
1194+
10141195
[TestMethod]
10151196
public void RangeCorrectUnderManyMutations_MatchesIndependentBaseline()
10161197
{

0 commit comments

Comments
 (0)