[ 
https://issues.apache.org/jira/browse/IMPALA-15443?page=com.atlassian.jira.plugin.system.issuetabpanels:all-tabpanel
 ]

Zoltán Borók-Nagy resolved IMPALA-15443.
----------------------------------------
    Resolution: Fixed

> Parquet stats filtering decodes INT32/INT64 min/max at the table's wider 
> DECIMAL width (decimal widening, int->decimal), silently dropping rows
> -----------------------------------------------------------------------------------------------------------------------------------------------
>
>                 Key: IMPALA-15443
>                 URL: https://issues.apache.org/jira/browse/IMPALA-15443
>             Project: IMPALA
>          Issue Type: Bug
>          Components: Backend
>            Reporter: Zoltán Borók-Nagy
>            Assignee: Zoltán Borók-Nagy
>            Priority: Major
>              Labels: correctness
>             Fix For: Impala 5.0.0
>
>
> h3. Problem
> If a Parquet file stores a column as INT32 or INT64, but the table's DECIMAL 
> type needs more bytes (INT32 file read as precision >= 10, INT64 file read as 
> precision >= 19), Impala decodes the file's min/max statistics at the 
> *table's* width. The upper bytes of the decoded value are not taken from the 
> file, so they depend on memory contents and answers can differ between 
> executions. Row-group statistics and the page index (column index) both use 
> these values, so row groups and pages that contain matching rows are skipped 
> and queries silently return too few rows. With default options, some tables 
> gave the same wrong answer in every try and others varied. With negative 
> values the answer was wrong in almost every try (4.5.2 and master; one master 
> run of {{t_neg}} with {{PARQUET_READ_PAGE_INDEX=false}} was right in 2 of 50 
> tries).
> This is the normal state after a decimal precision widening:
> * Iceberg type promotion {{decimal(10,2) -> decimal(20,2)}} (spec-valid). Old 
> files keep INT64 decimal(10,2).
> * Plain Parquet tables after {{ALTER TABLE ... CHANGE amt amt DECIMAL(20,2)}} 
> (the IMPALA-7087 path).
> * Master (5.0.0) only: INT/BIGINT columns read as DECIMAL (IMPALA-13625), 
> e.g. after {{ALTER TABLE t CHANGE c c DECIMAL(10,0)}}. IMPALA-13625 requires 
> precision >= 10 for INT32 and >= 19 for INT64, so every such read uses a 
> wider slot.
> Only files that store the column as INT32/INT64 *and* carry column orders are 
> affected: parquet-mr 1.17.1 (as used by Spark 4.1.3 + Iceberg 1.11.0), 
> pyarrow with {{store_decimal_as_integer=True}}, and on master Impala-written 
> INT/BIGINT columns read as DECIMAL through IMPALA-13625. Files without column 
> orders are not affected, because their decimal stats are ignored. 
> Impala-written DECIMAL columns (FIXED_LEN_BYTE_ARRAY) are not affected. The 
> values themselves are read correctly; only stats-based skipping is wrong.
> h3. Steps to reproduce (no Iceberg needed)
> {code:python}
> import decimal, pyarrow as pa, pyarrow.parquet as pq
> vals = [decimal.Decimal(f"{i}.00") for i in range(-5, 5)]
> t = pa.table({"id": pa.array(range(1, 11), pa.int64()), "amt": pa.array(vals, 
> pa.decimal128(10, 2))})
> pq.write_table(t, "decimal_int64_negative.parquet", 
> store_decimal_as_integer=True,
>                write_page_index=True, max_rows_per_page=2, 
> use_dictionary=False, compression="NONE")
> # amt: INT64 Decimal(precision=10, scale=2), min -5.00, max 4.00
> {code}
> Put the file into an empty directory, then in Impala:
> {code:sql}
> CREATE EXTERNAL TABLE hive_neg (id BIGINT, amt DECIMAL(20,2)) STORED AS 
> PARQUET LOCATION '<dir>';
> SELECT count(*) FROM hive_neg WHERE amt < 0;       -- expected 5
> SELECT count(*) FROM hive_neg WHERE amt = -3.00;   -- expected 1
> {code}
> Iceberg variant (Spark SQL, then register the table in Impala):
> {code:sql}
> CREATE TABLE t_neg (id BIGINT, amt DECIMAL(10,2)) USING iceberg TBLPROPERTIES 
> ('format-version'='2');
> INSERT INTO t_neg SELECT id, CAST(id AS DECIMAL(10,2)) FROM range(-5, 5, 1, 
> 1);  -- INT64 decimal(10,2)
> ALTER TABLE t_neg ALTER COLUMN amt TYPE DECIMAL(20,2);
> INSERT INTO t_neg SELECT id, CAST(id AS DECIMAL(20,2)) FROM range(-5, 5, 1, 
> 1);  -- FIXED_LEN_BYTE_ARRAY decimal(20,2)
> -- Spark: SELECT count(*) FROM t_neg WHERE amt < 0  ->  10
> {code}
> {code:sql}
> -- Impala, for a Hadoop-catalog table (use the HMS equivalent otherwise)
> CREATE EXTERNAL TABLE t_neg STORED AS ICEBERG LOCATION '<table dir>'
>   TBLPROPERTIES('iceberg.catalog'='hadoop.tables');
> SELECT count(*) FROM t_neg WHERE amt < 0;          -- expected 10
> {code}
> h3. Actual result (Impala 4.5.2; master 3d4d47dc30 RELEASE build gives the 
> same unless noted)
> * {{hive_neg}}: {{amt < 0}} returns *0* (50/50 tries, also on master), {{amt 
> = -3.00}} returns *0*. {{SELECT *}} returns the correct values. With 
> {{PARQUET_READ_STATISTICS=false}} the answers are 5 and 1.
> * {{t_neg}}: {{amt < 0}} returns *5* instead of 10 (50/50 on master); {{amt + 
> 0 < 0}} (no stats conjunct) returns 10. Profile: {{NumRowGroups: 2}}, 
> {{NumStatsFilteredRowGroups: 1}}. {{amt = -3.00}} returns 1 instead of 2.
> * On a larger Iceberg table (100k rows per file, 5 pages per column chunk), 
> {{amt < 50000.00}} returns 50278 instead of 100260 (the whole old file is 
> skipped). On 4.5.2 the answer varied between executions of the same query: in 
> one try only 4 of the 5 pages of the old file were skipped (60274), and the 
> original benchmark query returned 4 different answers in 4 tries.
> * Master: two independently generated tables of this shape skipped the whole 
> old file in every default-options try (100/100 and 330/330); with 
> {{PARQUET_READ_PAGE_INDEX=false}} they were wrong in 12/50 and 19/100 tries. 
> The 4-of-5-page skip showed up on another promoted column (INT32 decimal(9,2) 
> -> DECIMAL(12,2): 59719 instead of 99697, {{NumStatsFilteredPages: 4}}). A 
> 5-file table shaped like the benchmark returned 4 different wrong answers in 
> 40 tries, never the right one.
> * {{PARQUET_READ_PAGE_INDEX=false}} does *not* fix it in general, because 
> row-group statistics are decoded the same way.
> h3. Analysis (4.5.2 line numbers; master 3d4d47dc30 in brackets)
> * {{ColumnStatsReader::ReadFromString()}}, case {{TYPE_DECIMAL}} 
> ({{be/src/exec/parquet/parquet-column-stats.cc:164-177}} [167-180]), switches 
> on {{col_type_.GetByteSize()}}, i.e. the *table* type. For DECIMAL(20,2) it 
> calls {{DecodeDecimal<Decimal16Value>}} with the file's physical type INT64.
> * That ends in the generic 
> {{ParquetPlainEncoder::DecodeNoBoundsCheck<Decimal16Value, INT64>}} 
> ({{be/src/exec/parquet/parquet-common.h:468-478}} [469-479]), which does 
> {{memcpy(v, buffer, sizeof(InternalType))}}: it copies 16 bytes from an 
> 8-byte stat value. Its own comment says the generic template is only for no 
> conversion or narrowing conversions; there is no specialization for 
> Decimal8Value/INT32, Decimal16Value/INT32 or Decimal16Value/INT64.
> * The extra bytes come from whatever follows the stat value in memory, most 
> likely the {{std::string}} small-string buffer (not confirmed in a debugger). 
> This explains the nondeterminism. For an 8-byte stat, byte 8 is then the 
> string's NUL terminator, so the upper word can never be the all-ones sign 
> extension, which is why negative values fail almost always.
> * The data path is correct: {{CreateDecimalColumnReader()}} 
> ({{parquet-column-readers.cc:1677}} [1895]) decodes by the physical type and 
> widens with {{ParquetDataConverter}}.
> * {{ReadFromStringsBatch()}} (page-level min/max runtime filters) has the 
> same flaw, including for INT32 -> BIGINT and FLOAT -> DOUBLE 
> ({{DecodeBatchOneBoundsCheckFastTrack<int64_t>/<double>}}, 
> {{parquet-column-stats.cc:210/223}} [213/226]). It gives wrong join results 
> with explicit PAGE-level min/max filter settings, and also with *default* 
> query options when the widened column is the table's leading SORT BY column: 
> with {{MINMAX_FILTER_SORTED_COLUMNS=true}} (the default) Impala creates a 
> min/max filter for that column (IMPALA-10709), and the planner then runs it 
> with {{MINMAX_FILTER_THRESHOLD=0.5}} at PAGE level. Master, default options, 
> plain Parquet SORT BY table joined with {{VALUES (-3),(-2)}}: 0 rows instead 
> of 2 in 30/30 tries for INT32 read as BIGINT, 30/30 for FLOAT read as DOUBLE, 
> and 17/30 for DECIMAL(10,2) read as DECIMAL(20,2). Same-width controls were 
> correct. An Iceberg table promoted int -> long / float -> double with an 
> Impala SORT BY returned 2 instead of 4 in 20/20.
> * Reachable since IMPALA-7087 / IMPALA-8131 (commit 9d46255739, 4.1.0). 
> Before that, {{ParquetMetadataUtils::ValidateColumn()}} rejected INT32/INT64 
> decimals whose width differed from the table type; the table-width decode in 
> {{ReadFromString()}} is older. 4.1.0 through 4.4.x are likely affected but 
> were not tested. Master 3d4d47dc30 reproduces all steps above.
> * IMPALA-13625 (reading integer columns as DECIMAL, 5.0.0) reaches the same 
> decode (reproduced on master). Impala-written table (id BIGINT, c1 INT, c2 
> BIGINT) with values -5..4, then {{ALTER TABLE ... CHANGE c1 c1 
> DECIMAL(10,0)}} and {{CHANGE c2 c2 DECIMAL(19,0)}}: {{c1 < 0}} returns 0 
> (expected 5), {{c1 < 3}} returns 0 (expected 8), {{c2 < 0}} returns 0 
> (expected 5), 30/30 each. Correct with {{PARQUET_READ_STATISTICS=false}}.
> h3. Suggested fix
> Decode decimal stats at the file's width (Decimal4Value for INT32, 
> Decimal8Value for INT64) and then {{ConvertSlot()}} to the table width, like 
> the column readers do; use the BIGINT/DOUBLE fast track only for matching 
> physical types; add {{DCHECK_LE(sizeof(InternalType), byte_size)}} to the 
> generic {{DecodeNoBoundsCheck}}.
> h3. Workaround
> {{SET PARQUET_READ_STATISTICS=false}} (also fixes the min/max runtime filter 
> join cases), or rewrite the old data files with the new type (verified with 
> Spark {{rewrite_data_files}}). For the sorted-column join case only, 
> {{MINMAX_FILTER_SORTED_COLUMNS=false}} also helps. 
> {{PARQUET_READ_PAGE_INDEX=false}} is not a workaround.



--
This message was sent by Atlassian Jira
(v10.3.26#10030026)

Reply via email to