Skip to content

Commit 690d4b7

Browse files
committed
Extend ORDER BY elimination to multi-column leading-prefix sorts and equality-prefix-continued-by-order-columns, reusing one composite ordered view via a ragged-arity tuple comparer.
1 parent 3329bc0 commit 690d4b7

5 files changed

Lines changed: 506 additions & 105 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 **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.
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 single or multi-column leading-prefix sort (all NOT NULL, all one direction), including an equality-prefix continued by the order columns (`WHERE a = @x ORDER BY b`). 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: 163 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1094,6 +1094,125 @@ insert t values (1, 30), (2, 10), (3, 20)
10941094
AreEqual("10,20,30", Seq(rows));
10951095
}
10961096

1097+
[TestMethod]
1098+
public void EqualityPrefixThenOrderBySuffix_Eliminates()
1099+
{
1100+
// WHERE a = 1 ORDER BY b on PK (a, b): the seek positions on a = 1 and
1101+
// the trailing key column emerges already ordered — no buffered sort, and
1102+
// only the a = 1 group is touched (a = 2 rows are interleaved in heap
1103+
// order to prove the scan doesn't drag them in).
1104+
var (trace, rows) = Run("""
1105+
create table t (a int not null, b int not null, primary key (a, b));
1106+
insert t values (2, 5), (1, 30), (1, 10), (2, 1), (1, 20)
1107+
""", "select b from t where a = 1 order by b");
1108+
Contains("OrderedScan(t)", trace);
1109+
AreEqual("10,20,30", Seq(rows));
1110+
}
1111+
1112+
[TestMethod]
1113+
public void EqualityPrefixThenOrderBySuffixDescending_Eliminates()
1114+
{
1115+
var (trace, rows) = Run("""
1116+
create table t (a int not null, b int not null, primary key (a, b));
1117+
insert t values (2, 5), (1, 30), (1, 10), (1, 20)
1118+
""", "select b from t where a = 1 order by b desc");
1119+
Contains("OrderedScan(t)", trace);
1120+
AreEqual("30,20,10", Seq(rows));
1121+
}
1122+
1123+
[TestMethod]
1124+
public void EqualityPrefixWithRedundantOrderColumn_Eliminates()
1125+
{
1126+
// ORDER BY a, b with a pinned to a constant ≡ ORDER BY b: a is stripped as
1127+
// a constant, leaving b as the effective order column.
1128+
var (trace, rows) = Run("""
1129+
create table t (a int not null, b int not null, primary key (a, b));
1130+
insert t values (1, 30), (1, 10), (2, 99), (1, 20)
1131+
""", "select b from t where a = 1 order by a, b");
1132+
Contains("OrderedScan(t)", trace);
1133+
AreEqual("10,20,30", Seq(rows));
1134+
}
1135+
1136+
[TestMethod]
1137+
public void EqualityPrefixAndRangeOnOrderColumn_EliminatesAndNarrows()
1138+
{
1139+
// WHERE a = 1 AND b >= 20 AND b < 40 ORDER BY b: the pinned a = 1 prefix
1140+
// plus a folded range on b stream the matching slice already ordered.
1141+
var (trace, rows) = Run("""
1142+
create table t (a int not null, b int not null, primary key (a, b));
1143+
insert t values (1, 10), (1, 20), (1, 30), (1, 40), (2, 25)
1144+
""", "select b from t where a = 1 and b >= 20 and b < 40 order by b");
1145+
Contains("OrderedScan(t)", trace);
1146+
AreEqual("20,30", Seq(rows));
1147+
}
1148+
1149+
[TestMethod]
1150+
public void MultiColumnOrderByDescending_Eliminates_Reversed()
1151+
{
1152+
// ORDER BY a DESC, b DESC reverses the ascending composite view.
1153+
var (trace, rows) = Run("""
1154+
create table t (a int not null, b int not null, primary key (a, b));
1155+
insert t values (2, 1), (1, 2), (1, 1), (2, 2)
1156+
""", "select 10 * a + b from t order by a desc, b desc");
1157+
Contains("OrderedScan(t)", trace);
1158+
AreEqual("22,21,12,11", Seq(rows));
1159+
}
1160+
1161+
[TestMethod]
1162+
public void EqualityPrefixNullProbe_ReturnsEmpty()
1163+
{
1164+
// a = NULL matches nothing; the ordered seek resolves to an empty stream.
1165+
// (a is a nullable leading index column, not a PK column.)
1166+
var (_, rows) = Run("""
1167+
create table t (a int null, b int not null);
1168+
create index ix on t (a, b);
1169+
insert t values (1, 10), (1, 20)
1170+
""", "select b from t where a = null order by b");
1171+
IsEmpty(rows);
1172+
}
1173+
1174+
[TestMethod]
1175+
public void EqualityPrefixOrderedScanCorrectAfterMutations()
1176+
{
1177+
// The composite ordered view feeding an equality-prefix scan must stay
1178+
// current under the incremental journal: warm, mutate, then a later
1179+
// WHERE a = 1 ORDER BY b reflects the changes in order.
1180+
var c = new Simulation().CreateDbConnection();
1181+
c.Open();
1182+
Exec(c, "create table t (a int not null, b int not null, primary key (a, b))");
1183+
Exec(c, "insert t values (1, 30), (1, 10), (2, 5), (1, 20)");
1184+
_ = ReadRows(c, "select b from t where a = 1 order by b");
1185+
Exec(c, "insert t values (1, 5), (2, 1)");
1186+
Exec(c, "delete from t where a = 1 and b = 20");
1187+
Exec(c, "update t set b = 15 where a = 1 and b = 30");
1188+
var rows = ReadRows(c, "select b from t where a = 1 order by b");
1189+
AreEqual("5,10,15", Seq(rows));
1190+
}
1191+
1192+
[TestMethod]
1193+
public void MultiColumnOrderByDifferential_MatchesIndependentSort()
1194+
{
1195+
// Cross-check an eliminated composite ORDER BY against a C# sort over a
1196+
// no-WHERE read (which doesn't eliminate). 1000 * a + b fully encodes the
1197+
// (a, b) order (b < 1000), so sorting the integer equals ORDER BY a, b.
1198+
var c = new Simulation().CreateDbConnection();
1199+
c.Open();
1200+
Exec(c, "create table t (id int not null primary key, a int not null, b int not null)");
1201+
Exec(c, "create index ix on t (a, b)");
1202+
Exec(c, "insert t (id, a, b) select value, (value * 7919) % 17, (value * 104729) % 97 from generate_series(1, 300)");
1203+
1204+
var expected = new List<int>();
1205+
foreach (var o in ReadRows(c, "select 1000 * a + b from t"))
1206+
expected.Add(Convert.ToInt32(o));
1207+
expected.Sort();
1208+
1209+
var actual = new List<int>();
1210+
foreach (var o in ReadRows(c, "select 1000 * a + b from t order by a, b"))
1211+
actual.Add(Convert.ToInt32(o));
1212+
1213+
AreEqual(string.Join(",", expected), string.Join(",", actual));
1214+
}
1215+
10971216
// ---- declines (keeps the buffered sort), still correct order ----
10981217

10991218
[TestMethod]
@@ -1119,13 +1238,15 @@ public void OrderByExpression_Declines()
11191238
}
11201239

11211240
[TestMethod]
1122-
public void MultiColumnOrderBy_Declines()
1241+
public void MultiColumnOrderBy_OnCompositeKey_Eliminates()
11231242
{
1243+
// ORDER BY a, b matches the (a, b) PK leading prefix, both NOT NULL, so
1244+
// the composite ordered view streams in key order — no buffered sort.
11241245
var (trace, rows) = Run("""
11251246
create table t (a int not null, b int not null, primary key (a, b));
11261247
insert t values (2, 1), (1, 2), (1, 1)
11271248
""", "select a from t order by a, b");
1128-
DoesNotContain("OrderedScan(t)", trace);
1249+
Contains("OrderedScan(t)", trace);
11291250
AreEqual("1,1,2", Seq(rows));
11301251
}
11311252

@@ -1137,6 +1258,46 @@ public void DistinctOrderBy_Declines()
11371258
AreEqual("1,2,3,4,5", Seq(rows));
11381259
}
11391260

1261+
[TestMethod]
1262+
public void MixedDirectionMultiColumnOrderBy_Declines()
1263+
{
1264+
// a ASC, b DESC can't be served by the single ascending-by-value
1265+
// composite view (only all-ASC or all-DESC), so elimination declines.
1266+
var (trace, rows) = Run("""
1267+
create table t (a int not null, b int not null, primary key (a, b));
1268+
insert t values (1, 2), (1, 1), (2, 1)
1269+
""", "select 10 * a + b from t order by a asc, b desc");
1270+
DoesNotContain("OrderedScan(t)", trace);
1271+
AreEqual("12,11,21", Seq(rows));
1272+
}
1273+
1274+
[TestMethod]
1275+
public void MultiColumnOrderByNullableSuffix_Declines()
1276+
{
1277+
// b is nullable, so its NULL-key rows aren't in the composite view —
1278+
// elimination declines and the sort puts the NULL first.
1279+
var (trace, rows) = Run("""
1280+
create table t (a int not null, b int null);
1281+
create index ix on t (a, b);
1282+
insert t values (1, 30), (1, null), (1, 10)
1283+
""", "select b from t where a = 1 order by b");
1284+
DoesNotContain("OrderedScan(t)", trace);
1285+
AreEqual(",10,30", Seq(rows));
1286+
}
1287+
1288+
[TestMethod]
1289+
public void OrderByColumnsNotMatchingKeyOrder_Declines()
1290+
{
1291+
// ORDER BY b, a doesn't match the (a, b) key order, so no ordered view
1292+
// serves it.
1293+
var (trace, rows) = Run("""
1294+
create table t (a int not null, b int not null, primary key (a, b));
1295+
insert t values (1, 2), (2, 1), (1, 1)
1296+
""", "select 10 * a + b from t order by b, a");
1297+
DoesNotContain("OrderedScan(t)", trace);
1298+
AreEqual("11,21,12", Seq(rows));
1299+
}
1300+
11401301
[TestMethod]
11411302
public void EqualityOnOtherIndexedColumn_DeclinesOrderElimination_PrefersSeek()
11421303
{

0 commit comments

Comments
 (0)