-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Python: Add limit parameter to table scan #7163
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 13 commits
7ccabd4
d361c74
6842abe
30ec1f1
698253c
82d663c
e20dfe9
69ef3b8
56cf45a
e6f5f22
2360232
d2be213
1cba012
0073ef2
01d7f7c
77580c6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -24,9 +24,11 @@ | |
| """ | ||
| from __future__ import annotations | ||
|
|
||
| import multiprocessing | ||
| import os | ||
| from functools import lru_cache | ||
| from multiprocessing.pool import ThreadPool | ||
| from multiprocessing.sharedctypes import Synchronized | ||
| from typing import ( | ||
| TYPE_CHECKING, | ||
| Any, | ||
|
|
@@ -42,7 +44,7 @@ | |
|
|
||
| import pyarrow as pa | ||
| import pyarrow.compute as pc | ||
| import pyarrow.parquet as pq | ||
| import pyarrow.dataset as ds | ||
| from pyarrow.fs import ( | ||
| FileInfo, | ||
| FileSystem, | ||
|
|
@@ -491,14 +493,19 @@ def _file_to_table( | |
| projected_schema: Schema, | ||
| projected_field_ids: Set[int], | ||
| case_sensitive: bool, | ||
| rows_counter: Synchronized[int], | ||
| limit: Optional[int] = None, | ||
| ) -> Optional[pa.Table]: | ||
| _, path = PyArrowFileIO.parse_location(task.file.file_path) | ||
| if limit and rows_counter.value >= limit: | ||
| return None | ||
|
|
||
| # Get the schema | ||
| with fs.open_input_file(path) as fout: | ||
| parquet_schema = pq.read_schema(fout) | ||
| _, path = PyArrowFileIO.parse_location(task.file.file_path) | ||
| arrow_format = ds.ParquetFileFormat(pre_buffer=True, buffer_size=(ONE_MEGABYTE * 8)) | ||
| with fs.open_input_file(path) as fin: | ||
| fragment = arrow_format.make_fragment(fin) | ||
| physical_schema = fragment.physical_schema | ||
| schema_raw = None | ||
| if metadata := parquet_schema.metadata: | ||
| if metadata := physical_schema.metadata: | ||
| schema_raw = metadata.get(ICEBERG_SCHEMA) | ||
| if schema_raw is None: | ||
| raise ValueError( | ||
|
|
@@ -517,15 +524,22 @@ def _file_to_table( | |
| if file_schema is None: | ||
| raise ValueError(f"Missing Iceberg schema in Metadata for file: {path}") | ||
|
|
||
| arrow_table = pq.read_table( | ||
| source=fout, | ||
| schema=parquet_schema, | ||
| pre_buffer=True, | ||
| buffer_size=8 * ONE_MEGABYTE, | ||
| filters=pyarrow_filter, | ||
| fragment_scanner = ds.Scanner.from_fragment( | ||
| fragment=fragment, | ||
| schema=physical_schema, | ||
| filter=pyarrow_filter, | ||
| columns=[col.name for col in file_project_schema.columns], | ||
| ) | ||
|
|
||
| if limit: | ||
| arrow_table = fragment_scanner.head(limit) | ||
| with rows_counter.get_lock(): | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can remove this lock because we already did the expensive work. This will make the code a bit simpler and avoid locking.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The official python documentation for multiprocessing.Value suggests that non atomic operations like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ah, thanks for the explanation. Currently, we don't do multi-processing, but multi-threading. I did some extensive testing and noticed that multi-processing wasn't substantially faster than multithreading. Probably because most time is spent in fetching the files, and reading the data, which all happens in Arrow, which bypasses the GIL.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think you made the right choice going for multi-threading because the task is IO-bound and there are potentially thousands of tasks that need to be executed. Even though we use multi-threading which is bound to the GIL some operations might still not be safe (like
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thanks for clearing this up! |
||
| if rows_counter.value >= limit: | ||
| return None | ||
| rows_counter.value += len(arrow_table) | ||
| else: | ||
| arrow_table = fragment_scanner.to_table() | ||
|
|
||
| # If there is no data, we don't have to go through the schema | ||
| if len(arrow_table) > 0: | ||
| return to_requested_schema(projected_schema, file_project_schema, arrow_table) | ||
|
|
@@ -534,7 +548,12 @@ def _file_to_table( | |
|
|
||
|
|
||
| def project_table( | ||
| tasks: Iterable[FileScanTask], table: Table, row_filter: BooleanExpression, projected_schema: Schema, case_sensitive: bool | ||
| tasks: Iterable[FileScanTask], | ||
| table: Table, | ||
| row_filter: BooleanExpression, | ||
| projected_schema: Schema, | ||
| case_sensitive: bool, | ||
| limit: Optional[int] = None, | ||
| ) -> pa.Table: | ||
| """Resolves the right columns based on the identifier | ||
|
|
||
|
|
@@ -570,23 +589,30 @@ def project_table( | |
| id for id in projected_schema.field_ids if not isinstance(projected_schema.find_type(id), (MapType, ListType)) | ||
| }.union(extract_field_ids(bound_row_filter)) | ||
|
|
||
| rows_counter = multiprocessing.Value("i", 0) | ||
|
|
||
| with ThreadPool() as pool: | ||
| tables = [ | ||
| table | ||
| for table in pool.starmap( | ||
| func=_file_to_table, | ||
| iterable=[(fs, task, bound_row_filter, projected_schema, projected_field_ids, case_sensitive) for task in tasks], | ||
| iterable=[ | ||
| (fs, task, bound_row_filter, projected_schema, projected_field_ids, case_sensitive, rows_counter, limit) | ||
| for task in tasks | ||
| ], | ||
| chunksize=None, # we could use this to control how to materialize the generator of tasks (we should also make the expression above lazy) | ||
| ) | ||
| if table is not None | ||
| ] | ||
|
|
||
| if len(tables) > 1: | ||
| return pa.concat_tables(tables) | ||
| final_table = pa.concat_tables(tables) | ||
| elif len(tables) == 1: | ||
| return tables[0] | ||
| final_table = tables[0] | ||
| else: | ||
| return pa.Table.from_batches([], schema=schema_to_pyarrow(projected_schema)) | ||
| final_table = pa.Table.from_batches([], schema=schema_to_pyarrow(projected_schema)) | ||
|
|
||
| return final_table.slice(0, limit) | ||
|
|
||
|
|
||
| def to_requested_schema(requested_schema: Schema, file_schema: Schema, table: pa.Table) -> pa.Table: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -177,6 +177,12 @@ class DataFile(Record): | |
| sort_order_id: Optional[int] | ||
| spec_id: Optional[int] | ||
|
|
||
| def __setattr__(self, name: str, value: Any) -> None: | ||
| # The file_format is written as a string, so we need to cast it to the Enum | ||
| if name == "file_format": | ||
| value = FileFormat[value] | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Just curious. Is this related to changes in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was introduced by merging #7033 |
||
| super().__setattr__(name, value) | ||
|
|
||
| def __init__(self, *data: Any, **named_data: Any) -> None: | ||
| super().__init__(*data, **{"struct": DATA_FILE_TYPE, **named_data}) | ||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Works for now 👍🏻