When it does not fit

The resolution a range filter can afford

A prefix filter on each level of a log-structured store answers a range by asking one question for every prefix the range touches, and each can be wrong. A filter holding prefixes at every length from 16 to 24 bits could ask each range at the resolution it needs, and was proposed as the repair. At the store's five bits a key it costs 5.69 transfers a range query where one length a level costs 4.32, because a short range is asked only at the finest length, and that filter now holds a fraction of the memory. Asked at every length it recovers most of that, and seems to overtake the single length at twelve bits a key — but only a single length chosen for five bits. Chosen again for each memory, one length a level beats every multi-resolution design at every memory measured, because the resolution a level can afford is itself set by its memory.

The runs a range query can skip put a Bloom filter over key prefixes on each level of a four-level store of 2^20 keys. A range query can skip a level whose filter shows that no prefix the range touches holds a key. The small levels’ share of a range filter gave each level its own prefix length, which took the store’s mixed range workload from 4.514 transfers a query to about 4.32, and then its own share of five bits a key, which took it to 4.043, against an oracle’s 3.738. It decomposed what was left: three fifths of the gap to the oracle was false positives. A range asks the filter one question for each prefix it touches, every question can come back yes wrongly, and a long range asks many.

Its closing section proposed the repair: a filter that answers the range rather than its prefixes. Keep prefix filters at every length from 16 to 24 bits in one level’s memory. Ask a range at the longest length that covers it with at most two prefixes, and descend to finer lengths only below the answers that came back yes, the way a tree with nodes the size of a block descends only into the branches that could hold an answer. The section predicted that on short ranges the filter would match the single best length, since it asks the same one or two questions, and that on ranges of about ten keys it would recover skips the single length loses. It asked whether asking at the right resolution recovers more than the division of memory costs.

The answer depends entirely on the memory, and at the memory the store actually has, it is no.

The filter, built and charged

Each level keeps a Bloom filter for each stored length. Its memory, five bits for each of its keys, is divided among the lengths in proportion to the distinct prefixes each holds, so every stored prefix gets the same share. A range is asked at the longest stored length at which it touches at most two prefixes. Every prefix answered yes is replaced by its children at the next stored length that lie in the range, and those are asked in turn. The level is read if a yes survives to the finest length, and skipped if every branch has been answered no. Charges are the earlier pages’: a level read costs one transfer plus one for every 64 of its keys in the range. The single-length design is the earlier page’s lengths per level, 18, 19, 21 and 22 bits from the smallest level to the largest, with the memory spread evenly — the 4.32 design, so that every comparison here is at the same bits a key on every level. The workload is a thousand ranges each of four lengths, expected to hold a tenth of a key, one, ten and a hundred keys of the store.

A second way of asking is measured beside the proposal. Instead of starting where the range fits in two prefixes, it asks every stored length from the coarsest down, so that even a short range consults the coarse filters above it. Both are exact in the sense that matters: across every query, neither ever skips a level holding a key of the range.

Five bits a key, divided nine ways

By range length at 5 bits a key: on ranges of a tenth of a key the proposed filter costs 3.73 transfers against 0.91 for one length a level, because a short range is asked only at the finest length, and that filter now holds a small share of the memory; on ranges of ten keys, where it was predicted to gain, it costs 6.39 against 5.65Mean transfers a range query at 5 bits a key, for ranges expected to hold 0.1, 1, 10 and 100 of the store's keys. 16 to 24 bits, asked where it fits: 0.1 3.734, 1 4.260, 10 6.386, 100 8.396. 16 to 24 bits, asked at every length: 0.1 1.448, 1 3.307, 10 6.287, 100 8.396. One length a level: 0.1 0.914, 1 2.345, 10 5.647, 100 8.368. The oracle: 0.1 0.186, 1 1.558, 10 5.044, 100 8.165. Both axes are logarithmic.0.11101000.512510keys the range is expected to holdtransfers a range query16 to 24 bits, asked where itfits16 to 24 bits, asked at everylengthone length a levelthe oracle5 bits a keydashed: the oracle
Fig. 1 Transfers a range query at five bits a key, by the number of keys the range is expected to hold. One length a level: 0.91, 2.35, 5.65, 8.37. The proposed filter: 3.73, 4.26, 6.39, 8.40. Asked at every length: 1.45, 3.31, 6.29, 8.40. The oracle: 0.19, 1.56, 5.04, 8.17.

On ranges expected to hold a tenth of a key, the proposed filter costs 3.73 transfers a query where one length a level costs 0.91 — worse than the single length by four times, and the prediction said they would be equal. They ask the same one or two questions of the finest filter, as the prediction said, but not of the same filter. The single length’s filter holds all five bits a key. The proposed filter’s finest length holds about a ninth of them, since on a sparse level each of the nine lengths has about as many distinct prefixes as there are keys. A Bloom filter at under a bit a key says yes to almost everything. A floor on the bits put the least memory any membership structure needs at the logarithm of one over its false-positive rate, and a ninth of five bits is below that floor for any useful rate. Nothing about the descent can make a starved filter answer no. On ranges of ten keys, where the proposal was expected to gain, it costs 6.39 against 5.65, and it loses at every range length.

The skips a filter misses at 5 bits a key: of the levels holding nothing in a range of a tenth of a key, one length a level reads 19%, the proposed filter 91%, and asked at every length 32%; on ranges of ten keys the three read 41%, 91% and 84% of the empty levelsThe share of (query, level) pairs with no key of the range in the level whose level the filter nevertheless reads, at 5 bits a key. 16 to 24 bits, asked where it fits: 0.1 91%, 1 84%, 10 91%, 100 100%. 16 to 24 bits, asked at every length: 0.1 32%, 1 54%, 10 84%, 100 100%. One length a level: 0.1 19%, 1 24%, 10 41%, 100 88%. The horizontal axis is logarithmic.0.1110100keys the range is expected to holdempty levels read0%25%50%75%100%16 to 24 bits, asked where itfits16 to 24 bits, asked at everylengthone length a level5 bits a keya false read: an empty level read
Fig. 2 The share of levels holding nothing in the range that each filter nevertheless reads, at five bits a key. One length a level: 19% for ranges of a tenth of a key, 24% for one, 41% for ten, 88% for a hundred. The proposed filter: 91%, 84%, 91%, 100%. Asked at every length: 32%, 54%, 84%, 100%.

The false reads say where it goes wrong. On short ranges the proposed filter reads 91% of the levels it could have skipped, against the single length’s 19%. Asking every length from the coarsest down brings that to 32%, because a short range now meets eight coarser filters on its way down, and any of them can say no. That recovers most of the loss on short ranges, 1.45 transfers against the proposal’s 3.73. Averaged over the workload it costs 4.86 against the single length’s 4.32, still worse.

Coarse questions that are always answered yes

The prediction’s picture was that a coarse question is a cheap way to rule out a range that spans many fine prefixes. It works only where a coarse prefix can be empty.

Why coarse questions answer yes: on the largest level every 16-bit prefix holds a key and 93% of 18-bit ones do, so a filter asked about a coarse prefix there is nearly always right to say yes and learns nothing; on the smallest level 22% of 16-bit prefixes are occupied, and the coarse question can say noThe share of b-bit key prefixes holding at least one key, for each level of the store, b from 16 to 24. 16K keys: 16 bits 22.2%, 17 bits 11.8%, 18 bits 6.1%, 19 bits 3.1%, 20 bits 1.6%, 21 bits 0.8%, 22 bits 0.4%, 23 bits 0.2%, 24 bits 0.1%. 64K keys: 16 bits 63.3%, 17 bits 39.3%, 18 bits 22.1%, 19 bits 11.8%, 20 bits 6.1%, 21 bits 3.1%, 22 bits 1.6%, 23 bits 0.8%, 24 bits 0.4%. 256K keys: 16 bits 98.2%, 17 bits 86.5%, 18 bits 63.2%, 19 bits 39.4%, 20 bits 22.1%, 21 bits 11.8%, 22 bits 6.1%, 23 bits 3.1%, 24 bits 1.6%. 688K keys: 16 bits 100.0%, 17 bits 99.5%, 18 bits 93.2%, 19 bits 73.9%, 20 bits 49.0%, 21 bits 28.6%, 22 bits 15.5%, 23 bits 8.1%, 24 bits 4.1%. The vertical axis is logarithmic.0.0010.010.11prefix length, bitsprefixes holding a key16171819202122232416K keys64K keys256K keys688K keysfour levels, 2^20 keyslabels at 18 bits
Fig. 3 The share of b-bit prefixes holding at least one key, for each level. Smallest level, 16K keys: 22% at 16 bits, 6% at 18, 0.1% at 24. 64K keys: 63%, 22%, 0.4%. 256K keys: 98%, 63%, 1.6%. Largest level, 688K keys: 100%, 93%, 4.1%.

On the largest level every 16-bit prefix holds a key, and 93% of 18-bit prefixes do. A coarse question asked there is truthfully answered yes, and a correct yes rules out nothing. The largest level is where most of a range query’s transfers are spent, since its runs are the biggest, and it is the level where the coarse filters are useless. Their memory is not wasted there in the way a false positive is wasted; it is spent answering a question whose answer is known. On the smallest level, 22% of 16-bit prefixes are occupied, and a coarse no is common and worth having.

This is the same finding a penalty set by the block made about blocked Bloom filters in another form. A filter’s value lies in the questions it can answer no. A structure whose memory goes partly to questions the data always answers yes is paying for certainty it already had.

When the memory is there

Asking at the range's own resolution against one length a level: at five bits a key the proposed filter costs 5.69 transfers a range query and one length 4.32; asked at every length it overtakes lengths chosen for five bits at 12 bits a key — but lengths chosen for each memory beat it at every memory measured, 3.914 against 4.078 at ten bits and 3.764 against 3.788 at twenty, with an oracle's 3.738Mean transfers a range query over ranges expected to hold 0.1, 1, 10 and 100 keys of the store, a thousand of each, against the filter memory in bits a key. 16 to 24 bits, asked where it fits: 2 bits 5.941, 5 bits 5.694, 8 bits 5.174, 10 bits 4.872, 12 bits 4.627, 16 bits 4.299, 20 bits 4.102, 32 bits 3.894. 16 to 24 bits, asked at every length: 2 bits 5.819, 5 bits 4.860, 8 bits 4.275, 10 bits 4.078, 12 bits 3.966, 16 bits 3.842, 20 bits 3.788, 32 bits 3.767. One length, chosen for five bits: 2 bits 5.094, 5 bits 4.319, 8 bits 4.061, 10 bits 4.004, 12 bits 3.977, 16 bits 3.967, 20 bits 3.965, 32 bits 3.965. One length, chosen for each memory: 2 bits 5.024, 5 bits 4.319, 8 bits 4.021, 10 bits 3.914, 12 bits 3.846, 16 bits 3.783, 20 bits 3.764, 32 bits 3.760. The lengths chosen for each memory, smallest level to largest: 2 bits 16/18/20/21; 5 bits 18/19/21/22; 8 bits 18/20/22/23; 10 bits 19/21/23/24; 12 bits 19/21/23/24; 16 bits 21/23/24/25; 20 bits 21/24/24/26; 32 bits 21/24/24/26. The oracle: 3.738. No filter: 5.947.456filter bits a key, over the whole storetransfers a range query258101216203216 to 24 bits, asked where it fits16 to 24 bits, asked at everylengthone length, chosen for five bitsone length, chosen for each memorythe oraclefour levels, 2^20 keysdotted: the oracle
Fig. 4 Transfers a range query over the mixed workload against filter memory. The proposed filter: 5.69 at five bits a key, 4.87 at ten, 3.89 at thirty-two. Asked at every length: 4.86, 4.08, 3.77. One length a level chosen for five bits: 4.32, 4.00, then 3.965 from twenty on. One length a level chosen again for each memory: 4.32, 3.91, 3.76. The oracle: 3.74.

With more memory the multi-resolution filter improves quickly, and at first sight it overtakes. Asked at every length, it passes the single length at twelve bits a key, 3.966 against 3.977, and at twenty bits it costs 3.79 against 3.965, while the single length stays at 3.965 however much memory it is given. That looked like the answer to the section’s question. A length for each range, it seemed, recovers what one length a level cannot, once the memory can make nine filters reliable.

It is an artifact of the comparison. The single length in those numbers is the earlier page’s, chosen at five bits a key — 18, 19, 21 and 22 bits from the smallest level to the largest — and held there as the memory grew. Chosen again for each memory, one length a level costs 3.91 transfers a range query at ten bits a key against the multi-resolution filter’s 4.08, 3.76 at twenty against 3.79, and 3.760 at thirty-two against 3.767. It is ahead at every memory measured, from two bits a key to thirty-two, and within 0.03 of the oracle from twenty bits upwards. The multi-resolution filter’s apparent win came from its 23- and 24-bit filters answering short ranges more finely than an 18-bit filter could. A single length allowed to be 21 or 24 bits does the same thing, with all the memory behind one filter instead of a ninth of it.

The resolution a level can afford

The resolution a level can afford rises with its memory: the best single prefix length for the smallest level goes from 16 bits at 2 bits a key to 21 at 32, and for the largest from 21 to 26 — a finer prefix separates more of the range from its neighbours and needs more memory for the same false-positive rate, and each memory has its own balanceFor each level, the prefix length from 16 to 30 bits at which a single-length filter makes the fewest transfers over the mixed range workload, at each filter memory. 16K keys: 2 bits 16, 5 bits 18, 8 bits 18, 10 bits 19, 12 bits 19, 16 bits 21, 20 bits 21, 32 bits 21. 64K keys: 2 bits 18, 5 bits 19, 8 bits 20, 10 bits 21, 12 bits 21, 16 bits 23, 20 bits 24, 32 bits 24. 256K keys: 2 bits 20, 5 bits 21, 8 bits 22, 10 bits 23, 12 bits 23, 16 bits 24, 20 bits 24, 32 bits 24. 688K keys: 2 bits 21, 5 bits 22, 8 bits 23, 10 bits 24, 12 bits 24, 16 bits 25, 20 bits 26, 32 bits 26.161820222426filter bits a key, over the whole storebest prefix length, bits258101216203216K keys64K keys256K keys688K keysone length a levellines offset slightly where they coincide
Fig. 5 The best single prefix length for each level against the filter memory. Smallest level, 16K keys: 16 bits at two bits a key, 18 at five, 19 at ten, 21 from sixteen. 64K keys: 18, 19, 21, 24. 256K keys: 20, 21, 23, 24. Largest level, 688K keys: 21, 22, 24, 26.

The best single length rises with the memory: on the smallest level from 16 bits at two bits a key to 21 at sixteen, and on the largest from 21 to 26. A finer prefix separates more of a short range from its neighbours in key space, which the oracle rewards. It also has more distinct prefixes to store, and so a higher false-positive rate at the same memory. Each memory has its own balance point between the two, and the balance moves finer as the memory grows. A length chosen at one memory is the wrong length at another, which is what the fixed single length on the first plate showed. It flattened at 3.965 because its lengths stopped being the right ones, not because one resolution had run out.

The multi-resolution filter is an attempt to avoid choosing the resolution. The measurements say the choice is cheap to make and expensive to avoid. A store knows its filter memory when it builds a level, and the best length for that memory is one sweep over a few lengths away. The earlier page found that each level wants its own length at a fixed memory. The same argument applies across memories: the length is a function of both the level and the budget. Nine lengths in one budget are nine filters each too starved to be sure of anything. The runs a range query can skip chose one length for the whole store at one memory, and this page adds the memory as the second thing the length depends on.

Trimming the lengths does not rescue it

Two repairs suggest themselves, and both were measured. The first drops the lengths the occupancy plate shows are nearly always truthfully yes. Each level keeps only the lengths at which at most half its prefixes hold a key: all nine on the smallest level, 17 to 24 bits on the next, 19 to 24 and 20 to 24 on the two large ones. At five bits a key it costs 4.80 transfers a range query against 4.86 with all nine, a saving of a hundredth of the gap. The coarse lengths never held much memory. Memory was divided by distinct prefixes, and a coarse length on a dense level has few, so dropping them returns little. The memory is in the fine lengths. Each of them, on a sparse level, holds about one prefix for every key, so storing five fine lengths costs five times what storing one does. The filter each run carries found the same arithmetic spreading five bits a key across the levels of a store: memory divided among filters that each hold as many items as keys is memory each of them lacks. The multi-resolution filter makes that division inside one level.

The second repair stops each level’s lengths at the length a single filter would choose for the same memory, so that the descent never goes finer than the single length would. At five bits a key that costs 4.43 transfers against the single length’s 4.32, at ten bits 3.98 against 3.91, and at twenty 3.78 against 3.76. It closes most of the distance and never crosses. With the finest resolution the same, the coarse lengths above it only divide the memory, and on uniform keys they buy nothing for it.

What the descent asks

What each design asks, at 20 bits a key over four levels: one length chosen for this memory asks 13.79 filter probes a query on ranges of a tenth of a key, 600 on ranges of ten and 972 on ranges of a hundred; asked at every length, the descent asks 16.76, 146 and 719 — more questions on the shortest ranges and far fewer on long ones, the one place its coarse lengths earn their memoryFilter probes a range query, summed over the four levels, at 20 bits a key. One length, chosen for this memory: 0.1 13.79, 1 101.98, 10 599.99, 100 972.00. 16 to 24 bits, asked at every length: 0.1 16.76, 1 27.85, 10 145.76, 100 718.80. 16 to 24 bits, asked where it fits: 0.1 10.13, 1 23.00, 10 144.21, 100 718.80. A probe is one Bloom-filter lookup; a level asked more than 256 probes is read instead. Both axes are logarithmic.0.111010031010010³keys the range is expected to holdfilter probes a queryone length, chosen for thismemory16 to 24 bits, asked at everylength16 to 24 bits, asked where itfits20 bits a keyprobes: Bloom lookups, not transfers
Fig. 6 Filter probes a range query summed over the four levels, at twenty bits a key. One length a level chosen for twenty bits: 13.8 for ranges of a tenth of a key, 102 for one, 600 for ten, 972 for a hundred. Asked at every length: 16.8, 28, 146, 719. The proposed filter: 10.1, 23, 144, 719.

There is one currency in which the multi-resolution filter does win. At twenty bits a key, the single length chosen for that memory asks 600 filter probes a query on ranges of ten keys and 972 on ranges of a hundred; the descent, asked at every length, asks 146 and 719. The single length’s fine prefixes are what make its transfers low, and they make a long range touch hundreds of prefixes, each a question. The descent answers a long range at a coarse length with two questions and only refines below its yes answers. On the shortest ranges it asks slightly more, 16.8 against 13.8, because it walks down through all nine filters. A Bloom probe is a few memory reads, not a transfer. For a store on disk, where a transfer costs a thousand probes, the transfers decide, and the single length wins. For a store held in memory, where a probe and a read cost about the same, the multi-resolution filter’s economy of questions could be the thing worth having. That would be a different comparison, charged in probes and reads together, and it is not made here.

Where the measurement stops

One store, one key distribution. Keys are uniform over 32 bits, the store has four levels in a ratio of four, and ranges start at uniformly random points. A store whose keys cluster would have more empty coarse prefixes on every level, and the coarse filters would earn more.

Memory divided by distinct prefixes. Each stored length gets bits in proportion to how many prefixes it holds. The largest level’s coarse filters are therefore given little memory. They would be better given none, and a design that dropped every length a level’s occupancy makes useless would move the crossing below twelve bits. That design is not measured here.

A probe limit of 256 a level. A level asked more than 256 probes is read. The earlier pages used 64 for a single length, and the difference moves the single length’s floor, not its crossing with the descent.

Lengths chosen on the queries they are charged on. The single length for each memory is the best of thirteen lengths on the same thousand ranges of each size that it is then scored on. That flatters it slightly. The earlier page chose its allocation on one set of ranges and charged it on another, and the cost moved by under 1%. The margins that decide this page, 0.16 transfers at ten bits a key and 0.02 at twenty, are larger than that at ten bits and comparable at twenty. At twenty bits the honest reading is that the two designs are about equal, and the single one is simpler.

Four range lengths, equally weighted. The mixed workload gives ranges of a tenth of a key, one, ten and a hundred keys equal weight. A workload made mostly of long ranges would weight the probes plate’s economy more heavily in the multi-resolution filter’s favour, though still in probes and not transfers.

Transfers only. The bits a second threshold costs found a design that narrowed a spread and paid for it in table bits. Here the price is paid in bits and in probes, and only the transfers are optimised.

Still open: keys whose coarse prefixes are empty

Every coarse question on this page was asked of uniformly spread keys, and on the large levels uniform keys fill every coarse prefix. The occupancy plate is the whole reason the coarse filters earned nothing: a filter can only help where a prefix can be empty. Real keys cluster. Timestamps arrive in bursts, identifiers are handed out in blocks, and a key space of 32 bits holding a million keys in a few dense regions has most of its 16-bit prefixes empty on every level.

The measurement that follows draws keys from a stated number of clusters, from uniform down to a handful of dense regions, and repeats the comparison: one length a level chosen for each memory, against the multi-resolution filter asked at every length. The prediction is that on clustered keys the coarse filters begin to say no on long ranges. A range of a hundred keys that falls between clusters would then be skipped by one coarse question, where a single fine length asks hundreds of questions or gives up and reads. If that holds, the multi-resolution filter pays on clustered keys and not on uniform ones, and the choice between them is a property of the key distribution, not of the filter. It could fail if clustering also makes the fine prefixes inside a cluster denser. The single length’s best resolution would then move finer inside the clusters and coarser between them — a different length for different parts of one level, which neither design here can express.

Named alongside this one

Essays reaching for the same objects. Nobody chose these; they are what the concept index makes visible.

The objects this essay names

Each one links to every other essay that touches it.

Block transferBloom filterDesign parameterFalse-positiveFalse-positive rateLsm treePer levelRange queryRead amplification