// Copyright (c) 2023-2025 ParadeDB, Inc. // // This file is part of ParadeDB - Postgres for Search and Analytics // // This program is free software: you can redistribute it and/or modify // it under the terms of the GNU Affero General Public License as published by // the Free Software Foundation, either version 3 of the License, or // (at your option) any later version. // // This program is distributed in the hope that it will be useful // but WITHOUT ANY WARRANTY; without even the implied warranty of // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the // GNU Affero General Public License for more details. // // You should have received a copy of the GNU Affero General Public License // along with this program. If not, see . use pgrx::pg_sys::panic::ErrorReport; use pgrx::{ function_name, pg_sys, GucContext, GucFlags, GucRegistry, GucSetting, PgLogLevel, PgSqlErrorCode, }; use std::num::NonZeroUsize; /// Allows the user to toggle the use of our "ParadeDB Custom Scan". The default is `true`. static ENABLE_CUSTOM_SCAN: GucSetting = GucSetting::::new(true); /// Allows the user to enable or disable the FastFieldsExecState executor. Default is `true`. static ENABLE_FAST_FIELD_EXEC: GucSetting = GucSetting::::new(true); /// Allows the user to enable or disable the MixedFastFieldExecState executor. Default is `true`. static ENABLE_MIXED_FAST_FIELD_EXEC: GucSetting = GucSetting::::new(true); /// The number of fast-field columns below-which the MixedFastFieldExecState will be used, rather /// than the NormalExecState. The Mixed execution mode fetches data as column-oriented, whereas /// the Normal mode fetches data as row-oriented. /// /// Each fetch from a fast-field column costs one or two disk seeks, whereas a fetch of a row /// generally costs one. But with a wide enough row, fetching multiple columns might still result /// in better cache performance than fetching a row. static MIXED_FAST_FIELD_EXEC_COLUMN_THRESHOLD: GucSetting = GucSetting::::new(3); static MIXED_FAST_FIELD_EXEC_COLUMN_THRESHOLD_NAME: &str = "paradedb.mixed_fast_field_exec_column_threshold"; /// The `PER_TUPLE_COST` is an arbitrary value that needs to be really high. In fact, we default /// to one hundred million. /// /// The reason for this is we really do not want Postgres to choose a plan where the `@@@` operator /// is used in a sequential scan, filter, or recheck condition... unless of course there's no /// other way to solve the query. /// /// This value is a multiplier that Postgres applies to the estimated row count any given `@@@` /// query clause will return. In our case, higher is better. /// /// Our IAM impl has its own costing functions that don't use this GUC and provide sensible estimates /// for the overall IndexScan. That plus this help to persuade Postgres to use our IAM whenever /// it logically can. static PER_TUPLE_COST: GucSetting = GucSetting::::new(100_000_000.0); static TARGET_SEGMENT_COUNT: GucSetting = GucSetting::::new(0); pub fn init() { // Note that Postgres is very specific about the naming convention of variables. // They must be namespaced... we use 'paradedb.' below. GucRegistry::define_bool_guc( "paradedb.enable_custom_scan", "Enable ParadeDB's custom scan", "Enable ParadeDB's custom scan", &ENABLE_CUSTOM_SCAN, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_bool_guc( "paradedb.enable_fast_field_exec", "Enable StringFastFieldsExecState and NumericFastFieldsExecState executor", "Enable the StringFastFieldsExecState and NumericFastFieldsExecState executors for handling one string fast field or multiple numeric fast fields", &ENABLE_FAST_FIELD_EXEC, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_bool_guc( "paradedb.enable_mixed_fast_field_exec", "Enable MixedFastFieldExecState executor", "Enable the MixedFastFieldExecState executor for handling multiple string fast fields or mixed string/numeric fast fields", &ENABLE_MIXED_FAST_FIELD_EXEC, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_int_guc( MIXED_FAST_FIELD_EXEC_COLUMN_THRESHOLD_NAME, "Threshold of fetched columns below which MixedFastFieldExecState will be used.", "The number of fast-field columns below-which the MixedFastFieldExecState will be used, rather \ than the NormalExecState. The Mixed execution mode fetches data as column-oriented, whereas \ the Normal mode fetches data as row-oriented.", &MIXED_FAST_FIELD_EXEC_COLUMN_THRESHOLD, 0, i32::MAX, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_float_guc( "paradedb.per_tuple_cost", "Arbitrary multiplier for the cost of retrieving a tuple from a USING bm25 index outside of an IndexScan", "Default is 100,000,000.0. It is very expensive to use a USING bm25 index in the wrong query plan", &PER_TUPLE_COST, 0.0, f64::MAX, GucContext::Userset, GucFlags::default(), ); GucRegistry::define_int_guc( "paradedb.target_segment_count", "Set the target segment count for a CREATE INDEX/REINDEX statement", "Defaults to 0, which means the number of CPU cores will be used as the target segment count. Increasing the target segment count can be useful if max_parallel_workers_per_gather is greater than the CPU count.", &TARGET_SEGMENT_COUNT, 0, 1024, GucContext::Userset, GucFlags::default(), ); } pub fn enable_custom_scan() -> bool { ENABLE_CUSTOM_SCAN.get() } pub fn is_fast_field_exec_enabled() -> bool { ENABLE_FAST_FIELD_EXEC.get() } pub fn is_mixed_fast_field_exec_enabled() -> bool { ENABLE_MIXED_FAST_FIELD_EXEC.get() } pub fn mixed_fast_field_exec_column_threshold() -> usize { MIXED_FAST_FIELD_EXEC_COLUMN_THRESHOLD .get() .try_into() .unwrap_or_else(|e| { panic!("{MIXED_FAST_FIELD_EXEC_COLUMN_THRESHOLD_NAME} must be positive. {e}"); }) } pub fn per_tuple_cost() -> f64 { PER_TUPLE_COST.get() } // NB: These limits come from [`tantivy::index_writer::MEMORY_BUDGET_NUM_BYTES_MAX`], which is not publicly exposed mod limits { const MARGIN_IN_BYTES: usize = 1_000_000; // Size of the margin for the `memory_arena`. A segment is closed when the remaining memory // in the `memory_arena` goes below MARGIN_IN_BYTES. pub const MEMORY_BUDGET_NUM_BYTES_MIN: usize = 15 * MARGIN_IN_BYTES; // We impose the memory per thread to be no greater than 4GB as that's tantivy's limit pub const MEMORY_BUDGET_NUM_BYTES_MAX: usize = (4 * 1024 * 1024 * 1024) - MARGIN_IN_BYTES; } pub fn adjust_maintenance_work_mem(nlaunched: usize) -> NonZeroUsize { let nlaunched = nlaunched.max(1); let mwm_as_bytes = unsafe { pg_sys::maintenance_work_mem as usize } * 1024; let per_worker_budget = mwm_as_bytes / nlaunched; if per_worker_budget < limits::MEMORY_BUDGET_NUM_BYTES_MIN { ErrorReport::new( PgSqlErrorCode::ERRCODE_INSUFFICIENT_RESOURCES, "`maintenance_work_mem` is not high enough to give each parallel worker 15MB", function_name!(), ) .set_detail(format!("this query asked for {nlaunched} workers, so `maintenance_work_mem` must be at least {nlaunched} * 15MB")) .set_hint("`SET maintenance_work_mem = `") .report(PgLogLevel::ERROR); } else { pgrx::debug1!( "adjust_maintenance_work_mem: per_worker_budget: {per_worker_budget}, minimum: {}", limits::MEMORY_BUDGET_NUM_BYTES_MIN ); } // clamp the per_thread_budget to the min/max values let per_worker_budget = per_worker_budget.clamp( limits::MEMORY_BUDGET_NUM_BYTES_MIN, limits::MEMORY_BUDGET_NUM_BYTES_MAX - 1, ); NonZeroUsize::new(per_worker_budget * nlaunched).unwrap() } pub fn adjust_work_mem() -> NonZeroUsize { let wm_as_bytes = unsafe { pg_sys::work_mem as usize * 1024 }; let wm_as_bytes = wm_as_bytes.clamp( limits::MEMORY_BUDGET_NUM_BYTES_MIN, limits::MEMORY_BUDGET_NUM_BYTES_MAX - 1, ); NonZeroUsize::new(wm_as_bytes).unwrap() } pub fn target_segment_count() -> usize { if TARGET_SEGMENT_COUNT.get() > 0 { TARGET_SEGMENT_COUNT.get() as usize } else { std::thread::available_parallelism().unwrap().get() } } #[cfg(any(test, feature = "pg_test"))] #[pgrx::pg_schema] mod tests { use super::*; use pgrx::prelude::*; macro_rules! assert_approx_eq { ($a:expr, $b:expr, $percent:expr) => {{ let a = $a; let b = $b; let diff = if a > b { a - b } else { b - a }; let max_val = a.max(b); let max_diff = ((max_val as f64) * ($percent as f64 / 100.0)).ceil() as usize; assert!( diff <= max_diff, "assertion failed: `a = {}`, `b = {}` differ by more than {}% (allowed: {}, actual: {})", a, b, $percent, max_diff, diff ); }}; } #[pg_test] fn test_adjust_work_mem() { Spi::run("SET work_mem = '4MB';").unwrap(); assert_approx_eq!(adjust_work_mem().get(), 15 * 1_000_000, 1.0); Spi::run("SET work_mem = '1GB';").unwrap(); assert_approx_eq!(adjust_work_mem().get(), 1024 * 1024 * 1024, 1.0); } #[pg_test] fn test_adjust_maintenance_work_mem() { Spi::run("SET maintenance_work_mem = '16MB';").unwrap(); assert_approx_eq!(adjust_maintenance_work_mem(0).get(), 16 * 1024 * 1024, 1.0); assert_approx_eq!(adjust_maintenance_work_mem(1).get(), 16 * 1024 * 1024, 1.0); assert!(std::panic::catch_unwind(|| adjust_maintenance_work_mem(2)).is_err()); assert!(std::panic::catch_unwind(|| adjust_maintenance_work_mem(10)).is_err()); Spi::run("SET maintenance_work_mem = '1GB';").unwrap(); assert_approx_eq!( adjust_maintenance_work_mem(0).get(), 1024 * 1024 * 1024, 1.0 ); assert_approx_eq!( adjust_maintenance_work_mem(1).get(), 1024 * 1024 * 1024, 1.0 ); assert_approx_eq!( adjust_maintenance_work_mem(2).get(), 1024 * 1024 * 1024, 1.0 ); assert_approx_eq!( adjust_maintenance_work_mem(10).get(), 1024 * 1024 * 1024, 1.0 ); assert!(std::panic::catch_unwind(|| adjust_maintenance_work_mem(128)).is_err()); } }