Skip to content

Commit 5a856db

Browse files
MySQL: Improve parallel snapshot (#3002)
1. Fix comparison of previous partition with current partition. 2. Use a different query to obtain logical partitions for MySQL - [x] Adapt query to be able to handle timestamps
1 parent 3a7d4f8 commit 5a856db

4 files changed

Lines changed: 465 additions & 136 deletions

File tree

flow/connectors/mysql/qrep.go

Lines changed: 124 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,28 @@ import (
2020
"github.com/PeerDB-io/peerdb/flow/shared/types"
2121
)
2222

23+
func (c *MySqlConnector) GetDataTypeOfWatermarkColumn(
24+
ctx context.Context,
25+
watermarkTableName string,
26+
watermarkColumn string,
27+
) (types.QValueKind, error) {
28+
if watermarkColumn == "" {
29+
return "", errors.New("watermark column is not specified in the config")
30+
}
31+
32+
query := fmt.Sprintf("SELECT `%s` FROM %s LIMIT 0", watermarkColumn, watermarkTableName)
33+
rs, err := c.Execute(ctx, query)
34+
if err != nil {
35+
return "", fmt.Errorf("failed to execute query for watermark column type: %w", err)
36+
}
37+
38+
if len(rs.Fields) == 0 {
39+
return "", errors.New("no fields returned from select query: " + query)
40+
}
41+
42+
return qkindFromMysql(rs.Fields[0])
43+
}
44+
2345
func (c *MySqlConnector) GetQRepPartitions(
2446
ctx context.Context,
2547
config *protos.QRepConfig,
@@ -46,7 +68,7 @@ func (c *MySqlConnector) GetQRepPartitions(
4668

4769
whereClause := ""
4870
if last != nil && last.Range != nil {
49-
whereClause = fmt.Sprintf("WHERE %s > $1", quotedWatermarkColumn)
71+
whereClause = fmt.Sprintf("WHERE %s > ?", quotedWatermarkColumn)
5072
}
5173
parsedWatermarkTable, err := utils.ParseSchemaTable(config.WatermarkTable)
5274
if err != nil {
@@ -64,7 +86,7 @@ func (c *MySqlConnector) GetQRepPartitions(
6486
case *protos.PartitionRange_UintRange:
6587
minVal = lastRange.UintRange.End
6688
case *protos.PartitionRange_TimestampRange:
67-
minVal = lastRange.TimestampRange.End.AsTime()
89+
minVal = lastRange.TimestampRange.End.AsTime().String()
6890
}
6991
c.logger.Info(fmt.Sprintf("count query: %s - minVal: %v", countQuery, minVal))
7092

@@ -99,62 +121,115 @@ func (c *MySqlConnector) GetQRepPartitions(
99121
if totalRows%numRowsPerPartition != 0 {
100122
numPartitions++
101123
}
124+
125+
watermarkQKind, err := c.GetDataTypeOfWatermarkColumn(ctx, parsedWatermarkTable.MySQL(), config.WatermarkColumn)
126+
if err != nil {
127+
return nil, fmt.Errorf("failed to get data type of watermark column %s: %w", config.WatermarkColumn, err)
128+
}
129+
102130
c.logger.Info(fmt.Sprintf("total rows: %d, num partitions: %d, num rows per partition: %d",
103131
totalRows, numPartitions, numRowsPerPartition))
104132
var rs *mysql.Result
105-
if minVal != nil {
106-
// Query to get partitions using window functions
107-
partitionsQuery := fmt.Sprintf(
108-
`SELECT bucket, MIN(%[2]s) AS start, MAX(%[2]s) AS end
109-
FROM (
110-
SELECT NTILE(%[1]d) OVER (ORDER BY %[2]s) AS bucket, %[2]s
111-
FROM %[3]s WHERE %[2]s > $1
112-
) AS subquery
133+
134+
switch watermarkQKind {
135+
case types.QValueKindInt8, types.QValueKindInt16, types.QValueKindInt32, types.QValueKindInt64,
136+
types.QValueKindUInt8, types.QValueKindUInt16, types.QValueKindUInt32, types.QValueKindUInt64:
137+
if minVal != nil {
138+
partitionsQuery := fmt.Sprintf(
139+
`WITH stats AS (
140+
SELECT MIN(%[2]s) AS min_watermark,
141+
1.0 * (MAX(%[2]s) - MIN(%[2]s)) / (%[1]d) AS range_size
142+
FROM %[3]s WHERE %[2]s > ?
143+
)
144+
SELECT FLOOR((w.%[2]s - s.min_watermark) / s.range_size) AS bucket,
145+
MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
146+
FROM %[3]s AS w
147+
CROSS JOIN stats AS s
148+
WHERE w.%[2]s > ?
113149
GROUP BY bucket
114-
ORDER BY start`,
115-
numPartitions,
116-
quotedWatermarkColumn,
117-
parsedWatermarkTable.MySQL(),
118-
)
119-
c.logger.Info("partitions query", slog.String("query", partitionsQuery), slog.Any("minVal", minVal))
120-
rs, err = c.Execute(ctx, partitionsQuery, minVal)
121-
} else {
122-
partitionsQuery := fmt.Sprintf(
123-
`SELECT bucket, MIN(%[2]s) AS start, MAX(%[2]s) AS end
124-
FROM (
125-
SELECT NTILE(%[1]d) OVER (ORDER BY %[2]s) AS bucket, %[2]s FROM %[3]s
126-
) AS subquery
150+
ORDER BY start;`,
151+
numPartitions,
152+
quotedWatermarkColumn,
153+
parsedWatermarkTable.MySQL(),
154+
)
155+
c.logger.Info("partitions query", slog.String("query", partitionsQuery), slog.Any("minVal", minVal))
156+
rs, err = c.Execute(ctx, partitionsQuery, minVal, minVal)
157+
} else {
158+
partitionsQuery := fmt.Sprintf(
159+
`WITH stats AS (
160+
SELECT MIN(%[2]s) AS min_watermark,
161+
1.0 * (MAX(%[2]s) - MIN(%[2]s)) / (%[1]d) AS range_size
162+
FROM %[3]s
163+
)
164+
SELECT FLOOR((w.%[2]s - s.min_watermark) / s.range_size) AS bucket,
165+
MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
166+
FROM %[3]s AS w
167+
CROSS JOIN stats AS s
127168
GROUP BY bucket
128-
ORDER BY start`,
129-
numPartitions,
130-
quotedWatermarkColumn,
131-
parsedWatermarkTable.MySQL(),
132-
)
133-
c.logger.Info("partitions query", slog.String("query", partitionsQuery))
134-
rs, err = c.Execute(ctx, partitionsQuery)
135-
}
136-
if err != nil {
137-
return nil, fmt.Errorf("failed to query for partitions: %w", err)
169+
ORDER BY start;`,
170+
numPartitions,
171+
quotedWatermarkColumn,
172+
parsedWatermarkTable.MySQL(),
173+
)
174+
c.logger.Info("partitions query", slog.String("query", partitionsQuery))
175+
rs, err = c.Execute(ctx, partitionsQuery)
176+
}
177+
if err != nil {
178+
return nil, fmt.Errorf("failed to query for partitions: %w", err)
179+
}
180+
case types.QValueKindTimestamp, types.QValueKindTimestampTZ:
181+
if minVal != nil {
182+
partitionsQuery := fmt.Sprintf(
183+
`WITH stats AS (
184+
SELECT MIN(%[2]s) AS min_watermark,
185+
1.0 * (TIMESTAMPDIFF(MICROSECOND, MAX(%[2]s), MIN(%[2]s)) / (%[1]d)) AS range_size
186+
FROM %[3]s WHERE %[2]s > ?
187+
)
188+
SELECT FLOOR(TIMESTAMPDIFF(MICROSECOND, w.%[2]s, s.min_watermark) / s.range_size) AS bucket,
189+
MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
190+
FROM %[3]s AS w
191+
CROSS JOIN stats AS s
192+
WHERE w.%[2]s > ?
193+
GROUP BY bucket
194+
ORDER BY start;`,
195+
numPartitions,
196+
quotedWatermarkColumn,
197+
parsedWatermarkTable.MySQL(),
198+
)
199+
c.logger.Info("partitions query", slog.String("query", partitionsQuery), slog.Any("minVal", minVal))
200+
rs, err = c.Execute(ctx, partitionsQuery, minVal)
201+
} else {
202+
partitionsQuery := fmt.Sprintf(
203+
`WITH stats AS (
204+
SELECT MIN(%[2]s) AS min_watermark,
205+
1.0 * (TIMESTAMPDIFF(MICROSECOND, MAX(%[2]s), MIN(%[2]s)) / (%[1]d)) AS range_size
206+
FROM %[3]s
207+
)
208+
SELECT FLOOR(TIMESTAMPDIFF(MICROSECOND, w.%[2]s, s.min_watermark) / s.range_size) AS bucket,
209+
MIN(w.%[2]s) AS start, MAX(w.%[2]s) AS end
210+
FROM %[3]s AS w
211+
CROSS JOIN stats AS s
212+
GROUP BY bucket
213+
ORDER BY start;`,
214+
numPartitions,
215+
quotedWatermarkColumn,
216+
parsedWatermarkTable.MySQL(),
217+
)
218+
c.logger.Info("partitions query", slog.String("query", partitionsQuery))
219+
rs, err = c.Execute(ctx, partitionsQuery)
220+
}
221+
if err != nil {
222+
return nil, fmt.Errorf("failed to query for partitions: %w", err)
223+
}
138224
}
139225

140-
qk1, err := qkindFromMysql(rs.Fields[1])
141-
if err != nil {
142-
return nil, err
143-
}
144-
qk2, err := qkindFromMysql(rs.Fields[2])
145-
if err != nil {
146-
return nil, err
147-
}
148-
if qk1 != qk2 {
149-
return nil, fmt.Errorf("low/high of partition range should be same type, got low:%s, high:%s", qk1, qk2)
150-
}
151226
partitionHelper := utils.NewPartitionHelper(c.logger)
152227
for _, row := range rs.Values {
153-
val1, err := QValueFromMysqlFieldValue(qk1, row[1])
228+
val1, err := QValueFromMysqlFieldValue(watermarkQKind, row[1])
154229
if err != nil {
155230
return nil, err
156231
}
157-
val2, err := QValueFromMysqlFieldValue(qk2, row[2])
232+
val2, err := QValueFromMysqlFieldValue(watermarkQKind, row[2])
158233
if err != nil {
159234
return nil, err
160235
}
@@ -169,7 +244,7 @@ func (c *MySqlConnector) GetQRepPartitions(
169244
func (c *MySqlConnector) PullQRepRecords(
170245
ctx context.Context,
171246
config *protos.QRepConfig,
172-
last *protos.QRepPartition,
247+
partition *protos.QRepPartition,
173248
stream *model.QRecordStream,
174249
) (int64, int64, error) {
175250
tableSchema, err := c.getTableSchemaForTable(ctx, config.Env,
@@ -241,7 +316,7 @@ func (c *MySqlConnector) PullQRepRecords(
241316
return nil
242317
}
243318

244-
if last.FullTablePartition {
319+
if partition.FullTablePartition {
245320
// this is a full table partition, so just run the query
246321
if err := c.ExecuteSelectStreaming(ctx, config.Query, &rs, onRow, onResult); err != nil {
247322
return 0, 0, err
@@ -251,7 +326,7 @@ func (c *MySqlConnector) PullQRepRecords(
251326
var rangeEnd string
252327

253328
// Depending on the type of the range, convert the range into the correct type
254-
switch x := last.Range.Range.(type) {
329+
switch x := partition.Range.Range.(type) {
255330
case *protos.PartitionRange_IntRange:
256331
rangeStart = strconv.FormatInt(x.IntRange.Start, 10)
257332
rangeEnd = strconv.FormatInt(x.IntRange.End, 10)

0 commit comments

Comments
 (0)