#20520·databend

bug: the bloom index hides a row whose FLOAT/DOUBLE value is a signed zero

Author: hanke580Created Sep 17, 2026Updated Sep 18, 2026
LabelsC-bug

Search before asking

  • I had searched in the issues and found no similar issues.

Version

8.0.90-v1.2.945-nightly-3ee958b464(rust-1.94.0-nightly-2026-09-17T11:34:15.268283012Z)

Also reproduces on released v1.2.881 (ca29960f5c). Nothing is configured in any of this — default databend-query.toml, no SET.

What's Wrong?

SELECT count(*) FROM t WHERE k = 0.0 returns 0 on a table that holds a -0.0. The same predicate evaluated without the index returns 1. No error, no warning — the query succeeds and reports a count that is wrong.

The bloom index hashes -0.0 and +0.0 to different keys, while the engine's = treats them as one value. Both halves of that are visible from SQL in one query:

sql
SELECT siphash64('-0.0'::DOUBLE) AS h_neg,
       siphash64(0.0::DOUBLE)    AS h_pos,
       ('-0.0'::DOUBLE = 0.0)    AS engine_says_equal;
h_neg                 h_pos                 engine_says_equal
17224059632725561478  13646096770106105413  true

siphash64 is the digest the bloom filter is built with, so the filter's answer to "could this block contain 0.0?" is no for a block whose only zero is negative, and the block is never read. An index may return extra rows for the filter above it to discard; it may not return fewer rows than the predicate selects.

How to Reproduce?

sql
CREATE DATABASE b41; USE b41;

CREATE TABLE t (i BIGINT, k DOUBLE);
INSERT INTO t SELECT number,
       CASE number WHEN 0 THEN '-0.0'::DOUBLE ELSE number::DOUBLE + 10 END
FROM numbers(2);

SELECT to_string(k)      AS stored_value FROM t WHERE i = 0;   -- -0   the row is there
SELECT count(*)          AS with_index   FROM t WHERE k = 0.0; --  0   <-- WRONG
SELECT count_if(k = 0.0) AS full_scan    FROM t;               --  1   the same predicate

The same two rows with the index removed return the row:

sql
CREATE TABLE t2 (i BIGINT, k DOUBLE) BLOOM_INDEX_COLUMNS='';
-- same INSERT
SELECT count(*) FROM t2 WHERE k = 0.0;                         --  1   correct

Same server, same values, same query — only the index differs, so the index is what loses the row.

A note on writing the value: Databend's literal parser turns a written -0.0 into a DECIMAL (SELECT -0.0 returns 0.0), so the reproduction casts from text to get a genuine negative-zero DOUBLE into the column. That is probably why this has not been hit before.

It takes two rows

A one-row block gets no bloom filter (bloom_filter_size = 0) and is correct. From two rows on, a filter is written and the row disappears. Measured on main today:

rows in the table bloom_filter_size WHERE k = 0.0 count_if(k = 0.0)
1 0 1 1
2 623 0 1
3 629 0 1
10 647 0 1
100 867 0 1
10 000 25 238 0 1

No spelling of zero finds the row

Once the negative zero shares a block with other rows, it is unreachable through the index by any equality predicate. Stored -0.0, 100-row block, main:

probe WHERE k = probe count_if(k = probe)
0.0 0 1
-0.0 0 1
'-0.0'::DOUBLE 0 1
'0'::DOUBLE 0 1
0 0 1
0e0 0 1
0.00 0 1
0.0 + 0 0 1

including '-0.0'::DOUBLE, the exact expression that wrote the row.

Both signs and both float types are affected — whichever sign is stored, the other one cannot find it:

column stored probed with WHERE full scan
DOUBLE '-0.0'::DOUBLE 0.0 0 1
DOUBLE 0.0 '-0.0'::DOUBLE 0 1
FLOAT '-0.0'::FLOAT 0.0 0 1
FLOAT 0.0 '-0.0'::FLOAT 0 1

A fourth item in an IN list changes the answer

max_inlist_to_or is 3: a list of three or fewer items is rewritten into a chain of ORs, which consults the bloom index; four or more stays an IN and takes a path that does not. On a table with one -0.0 among a hundred rows, appending a fourth value that matches nothing turns a wrong answer into a right one:

items in the list v IN (0.0, …) v = 0.0 OR …
1 0 0
2 0 0
3 0 0
4 1 ✅ 0
5, 6, 8, 12, 16, 24, 32, 48, 64, 96, 128 1 ✅ 0

Swept at 15 list lengths; the flip is exactly at 4, where max_inlist_to_or says it should be. The OR spelling of the same predicate is wrong at every length, which is what identifies the rewrite — rather than the list length — as what matters. Same root cause, not a second bug; it is just the sharpest demonstration that two spellings of one predicate disagree because only one of them asks the index.

Everything else tried is clean

Same 100-row block shape, value planted at row 0 and probed for:

BIGINT (42, 0), VARCHAR, DATE, TIMESTAMP, and the non-zero floats 1.5, 1e308, 5e-324, nan — all found by the indexed path, all matching the full scan. So this is specific to signed zero rather than general distrust of the index. (DECIMAL and BOOLEAN columns get no bloom filter written at all — bloom_filter_size does not grow when the column is added — so they are untested rather than clean. DECIMAL has no signed zero, so it is moot there.)

Expected behavior

SELECT count(*) FROM t WHERE k = 0.0 should return the same answer as SELECT count_if(k = 0.0) FROM t, because they are the same predicate.

Concretely: the bloom digest for FLOAT/DOUBLE should normalise -0.0 to +0.0 before hashing — both when the filter is built and when it is probed — so that values the engine's = considers equal hash to the same key. Normalising only one side would make it worse.

Are you willing to submit PR?

  • Yes I am willing to submit a PR!