Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Add copy buttons to all
 blocks\n(function() {\n function addCopyButtons() {\n document.querySelectorAll('pre code').forEach(function(codeBlock) {\n if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;\n codeBlock.parentElement.setAttribute('data-copy-added', 'true');\n \n var btn = document.createElement('button');\n btn.textContent = 'Copy';\n btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';\n btn.onmouseover = function() { this.style.opacity = '1'; };\n btn.onmouseout = function() { this.style.opacity = '0.7'; };\n btn.onclick = function() {\n navigator.clipboard.writeText(codeBlock.textContent).then(function() {\n btn.textContent = 'Copied!';\n setTimeout(function() { btn.textContent = 'Copy'; }, 1500);\n });\n };\n codeBlock.parentElement.style.position = 'relative';\n codeBlock.parentElement.appendChild(btn);\n });\n }\n \n addCopyButtons();\n \n // Re-run on dynamic content\n var observer = new MutationObserver(addCopyButtons);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Add Copy Buttons to Code Blocks");
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Force GitHub README to respect dark mode\n(function() {\n var style = document.createElement('style');\n style.textContent = '\n .markdown-body {\n color-scheme: dark light;\n }\n .markdown-body pre { background: #161b22 !important; }\n .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; }\n .markdown-body table th, .markdown-body table td { border-color: #30363d !important; }\n .markdown-body img { background: #0d1117; }\n .markdown-body blockquote { border-left-color: #8b949e; }\n .markdown-body hr { border-color: #30363d; }\n ';\n document.head.appendChild(style);\n})();", "GitHub Dark Mode README Fix"); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Highlight search terms from Google/DuckDuckGo/Bing referrer\n(function() {\n var ref = document.referrer;\n var terms = [];\n \n if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) {\n var url = new URL(ref);\n var q = url.searchParams.get('q') || url.searchParams.get('p');\n if (q) {\n terms = q.split(/\\s+/).filter(function(t) { return t.length > 2; });\n }\n }\n \n if (terms.length === 0) return;\n \n var style = document.createElement('style');\n style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }';\n document.head.appendChild(style);\n \n function highlight(node) {\n if (node.nodeType === 3) { // text node\n var text = node.textContent;\n var found = false;\n terms.forEach(function(term) {\n var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\') + ')', 'gi');\n if (regex.test(text)) {\n found = true;\n var frag = document.createDocumentFragment();\n var parts = text.split(regex);\n parts.forEach(function(part, i) {\n if (i % 2 === 0) {\n frag.appendChild(document.createTextNode(part));\n } else {\n var span = document.createElement('span');\n span.className = 'userscript-highlight';\n span.textContent = part;\n frag.appendChild(span);\n }\n });\n node.parentNode.replaceChild(frag, node);\n }\n });\n } else if (node.nodeType === 1 && node.childNodes) { // element\n var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT'];\n if (!skipTags.includes(node.tagName)) {\n Array.from(node.childNodes).forEach(highlight);\n }\n }\n }\n \n highlight(document.body);\n \n // Re-highlight on dynamic content\n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1 || node.nodeType === 3) highlight(node);\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Highlight Search Terms"); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Strip utm_, fbclid, gclid, etc. from all links on page\n(function() {\n var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content',\n 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid',\n 'ref', 'ref_src', 'source', 'medium', 'campaign'];\n \n function cleanUrl(url) {\n try {\n var u = new URL(url, window.location.origin);\n var changed = false;\n trackingParams.forEach(function(p) {\n if (u.searchParams.has(p)) {\n u.searchParams.delete(p);\n changed = true;\n }\n });\n return changed ? u.toString() : url;\n } catch (e) {\n return url;\n }\n }\n \n function cleanLinks() {\n document.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n \n cleanLinks();\n \n var observer = new MutationObserver(function(mutations) {\n mutations.forEach(function(m) {\n m.addedNodes.forEach(function(node) {\n if (node.nodeType === 1) {\n if (node.tagName === 'A') cleanLinks();\n node.querySelectorAll('a[href]').forEach(function(a) {\n var clean = cleanUrl(a.href);\n if (clean !== a.href) a.href = clean;\n });\n }\n });\n });\n });\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "Remove Tracking Parameters from Links"); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + '
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Auto-enable theater mode on YouTube\n(function() {\n function tryTheater() {\n var btn = document.querySelector('button[aria-label=\"Theater mode\"], ytd-player #player button[title=\"Theater mode\"]');\n if (btn && !btn.classList.contains('activated')) {\n btn.click();\n }\n }\n \n // Try immediately\n tryTheater();\n \n // Try after navigation (SPA)\n var lastUrl = location.href;\n setInterval(function() {\n if (location.href !== lastUrl) {\n lastUrl = location.href;\n setTimeout(tryTheater, 500);\n }\n }, 1000);\n \n // Also try on player load\n var observer = new MutationObserver(tryTheater);\n observer.observe(document.body, { childList: true, subtree: true });\n})();", "YouTube Theater Mode Default"); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Remove or un-stick sticky/fixed headers that block content\n(function() {\n function unstick() {\n document.querySelectorAll('header, nav, [role=\"banner\"], .header, .navbar, .sticky, .fixed-top, [style*=\"position: fixed\"], [style*=\"position:sticky\"]').forEach(function(el) {\n if (el.style.position === 'fixed' || el.style.position === 'sticky' || \n getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') {\n el.style.position = 'static';\n el.style.top = 'auto';\n el.style.zIndex = 'auto';\n }\n });\n }\n \n unstick();\n \n var observer = new MutationObserver(unstick);\n observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] });\n})();", "Kill Sticky Headers"); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + '
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages

, 'i'); if (__m === '*' || __re.test(location.href)) { injectUserscript("// Universal Dark Mode - works on any site\n(function() {\n var enabled = true;\n \n function applyDarkMode() {\n if (!enabled) return;\n \n // Create style element if it doesn't exist\n var style = document.getElementById('universal-dark-mode-style');\n if (!style) {\n style = document.createElement('style');\n style.id = 'universal-dark-mode-style';\n document.head.appendChild(style);\n }\n \n // Dark mode CSS - inverts colors but preserves images/video\n style.textContent = '\n /* Invert everything except media */\n html {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #1a1a2e !important;\n }\n \n /* Restore images, videos, iframes, canvas */\n img, video, iframe, canvas, svg, picture, [style*=\"background-image\"] {\n filter: invert(1) hue-rotate(180deg) !important;\n }\n \n /* Preserve specific elements that should not be inverted */\n .no-dark-mode, .no-dark-mode *,\n [data-theme=\"light\"], [data-theme=\"light\"],\n .ace_editor, .ace_editor *,\n .CodeMirror, .CodeMirror *,\n .monaco-editor, .monaco-editor *,\n .markdown-body pre, .markdown-body pre *,\n .highlight, .highlight *,\n pre code, pre code * {\n filter: none !important;\n }\n \n /* Fix common UI elements */\n .modal, .popup, .dropdown-menu, .tooltip, .popover {\n filter: invert(1) hue-rotate(180deg) !important;\n background: #2d2d44 !important;\n border-color: #444 !important;\n }\n \n /* Scrollbars */\n ::-webkit-scrollbar { background: #1a1a2e !important; }\n ::-webkit-scrollbar-thumb { background: #444 !important; }\n ::-webkit-scrollbar-thumb:hover { background: #555 !important; }\n \n /* Selection */\n ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; }\n ';\n }\n \n function removeDarkMode() {\n var style = document.getElementById('universal-dark-mode-style');\n if (style) style.remove();\n }\n \n // Toggle with Alt+Shift+D\n document.addEventListener('keydown', function(e) {\n if (e.altKey && e.shiftKey && e.key === 'D') {\n e.preventDefault();\n enabled = !enabled;\n if (enabled) {\n applyDarkMode();\n console.log('[Universal Dark Mode] Enabled');\n } else {\n removeDarkMode();\n console.log('[Universal Dark Mode] Disabled');\n }\n }\n });\n \n // Apply on load\n applyDarkMode();\n \n // Re-apply on dynamic content\n var observer = new MutationObserver(function(mutations) {\n if (enabled && !document.getElementById('universal-dark-mode-style')) {\n applyDarkMode();\n }\n });\n observer.observe(document.head, { childList: true });\n \n console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle');\n})();", "Universal Dark Mode"); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })();
Skip to content

Repository files navigation

ParQuery

ParQuery is a query and aggregation framework for parquet files, enabling very fast big data aggregations on any hardware (from laptops to clusters). ParQuery is used in production environments to handle reporting and data retrieval queries over hundreds of files that each can contain billions of records.

Parquet is a light weight package that provides columnar, chunked data containers that can be compressed on-disk. It excels at storing and sequentially accessing large, numerical data sets.

The ParQuery framework provides methods to perform query and aggregation operations on Parquet containers using DuckDB (preferred) or PyArrow. It also contains helpers for serializing and de-serializing PyArrow tables, and writing DataFrames to Parquet. It is based on an OLAP-approach to aggregations with Dimensions and Measures.

Visualfabriq uses Parquet and ParQuery to reliably handle billions of records for our clients with real-time reporting and machine learning usage.

Performance: ParQuery automatically uses DuckDB when available for faster aggregations compared to PyArrow. DuckDB provides streaming execution with minimal memory footprint.

Dependencies:

  • Required: PyArrow (core functionality)
  • Optional:
    • Pandas, NumPy (DataFrame support)
    • Polars (efficient DataFrame I/O)
    • DuckDB (performance boost for aggregations)

Aggregation

A groupby with aggregation is easy to perform:

fromparqueryimportaggregate_pq# Assuming you have an example Parquet file called example.parquetpa_table=aggregate_pq(
'example.parquet',
groupby_col_list, # list of column names (dimensions) to group byaggregation_list, # list of measures and operations (see below)data_filter=data_filter, # optional filter conditions (see below)aggregate=True, # whether to aggregate results (True) or return raw filtered rows (False)as_df=None# None (auto), True (pandas DataFrame), or False (PyArrow Table)
)

Return Type (as_df parameter):

  • None (default): Auto-detects - returns pandas DataFrame if pandas is installed, otherwise PyArrow Table
  • True: Always returns pandas DataFrame (requires pandas to be installed)
  • False: Always returns PyArrow Table (no pandas needed)

Aggregation List Supported Operations

The aggregation_list contains the aggregation operations, which can be:

  • a straight forward list of columns (a sum is performed on each and stored in a column of the same name)
    • ['m1', 'm2', ...]
  • a list of lists where each list gives input column name and operation)
    • [['m1', 'sum'], ['m2', 'count'], ...]
  • a list of lists where each list additionally includes an output column name
    • [['m1', 'sum', 'm1_sum'], ['m1', 'count', 'm1_count'], ...]

Supported aggregation operations:

  • sum - Sum of values
  • mean / avg - Arithmetic mean (average)
  • std / stddev - Standard deviation
  • count - Count of non-null values
  • count_na - Count of null values
  • count_distinct - Count of unique values
  • sorted_count_distinct - Count of unique values (sorted)
  • min - Minimum value
  • max - Maximum value
  • one - Pick any value (useful for dimension columns)

Data Filter Supported Operations

The data_filter is optional and contains filters to be applied before the aggregation. Push-down filtering is applied to enhance performance using the parquet characteristics. It balances numexpr evaluation and Pandas filtering for optimal performance. It is a list that has a structure as follows:

data_filter = [[col1, operator, filter_values], ...]

We support the following operators:

  • in
  • not in
  • ==
  • !=
  • >
  • >=
  • <
  • <=

The first two operators assume the filter_values to be a list of values (e.g. [1, 2, ...]), the others for it to be a direct value (e.g. 1 or "A").

Examples

# Groupby column f0, perform a sum on column f2 and keep the output column with the same nameaggregate_pq('example.parquet', ['f0'], ['f2'])
# Groupby column f0, perform a count on column f2aggregate_pq('example.parquet', ['f0'], [['f2', 'count']])
# Groupby column f0, with a sum on f2 (output to 'f2_sum') and a mean on f2 (output to 'f2_mean')aggregate_pq('example.parquet', ['f0'], [['f2', 'sum', 'f2_sum'], ['f2', 'mean', 'f2_mean']])
# Groupby column f0, perform a sum on column f2, filtering column f1 on values 1 and 2, and where f0 equals 10aggregate_pq('example.parquet', ['f0'], ['f2'], data_filter=[['f1', 'in', [1, 2]], ['f0', '==', 10]])
# Return results as PyArrow Table (no pandas needed)pa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
# Return results as pandas DataFrame (requires pandas)df=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=True)
# Stream DuckDB results as Arrow record batches (requires DuckDB)fromparqueryimportaggregate_pq_streamreader=aggregate_pq_stream(
'example.parquet', ['f0'], ['f2'], batch_size=65_536
)
try:
forbatchinreader:
# Write each batch to your IPC or HTTP stream.write_batch(batch)
finally:
reader.close()

Engine Selection

ParQuery supports two execution engines with automatic selection:

DuckDB Engine (Recommended)

  • Faster than PyArrow for most workloads
  • Streaming execution with minimal memory footprint
  • Uses SQL-based query optimization
  • Install: pip install duckdb or uv pip install 'parquery[performance]'

PyArrow Engine (Fallback)

  • Pure Python with no external dependencies (beyond PyArrow)
  • Row-group level processing for memory efficiency
  • Automatic fallback when DuckDB is not installed

Usage:

# Auto-select engine (DuckDB if available, otherwise PyArrow)result=aggregate_pq('example.parquet', ['f0'], ['f2'])
# Force specific engineresult=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='duckdb')
result=aggregate_pq('example.parquet', ['f0'], ['f2'], engine='pyarrow')

Note: Both engines return identical results and support all the same operations. The engine parameter is primarily for performance tuning or debugging.

Streaming aggregation

aggregate_pq_stream() uses DuckDB's to_arrow_reader() API and yields pyarrow.RecordBatch objects incrementally. It is intended for HTTP/IPC responses where the complete result should not be materialized as a Python pa.Table. It requires DuckDB 1.5.5 or newer and currently uses the DuckDB engine only. The query may still require substantial internal memory for high-cardinality GROUP BY, ORDER BY, joins, or other blocking operators.

Always consume or close the returned iterator so its DuckDB connection, Arrow reader, file descriptor, and temporary spill directory are released.

Serialization and De-Serialization

The sender and receiver use the same Arrow IPC stream format. The API makes the direction explicit:

  • Send/write:serialize_pa_table_bytes(table) writes a table and returns a compressed pyarrow.Buffer.
  • Receive/read:open_pa_table_stream(source) opens an incoming stream and returns a lazy RecordBatchReader.
  • Receive/read all:deserialize_pa_table_bytes(source) reads an incoming stream into a complete pyarrow.Table.

All IPC streams written by this package use Zstandard compression. The reader functions transparently read both compressed and uncompressed IPC streams.

Sending: Binary Serialization (PyArrow Buffer)

Use for binary protocols, direct buffer transmission, or maximum efficiency. The function returns a pyarrow.Buffer directly, avoiding an extra copy:

fromparqueryimportserialize_pa_table_bytes, deserialize_pa_table_bytes, aggregate_pq# Create a serialized PyArrow buffer from an aggregation resultpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
buf=serialize_pa_table_bytes(pa_table) # pyarrow.Buffer# Receive and deserialize the complete tablepa_table=deserialize_pa_table_bytes(buf)
# Or receive incrementally for bounded-memory processing:fromparqueryimportopen_pa_table_streamwithopen_pa_table_stream(buf) asreader:
forbatchinreader:
process(batch)
# Convert to pandas if neededdf=pa_table.to_pandas()

Base64 Serialization (String)

Use for text-based protocols (JSON, XML, message queues like SQS):

fromparqueryimportserialize_pa_table_base64, deserialize_pa_table_base64# Serialize to base64 stringpa_table=aggregate_pq('example.parquet', ['f0'], ['f2'], as_df=False)
base64_str=serialize_pa_table_base64(pa_table)
# Deserialize from base64 stringpa_table=deserialize_pa_table_base64(base64_str)

Note: Base64 encoding adds ~33% size overhead compared to binary serialization.

Writing Parquet Files

ParQuery supports writing pandas DataFrames, Polars DataFrames, and PyArrow Tables to Parquet format:

fromparqueryimportdf_to_parquetimportpyarrowaspa# Write PyArrow Tabletable=pa.table({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(table, 'output.parquet')
# Write pandas DataFrame (if pandas installed)importpandasaspddf=pd.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet', chunksize=100000) # chunked writing for large DataFrames# Write Polars DataFrame (if polars installed)importpolarsaspldf=pl.DataFrame({'col1': [1, 2, 3], 'col2': ['a', 'b', 'c']})
df_to_parquet(df, 'output.parquet') # efficient zero-copy conversion via Arrow

Parquet writes use ZSTD compression by default for optimal file sizes.

Column Name Conversion

ParQuery provides utilities to handle column names with special characters (like hyphens) that aren't valid Python identifiers:

fromparqueryimportdf_to_natural_name, df_to_original_nameimportpyarrowaspa# Original table with hyphens in column namestable=pa.table({'col-1': [1, 2, 3], 'col-2': [4, 5, 6]})
# Convert hyphens to '_n_' for natural Python identifiersnatural_table=df_to_natural_name(table)
# Columns are now: ['col_n_1', 'col_n_2']# Convert back to original namesoriginal_table=df_to_original_name(natural_table)
# Columns are back to: ['col-1', 'col-2']

These functions work with pandas DataFrames, Polars DataFrames, and PyArrow Tables:

  • pandas: Modifies in-place and returns the DataFrame
  • Polars: Returns a new DataFrame (immutable)
  • PyArrow: Returns a new Table (immutable)

Debug Logging

ParQuery uses Python's standard logging module for debug output. To see debug messages, you need to configure both the library and your application's logging:

Basic Setup

importlogging# Enable debug logging for parquerylogging.basicConfig(level=logging.DEBUG)
# Or configure just parquery's loggerlogging.getLogger('parquery').setLevel(logging.DEBUG)
fromparqueryimportaggregate_pq# Now debug messages will be visibleresult=aggregate_pq(
'example.parquet',
['f0'],
['f2'],
debug=True# Enables debug log statements
)

AWS Lambda / CloudWatch

In AWS Lambda, stdout automatically goes to CloudWatch Logs. Configure logging at the module level:

importloggingimportos# Configure once per Lambda cold startLOG_LEVEL=os.getenv('LOG_LEVEL', 'INFO')
logging.getLogger('parquery').setLevel(LOG_LEVEL)
deflambda_handler(event, context):
fromparqueryimportaggregate_pq# Debug messages will appear in CloudWatch if LOG_LEVEL=DEBUGresult=aggregate_pq('file.parquet', ['col'], ['measure'], debug=True)
returnresult

Set the LOG_LEVEL environment variable in your Lambda configuration to control verbosity.

Production Recommendations

  • Development: Set LOG_LEVEL=DEBUG to see all processing details
  • Production: Use LOG_LEVEL=INFO or LOG_LEVEL=WARNING to reduce noise
  • The debug parameter must be True for debug messages to be logged
  • Logging level controls whether those messages actually appear

Installation

From PyPI (recommended)

# Install with PyArrow only (core functionality)
pip install parquery
# Install with DuckDB for better performance
pip install parquery[performance]
# Install with DataFrame support (pandas, numpy, polars)
pip install parquery[dataframes]
# Install with all optional dependencies
pip install parquery[optional]

From Source

Clone ParQuery to build and install it:

git clone https://github.com/visualfabriq/parquery.git
cd parquery
python setup.py build_ext --inplace
python setup.py install
# Or install with all optional dependencies
pip install -e .[optional]

Using uv (faster package manager)

# Core installation
uv pip install parquery
# With DuckDB for better performance
uv pip install 'parquery[performance]'# With DataFrame support
uv pip install 'parquery[dataframes]'# With all optional dependencies
uv pip install 'parquery[optional]'

Recommended: Install with [performance] extras to get DuckDB for faster aggregations.

Testing

# Run all tests
pytest tests
# Run with coverage
python -m coverage run -m pytest tests
python -m coverage xml -o cobertura.xml

About

ParQuery - A bquery compatible aggregation engine for Parquet

Resources

Stars

1 star

Watchers

8 watching

Forks

Releases

Packages

Used by

Contributors

Languages