Sorting number strings numerically
arangodb.com
arangodb.com
First, imagine we store the number in the form
aaaaaaaaaaa 92389210184
where the number of a's is equal to the length of the number. This encoding scheme works for the same reasons as discussed in the article. And it only takes 2*(length of original number)+1 characters.But now, we can do better by applying the same encoding scheme to the 'aaaaaaaaaaa'. That is, suppose the original number is 295 digits long. The number 295 is 3 digits long. So we store the number with 3 b's, as:
bbb 295 109382388575782352353453....
OK, but what if we want to store a number that's 4,000,000,000 digits long? This would look like bbbbbbbbbb 4000000000 109382388575782352353453....
And that's a bit wasteful. If there are more than two b's, then we can recursively apply the encoding to the b's. There are 10 b's, and 10 has two digits, so we get cc 10 4000000000 109382388575782352353453....
And so on. (We won't ever have hard drives that can make use of letters e and above, so it can probably stop there.)So the encoding schemes look like
a n # n is only one digit
aa n # n is two digits
b log10(n) n # log10(n) is one digit
bb log10(n) n # log10(n) is two digits
c log10(log10(n)) log10(n) n # log10(log10(n)) is one digit
cc log10(log10(n)) log10(n) n # log10(log10(n)) is two digits
d log10(log10(log10(n))) log10(log10(n)) log10(n) n
...1. Given n, write aaaaaaa n where the number of a's is equal to the length of n.
2. If the number of a's is 1 or 2, stop.
3. Let n' be the number of a's.
4. Write bbbbb n' n where the number of b's is equal to the length of n'.
5. If the number of b's is 1 or 2, stop.
6. ...continue until the number of initial letters is 1 or 2.
I think that algorithm is mostly well-defined and only produces one possible output for each input number. So let's see if I can give a good proof-ish argument that it sorts correctly.
Suppose n <= m, then length(n) <= length(m).
Case 1: If their lengths are equal, then in step 1, they are written as
aaaaaaaaa n
aaaaaaaaa m
where the number of a's are the same. Now the rest of the algorithm is the same for both numbers, since they have the same number of a's, so they get written as the exact same prefix followed by the number. Since they have the same length, the smaller one sorts first.Case 2: Now suppose length(n) < length(m), and further suppose length(n) <= 2. Then in step 1, they are written as, for instance,
a n
aa m
or something like aa n
aaaa m
Now any encoding of m in further steps will start with the letter b or greater, which sorts below "aa n".Case 3: Now suppose length(n) < length(m), but further suppose length(n) > 2. Then in step 1, they are written as
aaaaaaaaa n
aaaaaaaaaaa m
Now n will sort before m as long as the encoding of a smaller string of a's sorts before the encoding of a longer string. Let n' = length(n) and m' = length(m). We know that n' < m' and we want to prove that the encoding of n' sorts before the encoding of m'. This is true recursively by the same 3-case argument as above, though I haven't really formalized it well here.For example, if length(n') = length(m'), then they both get written in the next step as
bbbb n' n
bbbb m' m
which is just like Case 1 above. Otherwise, they get written something like b n' n
bbb m' m
which is just like Case 2 above. Otherwise, they get written bbbbb n' n
bbbbbb m' m
which is like Case 3 above, and requires us to recurse again.I implemented a decent portion of it here for a Go project: https://github.com/jordanorelli/lexnum
the whitepaper also discusses an implementation that accommodates floating point numbers, but I didn't implement that portion. This encoding scheme works well for file names.
A more fun problem is extending this scheme to support rational numbers. http://www.imada.sdu.dk/~kornerup/papers/lcf.ps.gz gives a neat scheme using continued fractions. (I've had a go at using it to implement a sort order preserving encoding that works for all the rationals at https://github.com/NegativeMjark/lexical-binary )
1/1 2/1 3/1 4/1 ...
1/2 2/2 3/2 4/2 ...
1/3 2/3 3/3 4/3 ...
...
You can traverse each SW-NE diagonal, producing the sequence 1/1, 1/2, 2/1, 1/3, 2/2, 3/1, ...This will eventually hit every positive rational (several representations, in fact). If you want, you can start with 0 and insert -q when you hit q.
JSON does not restrict the range of numbers. It happens they many languages that parse JSON do, but ArangoDB could try and be different! :)
If Arango would just allow unlimited numbers in JSON data and sort them numerically, developers could use a different JSON encoder and parser in their language of choice, that would encode and parse numbers from the unlimited representation allowed by JSON to whatever bignum library their language has.
Although if you wanted to save space you could also store the number itself in a bigger base to reduce the number of "digits".
Since the goal is to store really long numbers, it seems like a false economy to worry about a few bytes at the front.
+/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz
as the normal one is not order-preserving. Otherwise yeah, you could probably do that--and of course there are many ways to patch what you've got to handle negatives, e.g. by noticing that it is slightly wasteful (the base64 prefix ++++++++ is never used) or by using a leading character lower than + (so - is out but ! for example is available) and then encoding the rest of the string in an "ascii-reversed" way.However I must object to the idea that "real hardware" cannot handle the petabyte scale. The in-article recursive approach of using the algorithm:
function encode(number) {
//calculate some base number_string
return number < NUM_THRESHOLD?
number_string :
sigil + encode(number_string.length) + opt_divider + number_string;
}
is not a particularly bad one. The only thing that maybe I'd balk at is that the given encoding requires a human editor to use an ASCII table to lookup their string lengths, which is clearly undesirable... one might instead go for the most naive case where NUM_THRESHOLD is simply 10 and have the number 123,456,789,012,345,678,901 be represented as: ~~2 21 123456789012345678901
so that it's clearly "there are going to be 2 length strings" (hence two tildes) -- the first always has length 1, it specifies "the next has length 2" and then "the next has length 21."This is also pretty easy to parse and one can use a leading - sign to encode negative numbers, for example.
By the time I'm talking about integers that don't fit in x86 address space, I'm not talking JSON ;).
["-10", "-11", "0", "1", "-1", "-2", "10", "11", "110"].map(encodeLong).sort().map(decodeLong)
Results in: ["0", "1", "10", "11", "110", "-11", "-10", "-2", "-1"] function encodeLong(s) {
if (s[0] !== '-') { return encodeNonNegative(s, ' '); }
return '!' + translate(encodeNonNegative(s.slice(1), ' '));
}
function decodeLong(s) {
if (s[0] !== "!") { return decodeNonNegative(s); }
return '-' + decodeNonNegative(translate(s.slice(1)));
}https://sqlite.org/src4/doc/trunk/www/key_encoding.wiki
Numeric SQL values must be coded so as to sort in numeric order. We assume that numeric SQL values can be both integer and floating point values.
I think that utf-8 sorting may depend on the locale. But here you're not depending on that -- you're just using the subset that is ASCII sorting, no?
" 0
" 1
# 42
Why is the length stored in base 92 while the number is stored in base 10?Why not just store them both in base 128 or 127 (if you want C strings)? You can't use base 256 because some strings won't be valid UTF-8, but base 127 or 128 seems fine.
Do you want some notion of human readability? That isn't in the problem statement. Your problem statement seems fairly imprecise in a couple ways.
If it helps, I am not the author of the original article.
It is common to want to ask a database "tell me all groups of size greater than X, with properties A, B and C". Now, if you have arbitrary sized ints, no problem. If your database (or language) doesn't support big ints, you need to figure out how to do "bigger than X", when you are storing big numbers in some other format, probably strings.
If you can sort a numeric string in base 10, you can sort one in base 16, or base 64, or base 256.
However, the important bit (to me) is that you can use your database's sorting function to compare the numbers.
The advantage of base-10 is it's easy to display the number :) Others bases do provide a constant speed improvement of course.
In fact I'd make an index on (len(N), N), then include len(X) and X in all your comparisons and range queries (wrapping for ergonomics as needed). You can use any base storage (including compact varbinary base256).