Skip to content

Commit a07ed50

Browse files
committed
Make sure we're using indexes during rollbacks
Fixes #2083 The `queryMinRefId` query uses ```sql SELECT id FROM <table> WHERE <field> >= $1 ORDER BY id ASC LIMIT 1. ``` The planner sometimes picks a bad plan: ```sql Index Scan using tx_pkey on tx Filter: (block_id >= $1) ``` the filter is not Index Cond, so this ends up in a sequential scan. The index refers to the primary key and is only used for sorting. Instead we use a simpler query without ORDER BY: SELECT id FROM <table> WHERE <field> >= $1 LIMIT 10000 This forces the planner to use the field's index. The results are fetched and the minimum is found in Haskell. Near the tip this returns only a handful of rows. If there are more than 10000 matching rows (large rollback), we fall back to the original ORDER BY id ASC LIMIT 1 query.
1 parent 3c8724b commit a07ed50

1 file changed

Lines changed: 59 additions & 8 deletions

File tree

cardano-db/src/Cardano/Db/Statement/MinIds.hs

Lines changed: 59 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,9 @@ import Cardano.Db.Types (DbM)
3737
-- RAW INT64 QUERIES (for rollback operations)
3838
---------------------------------------------------------------------------
3939

40-
-- | Find the minimum ID in a table - returns raw Int64
40+
-- | Find the minimum ID in a table. Fetches all matching IDs (up to 10000)
41+
-- and finds the minimum in Haskell to avoid bad query plans with ORDER BY/MIN.
42+
-- Falls back to the original ORDER BY query if there are too many results.
4143
queryMinRefIdStmt ::
4244
forall a b.
4345
DbInfo a =>
@@ -47,8 +49,29 @@ queryMinRefIdStmt ::
4749
HsqlE.Params b ->
4850
-- | Raw ID decoder (Int64)
4951
HsqlD.Row Int64 ->
52+
HsqlStmt.Statement b [Int64]
53+
queryMinRefIdStmt fieldName encoder _idDecoder =
54+
HsqlStmt.Statement sql encoder decoder True
55+
where
56+
validCol = validateColumn @a fieldName
57+
sql =
58+
TextEnc.encodeUtf8 $
59+
Text.concat
60+
[ "SELECT id"
61+
, " FROM " <> tableName (Proxy @a)
62+
, " WHERE " <> validCol <> " >= $1"
63+
, " LIMIT 10000"
64+
]
65+
decoder = HsqlD.rowList (HsqlD.column (HsqlD.nonNullable HsqlD.int8))
66+
67+
queryMinRefIdFallbackStmt ::
68+
forall a b.
69+
DbInfo a =>
70+
Text.Text ->
71+
HsqlE.Params b ->
72+
HsqlD.Row Int64 ->
5073
HsqlStmt.Statement b (Maybe Int64)
51-
queryMinRefIdStmt fieldName encoder idDecoder =
74+
queryMinRefIdFallbackStmt fieldName encoder idDecoder =
5275
HsqlStmt.Statement sql encoder decoder True
5376
where
5477
validCol = validateColumn @a fieldName
@@ -73,8 +96,11 @@ queryMinRefId ::
7396
-- | Parameter encoder
7497
HsqlE.Params b ->
7598
DbM (Maybe Int64)
76-
queryMinRefId fieldName value encoder =
77-
runSession mkDbCallStack $ HsqlSes.statement value (queryMinRefIdStmt @a fieldName encoder rawInt64Decoder)
99+
queryMinRefId fieldName value encoder = do
100+
ids <- runSession mkDbCallStack $ HsqlSes.statement value (queryMinRefIdStmt @a fieldName encoder rawInt64Decoder)
101+
if length ids >= 10000
102+
then runSession mkDbCallStack $ HsqlSes.statement value (queryMinRefIdFallbackStmt @a fieldName encoder rawInt64Decoder)
103+
else pure $ if null ids then Nothing else Just (minimum ids)
78104
where
79105
rawInt64Decoder = HsqlD.column (HsqlD.nonNullable HsqlD.int8)
80106

@@ -91,12 +117,33 @@ queryMinRefIdNullableStmt ::
91117
HsqlE.Params b ->
92118
-- | Raw ID decoder (Int64)
93119
HsqlD.Row Int64 ->
120+
HsqlStmt.Statement b [Int64]
121+
queryMinRefIdNullableStmt fieldName encoder _idDecoder =
122+
HsqlStmt.Statement sql encoder decoder True
123+
where
124+
validCol = validateColumn @a fieldName
125+
sql =
126+
TextEnc.encodeUtf8 $
127+
Text.concat
128+
[ "SELECT id"
129+
, " FROM " <> tableName (Proxy @a)
130+
, " WHERE " <> validCol <> " IS NOT NULL"
131+
, " AND " <> validCol <> " >= $1"
132+
, " LIMIT 10000"
133+
]
134+
decoder = HsqlD.rowList (HsqlD.column (HsqlD.nonNullable HsqlD.int8))
135+
136+
queryMinRefIdNullableFallbackStmt ::
137+
forall a b.
138+
DbInfo a =>
139+
Text.Text ->
140+
HsqlE.Params b ->
141+
HsqlD.Row Int64 ->
94142
HsqlStmt.Statement b (Maybe Int64)
95-
queryMinRefIdNullableStmt fieldName encoder idDecoder =
143+
queryMinRefIdNullableFallbackStmt fieldName encoder idDecoder =
96144
HsqlStmt.Statement sql encoder decoder True
97145
where
98146
validCol = validateColumn @a fieldName
99-
decoder = HsqlD.rowMaybe idDecoder
100147
sql =
101148
TextEnc.encodeUtf8 $
102149
Text.concat
@@ -107,6 +154,7 @@ queryMinRefIdNullableStmt fieldName encoder idDecoder =
107154
, " ORDER BY id ASC"
108155
, " LIMIT 1"
109156
]
157+
decoder = HsqlD.rowMaybe idDecoder
110158

111159
queryMinRefIdNullable ::
112160
forall a b.
@@ -118,8 +166,11 @@ queryMinRefIdNullable ::
118166
-- | Parameter encoder
119167
HsqlE.Params b ->
120168
DbM (Maybe Int64)
121-
queryMinRefIdNullable fieldName value encoder =
122-
runSession mkDbCallStack $ HsqlSes.statement value (queryMinRefIdNullableStmt @a fieldName encoder rawInt64Decoder)
169+
queryMinRefIdNullable fieldName value encoder = do
170+
ids <- runSession mkDbCallStack $ HsqlSes.statement value (queryMinRefIdNullableStmt @a fieldName encoder rawInt64Decoder)
171+
if length ids >= 10000
172+
then runSession mkDbCallStack $ HsqlSes.statement value (queryMinRefIdNullableFallbackStmt @a fieldName encoder rawInt64Decoder)
173+
else pure $ if null ids then Nothing else Just (minimum ids)
123174
where
124175
rawInt64Decoder = HsqlD.column (HsqlD.nonNullable HsqlD.int8)
125176

0 commit comments

Comments
 (0)