88// by the Apache License, Version 2.0.
99
1010use mz_ore:: cast:: CastLossy ;
11+ use mz_ore:: str:: redact;
1112
1213use crate :: { KeyProber , MySqlError , QualifiedTableRef } ;
1314
14- /// When partitioning the data, how many ranges per worker should we break the
15- /// keyspace into. This helps avoid underestimates for large row counts resulting
16- /// in severe skew.
17- const TARGET_RANGES_PER_WORKER : f64 = 8.0 ;
18-
1915/// Computes up to `num_workers - 1` partition boundaries that divide the primary key space
20- /// into `num_workers` roughly even partitions. This should be run in a transaction.
16+ /// into `num_workers` roughly even partitions.
17+ /// This should be run in a repeatable read transaction against a primary key varchar/char column
18+ /// with collation that compares character by character (no contractions, expansions or ignorable
19+ /// characters).
2120pub async fn partition_table (
2221 conn : & mut mysql_async:: Conn ,
2322 table : QualifiedTableRef < ' _ > ,
@@ -38,22 +37,26 @@ pub async fn partition_table(
3837 tracing:: trace!(
3938 schema = schema_name,
4039 table = table_name,
41- ?boundaries,
40+ // The boundaries are user data, redacted outside of CI.
41+ boundaries = ?redact( & boundaries) ,
4242 "partitioned table by pk prefix"
4343 ) ;
4444 Ok ( boundaries)
4545}
4646
4747#[ derive( Debug ) ]
48- struct Range {
49- /// `None` for the beginning of the key space.
50- prefix : Option < String > ,
51- /// Exclusive end, `None` for the final open range .
48+ struct Prefix {
49+ /// Empty for the beginning of the key space.
50+ prefix : String ,
51+ /// Exclusive end, `None` for the final open prefix .
5252 end : Option < String > ,
53- /// Row estimate for the range , at least 1.
54- estimated_rows : f64 ,
55- /// Prefix length this range was split at.
53+ /// Row estimate for the prefix , at least 1.
54+ estimated_rows : u64 ,
55+ /// Length this prefix was split at.
5656 depth : usize ,
57+ /// Use the position within each parent as a surrogate sort key to maintain the sort ordering
58+ /// specified by MySQL.
59+ surrogate_sort_key : Vec < usize > ,
5760}
5861
5962async fn partition (
@@ -65,178 +68,122 @@ async fn partition(
6568 if workers <= 1 {
6669 return Ok ( Vec :: new ( ) ) ;
6770 }
68- let estimated_row_count = f64:: cast_lossy ( estimated_row_count. max ( 1 ) ) ;
69- let min_rows_per_worker = f64:: cast_lossy ( min_rows_per_worker. max ( 1 ) ) ;
70- let target_max_rows_per_range =
71- get_target_max_rows_per_range ( workers, estimated_row_count, min_rows_per_worker) ;
72- // Should be many more ranges than workers unless the overall row count of the table is quite small.
73- let ranges = split_into_ranges ( db, estimated_row_count, target_max_rows_per_range) . await ?;
74- Ok ( assign_boundaries ( & ranges, workers) )
75- }
71+ let estimated_row_count = estimated_row_count. max ( 1 ) ;
7672
77- fn get_target_max_rows_per_range (
78- workers : usize ,
79- estimated_row_count : f64 ,
80- min_rows_per_worker : f64 ,
81- ) -> f64 {
82- // Break up the key space into smaller ranges to more accurately rebuild the per-worker ranges later with less skew.
83- let estimated_rows_per_worker =
84- ( estimated_row_count / f64:: cast_lossy ( workers) ) . max ( min_rows_per_worker) ;
85- // Respect min_rows_per_worker as a lower bound for the granularity with which we attempt to break up the table.
86- // No need to add this overhead for small tables. Estimate accuracy isn't super clear for small numbers.
87- let target_rows_per_range =
88- ( estimated_rows_per_worker / TARGET_RANGES_PER_WORKER ) . max ( min_rows_per_worker) ;
73+ // Estimates vary wildly especially near the full table size (see `KeyProber::estimate_range_rows` for more details).
74+ // Estimates tend to get more useful as smaller chunks, so break up the table into at least 1/8ths before selecting partitions.
75+ // Prefixes estimate at least one row, so a target below one never
76+ // converges.
77+ let target_max_rows_per_prefix = ( f64:: cast_lossy ( estimated_row_count)
78+ / f64:: cast_lossy ( workers. max ( 8 ) ) )
79+ . max ( f64:: cast_lossy ( min_rows_per_worker) )
80+ . max ( 1.0 ) ;
8981
90- tracing:: debug!(
91- estimated_row_count,
92- workers,
93- estimated_rows_per_worker,
94- target_rows_per_range,
95- "partitioning key space"
96- ) ;
97- target_rows_per_range
82+ compute_boundaries ( db, workers, estimated_row_count, target_max_rows_per_prefix) . await
9883}
9984
100- async fn split_into_ranges (
85+ async fn compute_boundaries (
10186 db : & mut KeyProber < ' _ > ,
102- estimated_row_count : f64 ,
103- target_rows_per_bucket : f64 ,
104- ) -> Result < Vec < Range > , MySqlError > {
105- let mut ranges = vec ! [ Range {
106- prefix: None ,
87+ workers : usize ,
88+ estimated_row_count : u64 ,
89+ target_rows_per_prefix : f64 ,
90+ ) -> Result < Vec < String > , MySqlError > {
91+ // BFS of prefixes, splitting until estimates fall under the target.
92+ let mut final_prefixes: Vec < Prefix > = vec ! [ ] ;
93+ let mut pending_prefixes = vec ! [ Prefix {
94+ prefix: String :: new( ) ,
10795 end: None ,
10896 estimated_rows: estimated_row_count,
10997 depth: 0 ,
98+ surrogate_sort_key: Vec :: new( ) ,
11099 } ] ;
111- loop {
112- let mut split_any = false ;
113- let mut next: Vec < Range > = Vec :: with_capacity ( ranges. len ( ) ) ;
114- for range in ranges {
115- if range. estimated_rows > target_rows_per_bucket {
116- split_any = true ;
117- next. extend ( split_range ( db, & range, target_rows_per_bucket) . await ?) ;
118- } else {
119- next. push ( range) ;
100+
101+ while !pending_prefixes. is_empty ( ) {
102+ let mut next: Vec < Prefix > = Vec :: with_capacity ( pending_prefixes. len ( ) ) ;
103+ for prefix in pending_prefixes {
104+ for child in children_prefixes ( db, & prefix) . await ? {
105+ if f64:: cast_lossy ( child. estimated_rows ) > target_rows_per_prefix {
106+ next. push ( child) ;
107+ } else {
108+ final_prefixes. push ( child) ;
109+ }
120110 }
121111 }
122- ranges = next;
123- if !split_any {
124- break ;
125- }
112+ pending_prefixes = next;
126113 }
127- Ok ( ranges)
128- }
114+ final_prefixes. sort_unstable_by ( |a, b| a. surrogate_sort_key . cmp ( & b. surrogate_sort_key ) ) ;
129115
130- /// Accumulates `ranges` (in key order) into `workers` buckets of roughly
131- /// equal estimated rows and returns the bucket edges as boundaries .
132- fn assign_boundaries ( ranges : & [ Range ] , workers : usize ) -> Vec < String > {
133- // Emit a boundary each time the cumulative estimated rows pass the next
134- // worker's share.
135- let total : f64 = ranges . iter ( ) . map ( |r| r . estimated_rows ) . sum ( ) ;
116+ // Recompute the total after partitioning the table to get more even splits because the actual row count and the
117+ // granularly estimated row count can diverge from the original top level estimate .
118+ let total : f64 = final_prefixes
119+ . iter ( )
120+ . map ( |r| f64 :: cast_lossy ( r . estimated_rows ) )
121+ . sum ( ) ;
136122 let per_worker = total / f64:: cast_lossy ( workers) ;
137123 tracing:: debug!(
138- ranges = ranges . len( ) ,
124+ prefixes = final_prefixes . len( ) ,
139125 total_estimated_rows = total,
140126 per_worker,
141- "assigning prefix ranges to workers"
127+ "assigning prefixes to workers"
142128 ) ;
143129 let mut boundaries: Vec < String > = Vec :: with_capacity ( workers - 1 ) ;
144130 let mut rows_seen = 0.0 ;
145- for range in ranges {
131+ for prefix in & final_prefixes {
146132 if boundaries. len ( ) == workers - 1 {
147133 break ;
148134 }
149- rows_seen += range . estimated_rows ;
135+ rows_seen += f64 :: cast_lossy ( prefix . estimated_rows ) ;
150136 if rows_seen >= f64:: cast_lossy ( boundaries. len ( ) + 1 ) * per_worker {
151- // The final range 's end is None (open), it can never be a boundary.
152- if let Some ( end) = & range . end {
137+ // The final prefix 's end is None (open), it can never be a boundary.
138+ if let Some ( end) = & prefix . end {
153139 boundaries. push ( end. clone ( ) ) ;
154140 }
155141 }
156142 }
157- boundaries
143+ Ok ( boundaries)
158144}
159145
160- /// Splits `parent` at every distinct key prefix one character longer than the
161- /// prefix `parent`, i.e. "a" depth: 1 for a table with keys "a", "aa", "aaa", "ab" would be split to
162- /// "a" depth: 2 estimate 1, "aa" depth: 2 estimate 2, "ab" depth 2, estimate 1.
163- async fn split_range (
146+ /// Splits `parent` into prefixes one character longer. i.e. prefix "a", upper bound "b" in table
147+ /// with pks: ["a", "ab", "abc", "abd", "af", "bb"] will return: ["ab", "af"].
148+ ///
149+ /// Note: This will drop the key "a" on the floor. We accept this because we only lose
150+ /// at max one row per prefix we step deeper into.
151+ async fn children_prefixes (
164152 db : & mut KeyProber < ' _ > ,
165- parent : & Range ,
166- target_rows : f64 ,
167- ) -> Result < Vec < Range > , MySqlError > {
153+ parent : & Prefix ,
154+ ) -> Result < Vec < Prefix > , MySqlError > {
168155 let depth = parent. depth + 1 ;
169156 let mut children = Vec :: new ( ) ;
170157
158+ // Guaranteed to return None or a key longer than the current prefix assuming the upper
159+ // bound correctly caps keys to the current prefix and we're in a transaction where
160+ // new keys with a shorter length can't be inserted. Note that this only holds for
161+ // collations that sort character-by-character.
171162 let Some ( mut cur) = db
172- . prefix_of_first_key_in_range ( parent. prefix . as_deref ( ) , parent. end . as_deref ( ) , depth)
163+ . prefix_of_first_key_in_range ( & parent. prefix , parent. end . as_deref ( ) , depth)
173164 . await ?
174165 else {
175166 return Ok ( children) ;
176167 } ;
177- // The first child inherits the parent's start.
178- let mut start = parent. prefix . clone ( ) ;
179168
180169 loop {
181- // Small optimization to return early if the remaining rows past the current start prefix
182- // fits within the threshold we're looking for.
183- // A missing optimizer estimate reads as an empty range, which the
184- // splitting loop drops or leaves unsplit.
185- let remaining = db
186- . estimate_range_rows ( start. as_deref ( ) , parent. end . as_deref ( ) )
187- . await ?
188- . unwrap_or ( 0 ) ;
189- let remaining = f64:: cast_lossy ( remaining) . max ( 1.0 ) ;
190- if remaining <= target_rows {
191- children. push ( Range {
192- prefix : start,
193- end : parent. end . clone ( ) ,
194- estimated_rows : remaining,
195- depth,
196- } ) ;
197- return Ok ( children) ;
198- }
199- // When `cur` is an exact key shorter than `depth`, every key extending
200- // it matches `cur`, so next_prefix exhausts even though those
201- // extensions still need visiting. The first key past `cur` exposes
202- // them, so the walk can keep splitting instead of retrying the whole
203- // range at greater depths forever.
204- let next = match db
170+ let next = db
205171 . prefix_of_first_row_not_matching_prefix ( & cur, parent. end . as_deref ( ) , depth)
206- . await ?
207- {
208- Some ( next) => Some ( next) ,
209- None => {
210- db. prefix_of_first_key_in_range ( Some ( & cur) , parent. end . as_deref ( ) , depth)
211- . await ?
212- }
213- } ;
214- // A prefix equal to `cur` cannot advance the walk (only a misbehaving
215- // server produces one), stop splitting here.
216- match next. filter ( |next| next != & cur) {
217- Some ( next) => {
218- let estimated_rows = db
219- . estimate_range_rows ( start. as_deref ( ) , Some ( & next) )
220- . await ?
221- . unwrap_or ( 0 ) ;
222- children. push ( Range {
223- prefix : start. clone ( ) ,
224- end : Some ( next. clone ( ) ) ,
225- estimated_rows : f64:: cast_lossy ( estimated_rows) . max ( 1.0 ) ,
226- depth,
227- } ) ;
228- start = Some ( next. clone ( ) ) ;
229- cur = next;
230- }
231- None => {
232- children. push ( Range {
233- prefix : start,
234- end : parent. end . clone ( ) ,
235- estimated_rows : remaining,
236- depth,
237- } ) ;
238- return Ok ( children) ;
239- }
172+ . await ?;
173+ let end = next. clone ( ) . or_else ( || parent. end . clone ( ) ) ;
174+ let estimated_rows = db. estimate_range_rows ( & cur, end. as_deref ( ) ) . await ?;
175+ let mut surrogate_sort_key = parent. surrogate_sort_key . clone ( ) ;
176+ surrogate_sort_key. push ( children. len ( ) ) ;
177+ children. push ( Prefix {
178+ prefix : cur,
179+ end : end. clone ( ) ,
180+ estimated_rows : estimated_rows. max ( 1 ) ,
181+ depth,
182+ surrogate_sort_key,
183+ } ) ;
184+ match next {
185+ Some ( next) => cur = next,
186+ None => return Ok ( children) ,
240187 }
241188 }
242189}
0 commit comments