655 karma · joined August 31, 2016
Most products do the asynchronous rewrite, especially if they're based on immutable storage. That's fine, but it should be tested to verify that it's not triggering on every delete, for example, and that it's resource-efficient.
It started with realizing that Lemire's rejection method becomes more likely to need a division/modulo as the range maxes out (the gate on division is fraction < range, so as range -> maxint the division case approaches 100%, if the fraction and range have the same precision). If the range is 32 bit but the fraction is 64, the division case is 2^32 times less likely, so I worked out Lemire's method for 32.64 fixed point.
That turned out to be similar to the infinite-precision case described here, as we only need 32 bits of fraction if it's far enough from the edges, ie nothing can carry into the integer part if we extend to 64.
Once I worked out when to extend to 64 bits in the rejection method, I realized that the infinite-precision case is similar, using a loop instead of a conditional. It's simpler since there's no rejection, just a decision to continue as long as it's possible to carry.
For 64, a variable shift may work. Powers of 10 have a lot of trailing 0 bits.
// this increments the upper 32 bits (log10 T - 1) when >= T is added
#define K(T) (((sizeof(#T)-1)<<32) - T)
(T is 10,100, etc.)
On top of that BRIN (block range) indexes are usually used to capture the value of sorting by pruning I/O. I don't see these mentioned here -- they seem like a good open-source version of this idea.
Another approach would be to create a table with a small sample of the main one and refresh it periodically -- each query just needs to do a random reordering on the small sample, and the results will be truly random over time.
I upgraded my range by adding a couple of Unifi AP's to my existing ISP router. It was less of a commitment than a UDM, and I originally just set them up in standalone mode with no account or cloud presence.
At read time, though, Snowflake's zone map is the same as Redshift's and Vertica's; you'll see similar pruning for many queries.
Redshift however doesn't prune during joins, which is a huge deficiency.
Snowflake looks more flexible about getting the data into its final ordering.
But I did make it fairly readable:
FixedPoint<uint32_t> x;
do {
x.setFraction(src());
x *= range;
} while(isRejectedValue(x.fraction()));
return x.floor();
Basically all the weird casts and things in Lemire are parts of 32.32 fixed point arithmetic; we stuff a random value into the .32 part to make a number in [0,1) and multiply. The rejection condition is a bit of modular arithmetic but not super hard.https://github.com/KWillets/range_generator/blob/59ffea502c4...
I added another method where it doesn't reject but extends right (variable-precision) until it's certain of the exact answer (ie that the floor() cannot change). I believe the Ryu printf algorithm does something similar to get the requested number of digits. It's unbiased but uses more random bits, which are expensive.