diff --git a/.clangd b/.clangd new file mode 100644 index 0000000000000..490220f56a7c0 --- /dev/null +++ b/.clangd @@ -0,0 +1,43 @@ +Diagnostics: + MissingIncludes: None +InlayHints: + Enabled: true + ParameterNames: true + DeducedTypes: true +CompileFlags: + CompilationDatabase: build/ # Search build/ directory for compile_commands.json + Remove: [ -Werror ] + Add: + - -DDEBUG + - -DLOCAL + - -DPGDLLIMPORT= + - -DPIC + - -O2 + - -Wall + - -Wcast-function-type + - -Wconversion + - -Wdeclaration-after-statement + - -Wendif-labels + - -Werror=vla + - -Wextra + - -Wfloat-equal + - -Wformat-security + - -Wimplicit-fallthrough=3 + - -Wmissing-format-attribute + - -Wmissing-prototypes + - -Wno-format-truncation + - -Wno-sign-conversion + - -Wno-stringop-truncation + - -Wno-unused-const-variable + - -Wpointer-arith + - -Wshadow + - -Wshadow=compatible-local + - -fPIC + - -fexcess-precision=standard + - -fno-strict-aliasing + - -fvisibility=hidden + - -fwrapv + - -g + - -std=c11 + - -I. + - -I../../../../src/include diff --git a/.envrc b/.envrc new file mode 100644 index 0000000000000..cf781a39df685 --- /dev/null +++ b/.envrc @@ -0,0 +1,10 @@ +# Inherit shared CFLAGS from ~/ws/postgres/.envrc, load this worktree's +# flake, then layer the postgres agent context (helpers need flake's PATH + +# write into this worktree's dir). +source_up +watch_file flake.nix +use flake +pg_cflags +use project_steering postgresql aws c +use git_policy allow-force-push +use project_mcp postgresq diff --git a/.gdbinit b/.gdbinit new file mode 100644 index 0000000000000..854e5ecbaf69c --- /dev/null +++ b/.gdbinit @@ -0,0 +1,156 @@ +# HOT Indexed Updates — GDB breakpoints for code review +# +# Usage: gdb -x .gdbinit +# Or from gdb: source .gdbinit +# +# These breakpoints cover the major code paths introduced or modified by +# the HOT indexed updates patch series. They are organized by subsystem +# to make it easy to enable/disable groups during debugging. +# +# Tip: to skip to a specific subsystem, disable all then enable selectively: +# disable breakpoints +# enable 1 2 3 # just the update-decision group + +# ========================================================================= +# 1. UPDATE DECISION — heap_update() HOT/HOT-indexed/non-HOT choice +# src/backend/access/heap/heapam.c +# ========================================================================= + +# Main entry: heap_update +break heapam.c:3210 + +# HOT decision block: pure HOT vs HOT indexed vs non-HOT +# Line 4019: pure HOT (no indexed columns changed) +# Line 4024: HOT indexed path (non-catalog, some indexed columns changed) +# Line 4031: predict augmented tuple size +# Line 4033: size+space check before creating augmented tuple +break heapam.c:4019 +break heapam.c:4024 +break heapam.c:4033 + +# Set HEAP_INDEXED_UPDATED flag on new tuple before page insertion +break heapam.c:4101 + +# Restore HEAP_INDEXED_UPDATED on old tuple (only if it previously had it) +break heapam.c:4147 + +# ========================================================================= +# 2. TUPLE CREATION — building the augmented tuple with embedded bitmap +# src/backend/access/heap/heapam.c +# ========================================================================= + +# Predict augmented tuple size (returns 0 if t_hoff would overflow) +break heap_hot_indexed_tuple_size + +# Create augmented tuple with embedded modified-column bitmap +break heap_hot_indexed_create_tuple + +# Serialize Bitmapset into raw bytes in tuple header +break heap_hot_indexed_serialize_bitmap + +# ========================================================================= +# 3. BITMAP UTILITIES — raw bitmap operations for chain following +# src/backend/access/heap/heapam.c +# ========================================================================= + +# Compute raw bitmap byte size from natts +break heap_hot_indexed_bitmap_raw_size + +# Check if tuple header has room for bitmap between null bitmap and data +break heap_hot_indexed_has_bitmap_space + +# Read HOT indexed bitmap from tuple header (returns Bitmapset) +break heap_hot_indexed_read_bitmap + +# Fast overlap check: does tuple's raw bitmap overlap with indexed_attrs? +break heap_hot_indexed_bitmap_overlaps_raw + +# OR a tuple's raw bitmap into an accumulator buffer +break heap_hot_indexed_bitmap_or_raw + +# Check if accumulated raw bitmap overlaps with indexed_attrs +break heap_hot_indexed_accum_overlaps + +# Merge bitmaps from dead tuples into a target tuple on the page +break heap_hot_indexed_merge_bitmaps_raw + +# Deserialize raw bytes back to Bitmapset +break heap_hot_indexed_deserialize_bitmap + +# ========================================================================= +# 4. INDEX SCAN — HOT chain following with stale-entry detection +# src/backend/access/heap/heapam_indexscan.c +# ========================================================================= + +# Main HOT chain search with indexed update awareness +break heap_hot_search_buffer + +# Redirect-with-data: initialize bitmap accumulator from collapsed redirect +break heapam_indexscan.c:182 + +# Accumulate bitmap from INDEXED_UPDATED tuple in chain +break heapam_indexscan.c:250 + +# Stale entry detection: accumulated bitmap overlaps this index's attrs +break heapam_indexscan.c:297 + +# ========================================================================= +# 5. INDEX SCAN SETUP — indexed_attrs bitmap computation +# src/backend/access/index/indexam.c +# ========================================================================= + +# Compute indexed_attrs for HOT indexed update chain following +break indexam.c:299 + +# ========================================================================= +# 6. INDEX INSERTION — skip unchanged indexes for HOT indexed updates +# src/backend/executor/execIndexing.c +# ========================================================================= + +# Entry: insert/update index tuples +break ExecInsertIndexTuples + +# Index skip decision: skip indexes whose attrs don't overlap modified set +break execIndexing.c:370 + +# ========================================================================= +# 7. PRUNING — chain collapsing and redirect-with-data +# src/backend/access/heap/pruneheap.c +# ========================================================================= + +# Main prune function +break heap_page_prune_and_freeze + +# Per-chain pruning entry +break heap_prune_chain + +# Chain collapsing: collect bitmaps from dead INDEXED_UPDATED intermediates +break pruneheap.c:1802 + +# OR dead tuple bitmaps into combined bitmap +break pruneheap.c:1836 + +# Record redirect-with-data for execute phase +break pruneheap.c:1863 + +# Execute phase: apply redirect-with-data entries on the page +break pruneheap.c:1287 + +# ========================================================================= +# 8. WAL REPLAY — recovery of HOT indexed updates +# src/backend/access/heap/heapam_xlog.c +# ========================================================================= + +# WAL replay for XLOG_HEAP2_INDEXED_UPDATE +break heap_xlog_indexed_update + +# ========================================================================= +# 9. WAL LOGGING — writing HOT indexed update records +# src/backend/access/heap/heapam.c +# ========================================================================= + +# WAL logging for heap updates (handles indexed_update flag) +break log_heap_update + +# Serialize redirect-with-data into WAL record (pruneheap.c) +break pruneheap.c:2936 diff --git a/contrib/btree_gist/expected/timestamp.out b/contrib/btree_gist/expected/timestamp.out index 0d94f2f245cea..6589fc8125fc2 100644 --- a/contrib/btree_gist/expected/timestamp.out +++ b/contrib/btree_gist/expected/timestamp.out @@ -89,3 +89,37 @@ SELECT a, a <-> '2004-10-26 08:55:08' FROM timestamptmp ORDER BY a <-> '2004-10- Mon Nov 29 20:12:43 2004 | @ 34 days 11 hours 17 mins 35 secs (3 rows) +-- An ordering scan that hands an exact point distance to the target list +-- while another ORDER BY key, here an interval, is of a by-reference type. +CREATE TABLE timestamppt (p point, a timestamp); +INSERT INTO timestamppt + SELECT point(i % 7, i % 11), '2004-10-26 08:55:08'::timestamp + i * interval '37 min' + FROM generate_series(1, 600) i; +CREATE INDEX timestamppt_idx ON timestamppt USING gist (p, a); +SET enable_indexonlyscan=off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT p <-> point(3, 4) AS d, a <-> '2004-10-26 08:55:08' AS da + FROM timestamppt + ORDER BY p <-> point(3, 4), a <-> '2004-10-26 08:55:08' LIMIT 3; + QUERY PLAN +---------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: ((p <-> '(3,4)'::point)), ((a <-> 'Tue Oct 26 08:55:08 2004'::timestamp without time zone)) + -> Index Scan using timestamppt_idx on public.timestamppt + Output: ((p <-> '(3,4)'::point)), (a <-> 'Tue Oct 26 08:55:08 2004'::timestamp without time zone) + Order By: ((timestamppt.p <-> '(3,4)'::point) AND (timestamppt.a <-> 'Tue Oct 26 08:55:08 2004'::timestamp without time zone)) + Order By Values Used: 1 +(6 rows) + +SELECT p <-> point(3, 4) AS d, a <-> '2004-10-26 08:55:08' AS da + FROM timestamppt + ORDER BY p <-> point(3, 4), a <-> '2004-10-26 08:55:08' LIMIT 3; + d | da +---+--------------------------- + 0 | @ 1 day 12 hours 23 mins + 0 | @ 3 days 11 hours 52 mins + 0 | @ 5 days 11 hours 21 mins +(3 rows) + +RESET enable_indexonlyscan; +DROP TABLE timestamppt; diff --git a/contrib/btree_gist/sql/timestamp.sql b/contrib/btree_gist/sql/timestamp.sql index 95effebfc47a7..216b0211a3611 100644 --- a/contrib/btree_gist/sql/timestamp.sql +++ b/contrib/btree_gist/sql/timestamp.sql @@ -35,3 +35,21 @@ SELECT count(*) FROM timestamptmp WHERE a > '2004-10-26 08:55:08'::timestamp; EXPLAIN (COSTS OFF) SELECT a, a <-> '2004-10-26 08:55:08' FROM timestamptmp ORDER BY a <-> '2004-10-26 08:55:08' LIMIT 3; SELECT a, a <-> '2004-10-26 08:55:08' FROM timestamptmp ORDER BY a <-> '2004-10-26 08:55:08' LIMIT 3; + +-- An ordering scan that hands an exact point distance to the target list +-- while another ORDER BY key, here an interval, is of a by-reference type. +CREATE TABLE timestamppt (p point, a timestamp); +INSERT INTO timestamppt + SELECT point(i % 7, i % 11), '2004-10-26 08:55:08'::timestamp + i * interval '37 min' + FROM generate_series(1, 600) i; +CREATE INDEX timestamppt_idx ON timestamppt USING gist (p, a); +SET enable_indexonlyscan=off; +EXPLAIN (VERBOSE, COSTS OFF) +SELECT p <-> point(3, 4) AS d, a <-> '2004-10-26 08:55:08' AS da + FROM timestamppt + ORDER BY p <-> point(3, 4), a <-> '2004-10-26 08:55:08' LIMIT 3; +SELECT p <-> point(3, 4) AS d, a <-> '2004-10-26 08:55:08' AS da + FROM timestamppt + ORDER BY p <-> point(3, 4), a <-> '2004-10-26 08:55:08' LIMIT 3; +RESET enable_indexonlyscan; +DROP TABLE timestamppt; diff --git a/doc/src/sgml/indexam.sgml b/doc/src/sgml/indexam.sgml index f48da3185307c..ee20c47750adb 100644 --- a/doc/src/sgml/indexam.sgml +++ b/doc/src/sgml/indexam.sgml @@ -157,6 +157,7 @@ typedef struct IndexAmRoutine ambulkdelete_function ambulkdelete; amvacuumcleanup_function amvacuumcleanup; amcanreturn_function amcanreturn; /* can be NULL */ + amcanreturnorderby_function amcanreturnorderby; /* can be NULL */ amcostestimate_function amcostestimate; amgettreeheight_function amgettreeheight; /* can be NULL */ amoptions_function amoptions; @@ -481,6 +482,32 @@ amcanreturn (Relation indexRelation, int attno); +bool +amcanreturnorderby (Oid opfamily, Oid opcintype, Oid orderbyop); + + Check whether, for an ordering scan using ordering operator + orderbyop on a column of the operator class given + by opfamily and opcintype, + the values the access method reports in + scan->xs_orderbyvals with + xs_recheckorderby false are exactly the values the + operator would return for the heap tuple. If so, the executor may return + them for a target list entry that is the same expression as the + ORDER BY key, instead of evaluating the operator again. + The answer is for that operator class and operator, not for the access + method as a whole: an operator class whose distances merely sort like the + operator's result, or differ from it in any digit, or depend on anything + other than the indexed value, must answer false. The planner calls this + while building the plan; it is only consulted for an + float8 or float4 ORDER BY + expression, and for float4 it is the float4 + rounding of the distance the access method reports that must equal the + operator's result. If the amcanreturnorderby + field is set to NULL, no substitution is made. + + + + void amcostestimate (PlannerInfo *root, IndexPath *path, @@ -989,6 +1016,16 @@ amtranslatecmptype (CompareType cmptype, Oid opfamily, Oid opcintype); of that form can be passed to amrescan as described previously. + + Such an access method returns each entry's ordering values in + scan->xs_orderbyvals, and sets + scan->xs_recheckorderby to true when those values are + only lower bounds that the executor must recompute from the heap + tuple. When xs_recheckorderby is false the values + need only sort in the same order as the ordering operator's results; + they need not equal them. See amcanreturnorderby + below for how an access method can promise that they do. + diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000000000..0444ccd449e02 --- /dev/null +++ b/flake.lock @@ -0,0 +1,134 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "flake-utils_2": { + "inputs": { + "systems": "systems_2" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "libumem": { + "inputs": { + "flake-utils": "flake-utils_2", + "nixpkgs": [ + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1790620607, + "narHash": "sha256-KJMbA7HNuXhvtgmaMuZSXLf8wqarWHrlUlsSb+FzOLw=", + "ref": "refs/tags/v4.1.0", + "rev": "5f6f9efe505e0efb0697464d3b22443f6b2832c8", + "revCount": 1006, + "type": "git", + "url": "https://codeberg.org/gregburd/libumem" + }, + "original": { + "ref": "refs/tags/v4.1.0", + "type": "git", + "url": "https://codeberg.org/gregburd/libumem" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1767313136, + "narHash": "sha256-16KkgfdYqjaeRGBaYsNrhPRRENs0qzkQVUooNHtoy2w=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "ac62194c3917d5f474c1a844b6fd6da2db95077d", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixos-25.05", + "repo": "nixpkgs", + "type": "github" + } + }, + "nixpkgs-unstable": { + "locked": { + "lastModified": 1785141334, + "narHash": "sha256-kh35kIx7el4Jk8Ki3BH9/Pn1eZYSYLJ6LMALos0zOy0=", + "owner": "nixos", + "repo": "nixpkgs", + "rev": "38a4887411571457d700c51c64a6e49ead2ed5ab", + "type": "github" + }, + "original": { + "owner": "nixos", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "libumem": "libumem", + "nixpkgs": "nixpkgs", + "nixpkgs-unstable": "nixpkgs-unstable" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + }, + "systems_2": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000000000..4a25f8ff6ee40 --- /dev/null +++ b/flake.nix @@ -0,0 +1,63 @@ +{ + description = "PostgreSQL development environment"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.05"; + nixpkgs-unstable.url = "github:nixos/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + + # libumem: LD_PRELOAD malloc, the umem/umemctl CLI, and GDB/LLDB helpers. + # Track the latest tagged release from the repo. Bump the ref to the new + # tag and run `nix flake update libumem` to pick up a newer release; do NOT + # point this at a branch, so the dev shell always builds a released + # version rather than an arbitrary master commit. + # + # Follow the stable nixpkgs so libumem links the same glibc the dev + # shell's base tools (bash, coreutils) use; following unstable produced a + # glibc 2.42-vs-2.40 GLIBC_ABI mismatch when LD_PRELOAD'ing into them. + libumem = { + url = "git+https://codeberg.org/gregburd/libumem?ref=refs/tags/v4.1.0"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = { + self, + nixpkgs, + nixpkgs-unstable, + flake-utils, + libumem, + }: + flake-utils.lib.eachDefaultSystem ( + system: let + pkgs = import nixpkgs { + inherit system; + config.allowUnfree = true; + }; + pkgs-unstable = import nixpkgs-unstable { + inherit system; + config.allowUnfree = true; + }; + + libumemPkg = libumem.packages.${system}.libumem; + + shellConfig = import ./shell.nix { + inherit pkgs pkgs-unstable system libumemPkg; + }; + in { + formatter = pkgs.alejandra; + devShells = { + default = shellConfig.devShell; + gcc = shellConfig.devShell; + clang = shellConfig.clangDevShell; + }; + + packages = { + inherit (shellConfig) gdbConfig flameGraphScript pgbenchScript; + libumem = libumemPkg; + }; + + environment.localBinInPath = true; + } + ); +} diff --git a/glibc-no-fortify-warning.patch b/glibc-no-fortify-warning.patch new file mode 100644 index 0000000000000..4657a12adbcc5 --- /dev/null +++ b/glibc-no-fortify-warning.patch @@ -0,0 +1,24 @@ +From 130c231020f97e5eb878cc9fdb2bd9b186a5aa04 Mon Sep 17 00:00:00 2001 +From: Greg Burd +Date: Fri, 24 Oct 2025 11:58:24 -0400 +Subject: [PATCH] no warnings with -O0 and fortify source please + +--- + include/features.h | 1 - + 1 file changed, 1 deletion(-) + +diff --git a/include/features.h b/include/features.h +index 673c4036..a02c8a3f 100644 +--- a/include/features.h ++++ b/include/features.h +@@ -432,7 +432,6 @@ + + #if defined _FORTIFY_SOURCE && _FORTIFY_SOURCE > 0 + # if !defined __OPTIMIZE__ || __OPTIMIZE__ <= 0 +-# warning _FORTIFY_SOURCE requires compiling with optimization (-O) + # elif !__GNUC_PREREQ (4, 1) + # warning _FORTIFY_SOURCE requires GCC 4.1 or later + # elif _FORTIFY_SOURCE > 2 && (__glibc_clang_prereq (9, 0) \ +-- +2.50.1 + diff --git a/pg-aliases.sh b/pg-aliases.sh new file mode 100644 index 0000000000000..7979ec292620a --- /dev/null +++ b/pg-aliases.sh @@ -0,0 +1,706 @@ +# PostgreSQL Development Aliases + +# ============================================================ +# Build helpers shared by every variant. +# ============================================================ +pg_clean_for_compiler() { + local current_compiler="$(basename $CC)" + local build_dir="${1:-$PG_BUILD_DIR}" + + if [ -f "$build_dir/compile_commands.json" ]; then + local last_compiler=$(grep -o '/[^/]*/bin/[gc]cc\|/[^/]*/bin/clang' "$build_dir/compile_commands.json" | head -1 | xargs basename 2>/dev/null || echo "unknown") + + if [ "$last_compiler" != "$current_compiler" ] && [ "$last_compiler" != "unknown" ]; then + echo "Detected compiler change from $last_compiler to $current_compiler" + echo "Cleaning build directory..." + trash "$build_dir" 2>/dev/null || rm -rf "$build_dir" + mkdir -p "$build_dir" + fi + fi + + mkdir -p "$build_dir" + echo "$current_compiler" >"$build_dir/.compiler_used" +} + +# ============================================================ +# Core PostgreSQL commands (default/debug build) +# ============================================================ +alias pg-setup=' + if [ -z "$PERL_CORE_DIR" ]; then + echo "Error: Could not find perl CORE directory" >&2 + return 1 + fi + + pg_clean_for_compiler "$PG_BUILD_DIR" + + echo "=== PostgreSQL Build Configuration ===" + echo "Compiler: $CC" + echo "LLVM: $(llvm-config --version 2>/dev/null || echo disabled)" + echo "Source: $PG_SOURCE_DIR" + echo "Build: $PG_BUILD_DIR" + echo "Install: $PG_INSTALL_DIR" + echo "======================================" + + env CFLAGS="-I$PERL_CORE_DIR $CFLAGS" \ + LDFLAGS="-L$PERL_CORE_DIR -lperl $LDFLAGS" \ + meson setup $MESON_EXTRA_SETUP \ + --reconfigure \ + -Doptimization=g \ + -Ddebug=true \ + -Db_sanitize=none \ + -Db_lundef=false \ + -Dlz4=enabled \ + -Dzstd=enabled \ + -Dllvm=disabled \ + -Dplperl=enabled \ + -Dplpython=enabled \ + -Dpltcl=enabled \ + -Dlibxml=enabled \ + -Duuid=e2fs \ + -Dlibxslt=enabled \ + -Dssl=openssl \ + -Dldap=disabled \ + -Dcassert=true \ + -Dtap_tests=enabled \ + -Dinjection_points=true \ + -Ddocs_pdf=enabled \ + -Ddocs_html_style=website \ + --prefix="$PG_INSTALL_DIR" \ + "$PG_BUILD_DIR" \ + "$PG_SOURCE_DIR"' + +alias pg-compdb='compdb -p build/ list > compile_commands.json' +alias pg-build='meson compile -C "$PG_BUILD_DIR"' +alias pg-install='meson install -C "$PG_BUILD_DIR"' +alias pg-test='meson test -q --print-errorlogs -C "$PG_BUILD_DIR"' + +# Clean commands +alias pg-clean='ninja -C "$PG_BUILD_DIR" clean' +alias pg-full-clean='trash "$PG_BUILD_DIR" "$PG_INSTALL_DIR" 2>/dev/null || rm -rf "$PG_BUILD_DIR" "$PG_INSTALL_DIR"; echo "Build and install directories cleaned"' + +# Database management +alias pg-init='trash "$PG_DATA_DIR" 2>/dev/null || rm -rf "$PG_DATA_DIR"; "$PG_INSTALL_DIR/bin/initdb" --debug --no-clean "$PG_DATA_DIR"' + +alias pg-start='ulimit -c unlimited && "$PG_INSTALL_DIR/bin/postgres" -D "$PG_DATA_DIR" -k "$PG_DATA_DIR"' + +alias pg-stop='pkill -f "postgres.*-D.*$PG_DATA_DIR" || true' +alias pg-restart='pg-stop && sleep 2 && pg-start' +alias pg-status='pgrep -f "postgres.*-D.*$PG_DATA_DIR" && echo "PostgreSQL is running" || echo "PostgreSQL is not running"' + +# Client connections +alias pg-psql='"$PG_INSTALL_DIR/bin/psql" -h "$PG_DATA_DIR" postgres' +alias pg-createdb='"$PG_INSTALL_DIR/bin/createdb" -h "$PG_DATA_DIR"' +alias pg-dropdb='"$PG_INSTALL_DIR/bin/dropdb" -h "$PG_DATA_DIR"' + +# ============================================================ +# Debugger attachments +# ============================================================ +alias pg-debug-gdb='gdb -x "$GDBINIT" -x .gdbinit "$PG_INSTALL_DIR/bin/postgres"' +alias pg-debug-lldb='lldb "$PG_INSTALL_DIR/bin/postgres"' +alias pg-debug=' + if command -v gdb >/dev/null 2>&1; then + pg-debug-gdb + elif command -v lldb >/dev/null 2>&1; then + pg-debug-lldb + else + echo "No debugger available (gdb or lldb required)" + fi' + +alias pg-attach-gdb=' + PG_PID=$(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1) + if [ -n "$PG_PID" ]; then + echo "Attaching GDB to PostgreSQL process $PG_PID" + gdb -x "$GDBINIT" -x .gdbinit -p "$PG_PID" + else + echo "No PostgreSQL process found" + fi' + +alias pg-attach-lldb=' + PG_PID=$(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1) + if [ -n "$PG_PID" ]; then + echo "Attaching LLDB to PostgreSQL process $PG_PID" + lldb -p "$PG_PID" + else + echo "No PostgreSQL process found" + fi' + +alias pg-attach=' + if command -v gdb >/dev/null 2>&1; then + pg-attach-gdb + elif command -v lldb >/dev/null 2>&1; then + pg-attach-lldb + else + echo "No debugger available (gdb or lldb required)" + fi' + +# ============================================================ +# libumem (LD_PRELOAD malloc + umem/umemctl CLI + GDB/LLDB helpers) +# ============================================================ +# The nix devShell exports PG_UMEM_PRELOAD / PG_UMEM_GDB_HELPER / +# PG_UMEM_LLDB_HELPER and puts umem, umemctl and pg-umem-preload on PATH. +# These aliases wire libumem into the normal pg-start / debug flow. + +# Start the server with libumem's malloc LD_PRELOAD'ed (heap debugging). +alias pg-start-umem='ulimit -c unlimited && pg-umem-preload "$PG_INSTALL_DIR/bin/postgres" -D "$PG_DATA_DIR" -k "$PG_DATA_DIR"' + +# umemctl against the running postgres backend (default: the postmaster). +# Usage: pg-umem-ctl [args] (stats|caches|cache |whatis |leaks) +# pg-umem-ctl -p ... to target a specific backend +pg-umem-ctl() { + local pid + if [ "$1" = "-p" ]; then pid="$2"; shift 2; + else pid=$(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1); fi + if [ -z "$pid" ]; then echo "No PostgreSQL process found"; return 1; fi + umemctl "$pid" "$@" +} +alias pg-umem-stats='pg-umem-ctl stats' +alias pg-umem-leaks='pg-umem-ctl leaks' +alias pg-umem-caches='pg-umem-ctl caches' + +# Attach GDB with the libumem extension loaded (adds the `umem ...` commands). +# The postgres gdbinit already sources the helper, but this is explicit and +# also works when attaching to a non-postgres umem process. +alias pg-umem-gdb=' + PG_PID=$(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1) + if [ -n "$PG_PID" ]; then + gdb -x "$GDBINIT" -ex "source $PG_UMEM_GDB_HELPER" -p "$PG_PID" + else + echo "No PostgreSQL process found" + fi' + +# Attach LLDB with the libumem Python extension imported. +alias pg-umem-lldb=' + PG_PID=$(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1) + if [ -n "$PG_PID" ]; then + lldb -o "command script import $PG_UMEM_LLDB_HELPER" -p "$PG_PID" + else + echo "No PostgreSQL process found" + fi' + +# ============================================================ +# Valgrind-instrumented build and tests +# +# The valgrind build lives in a separate directory so the normal +# build stays warm. Runs use a wrapper dir that shadows `postgres` +# with a valgrind wrapper -- pg_regress finds it via PATH. +# ============================================================ +pg-build-valgrind() { + local bdir="$PG_BUILD_DIR_VALGRIND" + if [ -z "$PERL_CORE_DIR" ]; then + echo "Error: PERL_CORE_DIR is not set" >&2 + return 1 + fi + + pg_clean_for_compiler "$bdir" + + echo "=== Configuring Valgrind build in $bdir ===" + env CFLAGS="-Og -ggdb3 -fno-omit-frame-pointer -DUSE_VALGRIND -I$PERL_CORE_DIR $CFLAGS" \ + LDFLAGS="-L$PERL_CORE_DIR -lperl $LDFLAGS" \ + meson setup --reconfigure \ + -Doptimization=g \ + -Ddebug=true \ + -Dcassert=true \ + -Dtap_tests=enabled \ + -Dinjection_points=true \ + -Dllvm=disabled \ + -Dplperl=enabled -Dplpython=enabled -Dpltcl=enabled \ + -Dlz4=enabled -Dzstd=enabled \ + -Dlibxml=enabled -Dlibxslt=enabled -Dssl=openssl -Duuid=e2fs \ + -Dldap=disabled \ + --prefix="$PG_INSTALL_DIR-valgrind" \ + "$bdir" "$PG_SOURCE_DIR" || return 1 + + meson compile -C "$bdir" +} + +# Drop a wrapper directory that shadows the real binaries; `postgres` +# exec's into valgrind, everything else is a symlink. Writes to the +# supplied wrap dir and echoes its path. +_pg_make_valgrind_wrapper() { + local bindir="$1" + local wrapdir="$2" + + mkdir -p "$wrapdir" + cat >"$wrapdir/postgres" <&2 + return 1 + fi + + local tmpbin="$bdir/tmp_install$PG_INSTALL_DIR-valgrind/bin" + if [ ! -x "$tmpbin/postgres" ]; then + echo "Populating tmp_install..." + meson test -C "$bdir" tmp_install install_test_files initdb_cache >/dev/null || return 1 + fi + + local wrap + wrap=$(mktemp -d /tmp/pg-vg-wrap-XXXXXX) + _pg_make_valgrind_wrapper "$tmpbin" "$wrap" + + mkdir -p "$PG_BENCH_DIR" + echo "Valgrind logs: $PG_BENCH_DIR/valgrind-*.log" + echo "Wrapper dir: $wrap (will be removed on exit)" + echo "Expect the regress suite to take 15-45 minutes under valgrind." + + local rc=0 + (cd "$bdir" && PATH="$wrap:$PATH" meson test -t 60 --print-errorlogs regress/regress) || rc=$? + + trash "$wrap" 2>/dev/null || rm -rf "$wrap" + return "$rc" +} + +pg-valgrind-test() { + local bdir="$PG_BUILD_DIR_VALGRIND" + if [ ! -x "$bdir/src/backend/postgres" ]; then + echo "Valgrind build not found; run 'pg-build-valgrind' first." >&2 + return 1 + fi + + echo "This runs the FULL postgres test suite under valgrind." + echo "Expect many hours, and tens of GB of valgrind log output." + echo "Logs: $PG_BENCH_DIR/valgrind-*.log" + local yn + read -r -p "Continue? [y/N] " yn + case "$yn" in + y | Y | yes) ;; + *) echo "Aborted."; return 0 ;; + esac + + local tmpbin="$bdir/tmp_install$PG_INSTALL_DIR-valgrind/bin" + if [ ! -x "$tmpbin/postgres" ]; then + echo "Populating tmp_install..." + meson test -C "$bdir" tmp_install install_test_files initdb_cache >/dev/null || return 1 + fi + + local wrap + wrap=$(mktemp -d /tmp/pg-vg-wrap-XXXXXX) + _pg_make_valgrind_wrapper "$tmpbin" "$wrap" + mkdir -p "$PG_BENCH_DIR" + + local rc=0 + (cd "$bdir" && PATH="$wrap:$PATH" meson test -t 60 --print-errorlogs) || rc=$? + + trash "$wrap" 2>/dev/null || rm -rf "$wrap" + return "$rc" +} + +# ============================================================ +# AddressSanitizer / UndefinedBehaviorSanitizer build and tests +# ============================================================ +pg-build-asan() { + local bdir="$PG_BUILD_DIR_ASAN" + if [ -z "$PERL_CORE_DIR" ]; then + echo "Error: PERL_CORE_DIR is not set" >&2 + return 1 + fi + + pg_clean_for_compiler "$bdir" + + echo "=== Configuring ASan+UBSan build in $bdir ===" + env CFLAGS="-Og -ggdb3 -fno-omit-frame-pointer -fsanitize=address,undefined -fno-sanitize-recover=all -I$PERL_CORE_DIR $CFLAGS" \ + LDFLAGS="-fsanitize=address,undefined -L$PERL_CORE_DIR -lperl $LDFLAGS" \ + meson setup --reconfigure \ + -Doptimization=g \ + -Ddebug=true \ + -Dcassert=true \ + -Dtap_tests=enabled \ + -Dinjection_points=true \ + -Dllvm=disabled \ + -Dplperl=enabled -Dplpython=enabled -Dpltcl=enabled \ + -Dlz4=enabled -Dzstd=enabled \ + -Dlibxml=enabled -Dlibxslt=enabled -Dssl=openssl -Duuid=e2fs \ + -Dldap=disabled \ + --prefix="$PG_INSTALL_DIR-asan" \ + "$bdir" "$PG_SOURCE_DIR" || return 1 + + meson compile -C "$bdir" +} + +pg-asan-regress() { + local bdir="$PG_BUILD_DIR_ASAN" + if [ ! -x "$bdir/src/backend/postgres" ]; then + echo "ASan build not found; run 'pg-build-asan' first." >&2 + return 1 + fi + + # halt_on_error=0 lets regress continue past the first diagnostic so + # the whole suite runs; abort_on_error=1 makes each hit fail the test. + ASAN_OPTIONS="halt_on_error=0:abort_on_error=1:detect_leaks=0:print_summary=1:print_stacktrace=1" \ + UBSAN_OPTIONS="halt_on_error=1:abort_on_error=1:print_stacktrace=1:print_summary=1" \ + meson test -t 5 --print-errorlogs -C "$bdir" regress/regress +} + +# ============================================================ +# rr (deterministic record-and-replay) +# Requires kernel.perf_event_paranoid <= 1. rr is the single most +# effective tool for postgres bugs that reproduce intermittently. +# ============================================================ +pg-rr-check() { + if ! command -v rr >/dev/null; then + echo "rr is not installed (expected in the dev shell)." >&2 + return 1 + fi + local paranoid + paranoid=$(cat /proc/sys/kernel/perf_event_paranoid 2>/dev/null || echo 99) + if [ "$paranoid" -gt 1 ]; then + echo "rr requires kernel.perf_event_paranoid <= 1; currently $paranoid" + echo "To enable (root needed):" + echo " echo 1 | sudo tee /proc/sys/kernel/perf_event_paranoid" + return 1 + fi + echo "rr ready (perf_event_paranoid=$paranoid)" +} + +pg-rr-record() { + pg-rr-check >/dev/null || { + pg-rr-check + return 1 + } + ulimit -c unlimited + rr record -- "$PG_INSTALL_DIR/bin/postgres" -D "$PG_DATA_DIR" -k "$PG_DATA_DIR" +} + +pg-rr-replay() { + rr replay "$@" +} + +# ============================================================ +# perf wrappers (parallel to the flame-graph helper) +# ============================================================ +pg-perf-record() { + local pid + pid=$(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1) + if [ -z "$pid" ]; then + echo "No postgres running under $PG_DATA_DIR" >&2 + return 1 + fi + mkdir -p "$PG_BENCH_DIR" + local out="$PG_BENCH_DIR/perf-$(date +%Y%m%d_%H%M%S).data" + echo "Recording to $out (Ctrl-C to stop)" + perf record -F 997 --call-graph dwarf -p "$pid" -o "$out" "$@" + echo "Saved: $out" +} + +pg-perf-report() { + local data + data=$(ls -t "$PG_BENCH_DIR"/perf-*.data 2>/dev/null | head -1) + if [ -z "$data" ]; then + echo "No perf data in $PG_BENCH_DIR" >&2 + return 1 + fi + echo "Reading $data" + perf report -i "$data" "$@" +} + +pg-perf-annotate() { + local data + data=$(ls -t "$PG_BENCH_DIR"/perf-*.data 2>/dev/null | head -1) + if [ -z "$data" ]; then + echo "No perf data in $PG_BENCH_DIR" >&2 + return 1 + fi + perf annotate -i "$data" "$@" +} + +# ============================================================ +# Single regression test / group runner. +# Runs pg_regress directly against the existing build so you skip the +# full meson-driven suite wrapper. Usage: pg-test-one boolean [name ...] +# ============================================================ +pg-test-one() { + if [ $# -eq 0 ]; then + echo "usage: pg-test-one TESTNAME [TESTNAME ...]" + echo "example: pg-test-one boolean" + return 2 + fi + local bdir="${PG_BUILD_DIR_ONE:-$PG_BUILD_DIR}" + local tmpbin="$bdir/tmp_install$PG_INSTALL_DIR/bin" + if [ ! -x "$tmpbin/postgres" ]; then + echo "Populating tmp_install..." + meson test -C "$bdir" tmp_install install_test_files initdb_cache >/dev/null || return 1 + fi + local outdir + outdir=$(mktemp -d /tmp/pg-test-one-XXXXXX) + echo "Test output: $outdir" + LD_LIBRARY_PATH="$bdir/tmp_install$PG_INSTALL_DIR/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" \ + "$bdir/src/test/regress/pg_regress" \ + --bindir="$tmpbin" \ + --inputdir="$PG_SOURCE_DIR/src/test/regress" \ + --expecteddir="$PG_SOURCE_DIR/src/test/regress" \ + --dlpath="$bdir/src/test/regress" \ + --outputdir="$outdir" \ + --temp-instance="$outdir/tmp" \ + --port=40099 \ + "$@" +} + +# Full flame graph / benchmark aliases +alias pg-flame='pg-flame-generate' +alias pg-flame-30='pg-flame-generate 30' +alias pg-flame-60='pg-flame-generate 60' +alias pg-flame-120='pg-flame-generate 120' + +pg-flame-custom() { + local duration=${1:-30} + local output_dir=${2:-$PG_FLAME_DIR} + echo "Generating flame graph for ${duration}s, output to: $output_dir" + pg-flame-generate "$duration" "$output_dir" +} + +alias pg-bench='pg-bench-run' +alias pg-bench-quick='pg-bench-run 5 1 100 1 30 select-only' +alias pg-bench-standard='pg-bench-run 10 2 1000 10 60 tpcb-like' +alias pg-bench-heavy='pg-bench-run 50 4 5000 100 300 tpcb-like' +alias pg-bench-readonly='pg-bench-run 20 4 2000 50 120 select-only' + +pg-bench-custom() { + local clients=${1:-10} + local threads=${2:-2} + local transactions=${3:-1000} + local scale=${4:-10} + local duration=${5:-60} + local test_type=${6:-tpcb-like} + + echo "Running custom benchmark:" + echo " Clients: $clients, Threads: $threads" + echo " Transactions: $transactions, Scale: $scale" + echo " Duration: ${duration}s, Type: $test_type" + + pg-bench-run "$clients" "$threads" "$transactions" "$scale" "$duration" "$test_type" +} + +pg-bench-flame() { + local duration=${1:-60} + local clients=${2:-10} + local scale=${3:-10} + + echo "Running benchmark with flame graph generation" + echo "Duration: ${duration}s, Clients: $clients, Scale: $scale" + + pg-bench-run "$clients" 2 1000 "$scale" "$duration" tpcb-like & + local bench_pid=$! + + sleep 5 + + local flame_duration=$((duration - 10)) + if [ $flame_duration -gt 10 ]; then + pg-flame-generate "$flame_duration" & + local flame_pid=$! + fi + + wait $bench_pid + if [ -n "${flame_pid:-}" ]; then + wait $flame_pid + fi + + echo "Benchmark and flame graph generation completed" +} + +# Live monitoring +alias pg-perf='perf top -p $(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | head -1)' +alias pg-htop='htop -p $(pgrep -f "postgres.*-D.*$PG_DATA_DIR" | tr "\n" "," | sed "s/,$//")' + +pg-stats() { + local duration=${1:-30} + echo "Collecting system stats for ${duration}s..." + + iostat -x 1 "$duration" >"$PG_BENCH_DIR/iostat_$(date +%Y%m%d_%H%M%S).log" & + vmstat 1 "$duration" >"$PG_BENCH_DIR/vmstat_$(date +%Y%m%d_%H%M%S).log" & + + wait + echo "System stats saved to $PG_BENCH_DIR" +} + +# ============================================================ +# Code quality helpers +# ============================================================ +pg-format() { + local since=${1:-HEAD} + + if [ ! -f "$PG_SOURCE_DIR/src/tools/pgindent/pgindent" ]; then + echo "Error: pgindent not found at $PG_SOURCE_DIR/src/tools/pgindent/pgindent" + else + # pgindent needs pg_bsd_indent; meson builds it in-tree. + export PATH="$PG_BUILD_DIR/src/tools/pg_bsd_indent:$PATH" + + modified_files=$(git diff --name-only "${since}" | grep -E "\.c$|\.h$") + + if [ -z "$modified_files" ]; then + echo "No modified .c or .h files found" + else + + echo "Formatting modified files with pgindent:" + for file in $modified_files; do + if [ -f "$file" ]; then + echo " Formatting: $file" + "$PG_SOURCE_DIR/src/tools/pgindent/pgindent" "$file" + else + echo " Warning: File not found: $file" + fi + done + + echo "Checking files for whitespace:" + git diff --check "${since}" + fi + fi +} + +pg-tidy() { + local since=${1:-HEAD} + local files + files=$(git diff --name-only "$since" | grep -E "\.(c|h)$") + if [ -z "$files" ]; then + echo "No modified .c or .h files." + return 0 + fi + for f in $files; do + [ -f "$f" ] || continue + echo "clang-tidy: $f" + clang-tidy -p "$PG_BUILD_DIR" "$f" 2>&1 | head -50 + done +} + +pg-spell() { + local since=${1:-HEAD} + local files=$(git diff --name-only "$since" | grep -E '\.(c|h|sgml|md)$') + if [ -z "$files" ]; then + echo "No .c/.h/.sgml/.md files changed since $since" + return 0 + fi + for f in $files; do + [ -f "$f" ] || continue + case "$f" in + *.c | *.h) + grep -nE '^\s*(/\*|\*|//)' "$f" | codespell --stdin-single-line - 2>/dev/null \ + && echo " $f: ok" || true + ;; + *.sgml | *.md) + codespell "$f" || true + ;; + esac + done +} + +# ============================================================ +# Core dump one-shots (one-time, requires root). kernel.core_pattern +# is a system-wide sysctl -- we don't touch it on every shell entry. +# ============================================================ +pg-cores-status() { + echo "ulimit -c: $(ulimit -c)" + echo "kernel.core_pattern: $(cat /proc/sys/kernel/core_pattern 2>/dev/null || echo unreadable)" + echo "cwd: $(pwd)" +} + +pg-enable-cores() { + ulimit -c unlimited + if ! [ -w /proc/sys/kernel/core_pattern ]; then + echo "Setting kernel.core_pattern (requires sudo)..." + echo "core.%p" | sudo tee /proc/sys/kernel/core_pattern >/dev/null || { + echo "Failed to write /proc/sys/kernel/core_pattern" >&2 + return 1 + } + else + echo "core.%p" >/proc/sys/kernel/core_pattern + fi + pg-cores-status +} + +pg-disable-cores() { + ulimit -c 0 + if ! [ -w /proc/sys/kernel/core_pattern ]; then + echo "Restoring kernel.core_pattern to 'core' (requires sudo)..." + echo "core" | sudo tee /proc/sys/kernel/core_pattern >/dev/null || { + echo "Failed to restore /proc/sys/kernel/core_pattern" >&2 + return 1 + } + else + echo "core" >/proc/sys/kernel/core_pattern + fi + pg-cores-status +} + +# ============================================================ +# Logs and results +# ============================================================ +alias pg-log='tail -f "$PG_DATA_DIR/log/postgresql-$(date +%Y-%m-%d).log" 2>/dev/null || echo "No log file found"' +alias pg-log-errors='grep -i error "$PG_DATA_DIR/log/"*.log 2>/dev/null || echo "No error logs found"' + +alias pg-build-log='cat "$PG_BUILD_DIR/meson-logs/meson-log.txt"' +alias pg-build-errors='grep -i error "$PG_BUILD_DIR/meson-logs/meson-log.txt" 2>/dev/null || echo "No build errors found"' + +alias pg-bench-results='ls -la "$PG_BENCH_DIR" && echo "Latest results:" && tail -20 "$PG_BENCH_DIR"/results_*.txt 2>/dev/null | tail -20' +alias pg-flame-results='ls -la "$PG_FLAME_DIR" && echo "Open flame graphs with: firefox $PG_FLAME_DIR/*.svg"' + +pg-clean-results() { + local days=${1:-7} + echo "Cleaning benchmark and flame graph results older than $days days..." + find "$PG_BENCH_DIR" -type f -mtime +$days -delete 2>/dev/null || true + find "$PG_FLAME_DIR" -type f -mtime +$days -delete 2>/dev/null || true + echo "Cleanup completed" +} + +# ============================================================ +# Info +# ============================================================ +alias pg-info=' + echo "=== PostgreSQL Development Environment ===" + echo "Source: $PG_SOURCE_DIR" + echo "Build (default): $PG_BUILD_DIR" + echo "Build (valgrind):$PG_BUILD_DIR_VALGRIND" + echo "Build (asan): $PG_BUILD_DIR_ASAN" + echo "Install: $PG_INSTALL_DIR" + echo "Data: $PG_DATA_DIR" + echo "Benchmarks: $PG_BENCH_DIR" + echo "Flame graphs: $PG_FLAME_DIR" + echo "Compiler: $CC" + echo "" + echo "Available commands:" + echo " Setup/build: pg-setup, pg-build, pg-install" + echo " Database: pg-init, pg-start, pg-stop, pg-psql" + echo " Tests: pg-test, pg-test-one NAME" + echo " Valgrind: pg-build-valgrind, pg-valgrind-regress, pg-valgrind-test" + echo " ASan/UBSan: pg-build-asan, pg-asan-regress" + echo " Debug: pg-debug, pg-attach" + echo " libumem: pg-start-umem, pg-umem-ctl, pg-umem-stats/leaks/caches, pg-umem-gdb/lldb" + echo " Record/replay: pg-rr-check, pg-rr-record, pg-rr-replay" + echo " Perf: pg-perf-record, pg-perf-report, pg-perf-annotate, pg-perf" + echo " Flame graphs: pg-flame, pg-flame-30, pg-flame-60, pg-flame-custom" + echo " Benchmarks: pg-bench-quick, pg-bench-standard, pg-bench-heavy" + echo " Combined: pg-bench-flame" + echo " Results: pg-bench-results, pg-flame-results" + echo " Logs: pg-log, pg-build-log" + echo " Clean: pg-clean, pg-full-clean, pg-clean-results" + echo " Code quality: pg-format, pg-tidy, pg-spell" + echo " Cores: pg-enable-cores, pg-disable-cores, pg-cores-status" + echo "=========================================="' + +echo "PostgreSQL aliases loaded. Run 'pg-info' for available commands." diff --git a/shell.nix b/shell.nix new file mode 100644 index 0000000000000..d62e76802e5a6 --- /dev/null +++ b/shell.nix @@ -0,0 +1,705 @@ +{ + pkgs, + pkgs-unstable, + system, + libumemPkg, +}: let + # Modern toolchain, pinned to nixpkgs-unstable. + gccPkg = pkgs-unstable.gcc16; + gdbPkg = pkgs-unstable.gdb; + llvmPkgs = pkgs-unstable.llvmPackages_22; + + # libumem debugger helpers (installed by the flake package under + # $out/share/umem/debugger/{gdb,lldb}). + umemGdbHelper = "${libumemPkg}/share/umem/debugger/gdb/umem_gdb.py"; + umemLldbHelper = "${libumemPkg}/share/umem/debugger/lldb/umem_lldb.py"; + umemPreload = "${libumemPkg}/lib/libumem_malloc.so"; + + # Configuration constants + config = { + pgSourceDir = "$PWD"; + pgBuildDir = "$PWD/build"; + pgBuildDirValgrind = "$PWD/build-valgrind"; + pgBuildDirAsan = "$PWD/build-asan"; + pgInstallDir = "$PWD/install"; + pgDataDir = "/tmp/test-db-$(basename $PWD)"; + pgBenchDir = "/tmp/pgbench-results-$(basename $PWD)"; + pgFlameDir = "/tmp/flame-graphs-$(basename $PWD)"; + }; + + # PostgreSQL build + development dependencies (glibc target). + postgreSQLDeps = with pkgs; + [ + # Build system (always use host tools) + pkgs-unstable.meson + pkgs-unstable.ninja + pkg-config + autoconf + git + which + binutils + gnumake + mold # fast linker, big wins on large postgres links + + # Parser/lexer tools + bison + flex + + gettext # msgfmt/xgettext for NLS (translation) builds + + # Perl with required packages + (perl.withPackages (ps: with ps; [IPCRun])) + + # Documentation + docbook_xml_dtd_45 + docbook-xsl-nons + libxslt + libxml2 + fop + + # Development tools (always use host tools) + coreutils + shellcheck + ripgrep + valgrind + curl + uv + pylint + black + lcov + strace + ltrace + perf-tools + linuxPackages.perf + flamegraph + bpftrace # kernel-level tracing (probes, uprobes) + rr # record-and-replay deterministic debugger + htop + iotop + sysstat + ccache + cppcheck + compdb + + # Spell checking + aspell + aspellDicts.en + codespell + + # GCC 16 + GDB (nixpkgs-unstable) + gccPkg + gdbPkg + + # LLVM 22 toolchain + llvmPkgs.llvm + llvmPkgs.llvm.dev + llvmPkgs.clang-tools + llvmPkgs.lldb + + # libumem: LD_PRELOAD malloc + umem/umemctl CLI + debugger helpers + libumemPkg + + # Language support + (python3.withPackages (ps: with ps; [requests browser-cookie3])) + tcl + ] + ++ [ + # Glibc target libraries + readline + zlib + openssl + icu + lz4 + zstd + libuuid + libkrb5 + linux-pam + libxcrypt + numactl + openldap + liburing + libselinux + ]; + + # GDB configuration for PostgreSQL debugging + gdbConfig = pkgs.writeText "gdbinit-postgres" '' + # PostgreSQL-specific GDB configuration + + # Pretty-print PostgreSQL data structures + define print_node + if $arg0 + printf "Node type: %s\n", nodeTagNames[$arg0->type] + print *$arg0 + else + printf "NULL node\n" + end + end + document print_node + Print a PostgreSQL Node with type information + Usage: print_node + end + + define print_list + set $list = (List*)$arg0 + if $list + printf "List length: %d\n", $list->length + set $cell = $list->head + set $i = 0 + while $cell && $i < $list->length + printf " [%d]: ", $i + print_node $cell->data.ptr_value + set $cell = $cell->next + set $i = $i + 1 + end + else + printf "NULL list\n" + end + end + document print_list + Print a PostgreSQL List structure + Usage: print_list + end + + define print_query + set $query = (Query*)$arg0 + if $query + printf "Query type: %d, command type: %d\n", $query->querySource, $query->commandType + print *$query + else + printf "NULL query\n" + end + end + document print_query + Print a PostgreSQL Query structure + Usage: print_query + end + + define print_relcache + set $rel = (Relation)$arg0 + if $rel + printf "Relation: %s.%s (OID: %u)\n", $rel->rd_rel->relnamespace, $rel->rd_rel->relname.data, $rel->rd_id + printf " natts: %d, relkind: %c\n", $rel->rd_rel->relnatts, $rel->rd_rel->relkind + else + printf "NULL relation\n" + end + end + document print_relcache + Print relation cache entry information + Usage: print_relcache + end + + define print_tupdesc + set $desc = (TupleDesc)$arg0 + if $desc + printf "TupleDesc: %d attributes\n", $desc->natts + set $i = 0 + while $i < $desc->natts + set $attr = $desc->attrs[$i] + printf " [%d]: %s (type: %u, len: %d)\n", $i, $attr->attname.data, $attr->atttypid, $attr->attlen + set $i = $i + 1 + end + else + printf "NULL tuple descriptor\n" + end + end + document print_tupdesc + Print tuple descriptor information + Usage: print_tupdesc + end + + define print_slot + set $slot = (TupleTableSlot*)$arg0 + if $slot + printf "TupleTableSlot: %s\n", $slot->tts_ops->name + printf " empty: %d, shouldFree: %d\n", $slot->tts_empty, $slot->tts_shouldFree + if $slot->tts_tupleDescriptor + print_tupdesc $slot->tts_tupleDescriptor + end + else + printf "NULL slot\n" + end + end + document print_slot + Print tuple table slot information + Usage: print_slot + end + + # Memory context debugging + define print_mcxt + set $context = (MemoryContext)$arg0 + if $context + printf "MemoryContext: %s\n", $context->name + printf " type: %s, parent: %p\n", $context->methods->name, $context->parent + printf " total: %zu, free: %zu\n", $context->mem_allocated, $context->freep - $context->freeptr + else + printf "NULL memory context\n" + end + end + document print_mcxt + Print memory context information + Usage: print_mcxt + end + + # Process debugging + define print_proc + set $proc = (PGPROC*)$arg0 + if $proc + printf "PGPROC: pid=%d, database=%u\n", $proc->pid, $proc->databaseId + printf " waiting: %d, waitStatus: %d\n", $proc->waiting, $proc->waitStatus + else + printf "NULL process\n" + end + end + document print_proc + Print process information + Usage: print_proc + end + + # Set useful defaults + set print pretty on + set print object on + set print static-members off + set print vtbl on + set print demangle on + set demangle-style gnu-v3 + set print sevenbit-strings off + set history save on + set history size 1000 + set history filename ~/.gdb_history_postgres + + # Common breakpoints for PostgreSQL debugging + define pg_break_common + break elog + break errfinish + break ExceptionalCondition + break ProcessInterrupts + end + document pg_break_common + Set common PostgreSQL debugging breakpoints + end + + # libumem GDB helpers (umem heap/event inspection). Loaded if present; + # provides the 'umem' command family (umem break, umem events, ...). + python + try: + gdb.execute("source ${umemGdbHelper}") + except Exception as e: + pass + end + + printf "PostgreSQL GDB configuration loaded.\n" + printf "Available commands: print_node, print_list, print_query, print_relcache,\n" + printf " print_tupdesc, print_slot, print_mcxt, print_proc, pg_break_common\n" + ''; + + # Flame graph generation script + flameGraphScript = pkgs.writeScriptBin "pg-flame-generate" '' + #!${pkgs.bash}/bin/bash + set -euo pipefail + + DURATION=''${1:-30} + OUTPUT_DIR=''${2:-${config.pgFlameDir}} + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + + mkdir -p "$OUTPUT_DIR" + + echo "Generating flame graph for PostgreSQL (duration: ''${DURATION}s)" + + # Find PostgreSQL processes + PG_PIDS=$(pgrep -f "postgres.*-D.*${config.pgDataDir}" || true) + + if [ -z "$PG_PIDS" ]; then + echo "Error: No PostgreSQL processes found" + exit 1 + fi + + echo "Found PostgreSQL processes: $PG_PIDS" + + # Record perf data + PERF_DATA="$OUTPUT_DIR/perf_$TIMESTAMP.data" + echo "Recording perf data to $PERF_DATA" + + ${pkgs.linuxPackages.perf}/bin/perf record \ + -F 997 \ + -g \ + --call-graph dwarf \ + -p "$(echo $PG_PIDS | tr ' ' ',')" \ + -o "$PERF_DATA" \ + sleep "$DURATION" + + # Generate flame graph + FLAME_SVG="$OUTPUT_DIR/postgres_flame_$TIMESTAMP.svg" + echo "Generating flame graph: $FLAME_SVG" + + ${pkgs.linuxPackages.perf}/bin/perf script -i "$PERF_DATA" | \ + ${pkgs.flamegraph}/bin/stackcollapse-perf.pl | \ + ${pkgs.flamegraph}/bin/flamegraph.pl \ + --title "PostgreSQL Flame Graph ($TIMESTAMP)" \ + --width 1200 \ + --height 800 \ + > "$FLAME_SVG" + + echo "Flame graph generated: $FLAME_SVG" + echo "Perf data saved: $PERF_DATA" + + # Generate summary report + REPORT="$OUTPUT_DIR/report_$TIMESTAMP.txt" + echo "Generating performance report: $REPORT" + + { + echo "PostgreSQL Performance Analysis Report" + echo "Generated: $(date)" + echo "Duration: ''${DURATION}s" + echo "Processes: $PG_PIDS" + echo "" + echo "=== Top Functions ===" + ${pkgs.linuxPackages.perf}/bin/perf report -i "$PERF_DATA" --stdio --sort comm,dso,symbol | head -50 + echo "" + echo "=== Call Graph ===" + ${pkgs.linuxPackages.perf}/bin/perf report -i "$PERF_DATA" --stdio -g --sort comm,dso,symbol | head -100 + } > "$REPORT" + + echo "Report generated: $REPORT" + echo "" + echo "Files created:" + echo " Flame graph: $FLAME_SVG" + echo " Perf data: $PERF_DATA" + echo " Report: $REPORT" + ''; + + # pgbench wrapper script + pgbenchScript = pkgs.writeScriptBin "pg-bench-run" '' + #!${pkgs.bash}/bin/bash + set -euo pipefail + + # Default parameters + CLIENTS=''${1:-10} + THREADS=''${2:-2} + TRANSACTIONS=''${3:-1000} + SCALE=''${4:-10} + DURATION=''${5:-60} + TEST_TYPE=''${6:-tpcb-like} + + OUTPUT_DIR="${config.pgBenchDir}" + TIMESTAMP=$(date +%Y%m%d_%H%M%S) + + mkdir -p "$OUTPUT_DIR" + + echo "=== PostgreSQL Benchmark Configuration ===" + echo "Clients: $CLIENTS" + echo "Threads: $THREADS" + echo "Transactions: $TRANSACTIONS" + echo "Scale factor: $SCALE" + echo "Duration: ''${DURATION}s" + echo "Test type: $TEST_TYPE" + echo "Output directory: $OUTPUT_DIR" + echo "============================================" + + # Check if PostgreSQL is running + if ! pgrep -f "postgres.*-D.*${config.pgDataDir}" >/dev/null; then + echo "Error: PostgreSQL is not running. Start it with 'pg-start'" + exit 1 + fi + + PGBENCH="${config.pgInstallDir}/bin/pgbench" + PSQL="${config.pgInstallDir}/bin/psql" + CREATEDB="${config.pgInstallDir}/bin/createdb" + DROPDB="${config.pgInstallDir}/bin/dropdb" + + DB_NAME="pgbench_test_$TIMESTAMP" + RESULTS_FILE="$OUTPUT_DIR/results_$TIMESTAMP.txt" + LOG_FILE="$OUTPUT_DIR/pgbench_$TIMESTAMP.log" + + echo "Creating test database: $DB_NAME" + "$CREATEDB" -h "${config.pgDataDir}" "$DB_NAME" || { + echo "Failed to create database" + exit 1 + } + + # Initialize pgbench tables + echo "Initializing pgbench tables (scale factor: $SCALE)" + "$PGBENCH" -h "${config.pgDataDir}" -i -s "$SCALE" "$DB_NAME" || { + echo "Failed to initialize pgbench tables" + "$DROPDB" -h "${config.pgDataDir}" "$DB_NAME" 2>/dev/null || true + exit 1 + } + + # Run benchmark based on test type + echo "Running benchmark..." + + case "$TEST_TYPE" in + "tpcb-like"|"default") + BENCH_ARGS="" + ;; + "select-only") + BENCH_ARGS="-S" + ;; + "simple-update") + BENCH_ARGS="-N" + ;; + "read-write") + BENCH_ARGS="-b select-only@70 -b tpcb-like@30" + ;; + *) + echo "Unknown test type: $TEST_TYPE" + echo "Available types: tpcb-like, select-only, simple-update, read-write" + "$DROPDB" -h "${config.pgDataDir}" "$DB_NAME" 2>/dev/null || true + exit 1 + ;; + esac + + { + echo "PostgreSQL Benchmark Results" + echo "Generated: $(date)" + echo "Test type: $TEST_TYPE" + echo "Clients: $CLIENTS, Threads: $THREADS" + echo "Transactions: $TRANSACTIONS, Duration: ''${DURATION}s" + echo "Scale factor: $SCALE" + echo "Database: $DB_NAME" + echo "" + echo "=== System Information ===" + echo "CPU: $(nproc) cores" + echo "Memory: $(free -h | grep '^Mem:' | awk '{print $2}')" + echo "Compiler: $CC" + echo "PostgreSQL version: $("$PSQL" --no-psqlrc -h "${config.pgDataDir}" -d "$DB_NAME" -t -c "SELECT version();" | head -1)" + echo "" + echo "=== Benchmark Results ===" + } > "$RESULTS_FILE" + + # Run the actual benchmark + "$PGBENCH" \ + -h "${config.pgDataDir}" \ + -c "$CLIENTS" \ + -j "$THREADS" \ + -T "$DURATION" \ + -P 5 \ + --log \ + --log-prefix="$OUTPUT_DIR/pgbench_$TIMESTAMP" \ + $BENCH_ARGS \ + "$DB_NAME" 2>&1 | tee -a "$RESULTS_FILE" + + # Collect additional statistics + { + echo "" + echo "=== Database Statistics ===" + "$PSQL" --no-psqlrc -h "${config.pgDataDir}" -d "$DB_NAME" -c " + SELECT + schemaname, + relname, + n_tup_ins as inserts, + n_tup_upd as updates, + n_tup_del as deletes, + n_live_tup as live_tuples, + n_dead_tup as dead_tuples + FROM pg_stat_user_tables; + " + + echo "" + echo "=== Index Statistics ===" + "$PSQL" --no-psqlrc -h "${config.pgDataDir}" -d "$DB_NAME" -c " + SELECT + schemaname, + relname, + indexrelname, + idx_scan, + idx_tup_read, + idx_tup_fetch + FROM pg_stat_user_indexes; + " + } >> "$RESULTS_FILE" + + # Clean up + echo "Cleaning up test database: $DB_NAME" + "$DROPDB" -h "${config.pgDataDir}" "$DB_NAME" 2>/dev/null || true + + echo "" + echo "Benchmark completed!" + echo "Results saved to: $RESULTS_FILE" + echo "Transaction logs: $OUTPUT_DIR/pgbench_$TIMESTAMP*" + + # Show summary + echo "" + echo "=== Quick Summary ===" + grep -E "(tps|latency)" "$RESULTS_FILE" | tail -5 + ''; + + # LD_PRELOAD libumem's malloc into a command: pg-umem-preload + # A script (not a shell function) so it survives subshells and non-login + # invocations. Preloads $out/lib/libumem_malloc.so ahead of any existing + # LD_PRELOAD. + umemPreloadScript = pkgs.writeScriptBin "pg-umem-preload" '' + #!${pkgs.bash}/bin/bash + if [ $# -eq 0 ]; then + echo "usage: pg-umem-preload [args...]" >&2 + echo " runs with libumem_malloc.so LD_PRELOAD'ed" >&2 + exit 2 + fi + export LD_PRELOAD="${umemPreload}''${LD_PRELOAD:+:$LD_PRELOAD}" + exec "$@" + ''; + + # Shared shellHook fragments. Each devShell prepends its own compiler/CFLAGS + # block, then appends the common tail via ${commonHookTail variant}. + commonHookHead = icon: '' + # History configuration + export HISTFILE=.history + export HISTSIZE=1000000 + export HISTFILESIZE=1000000 + + # Clean environment + unset LD_LIBRARY_PATH LD_PRELOAD LIBRARY_PATH C_INCLUDE_PATH CPLUS_INCLUDE_PATH + + # Essential tools in PATH + export PATH="${pkgs.which}/bin:${pkgs.coreutils}/bin:$PATH" + export PS1="$(echo -e '\u${icon}') {\[$(tput sgr0)\]\[\033[38;5;228m\]\w\[$(tput sgr0)\]\[\033[38;5;15m\]} ($(git rev-parse --abbrev-ref HEAD)) \\$ \[$(tput sgr0)\]" + + # Ccache configuration + export PATH=${pkgs.ccache}/bin:$PATH + export CCACHE_COMPILERCHECK=content + # Loosen a few rules so ccache hits across rebuilds with touched headers. + export CCACHE_SLOPPINESS=pch_defines,time_macros,include_file_mtime,include_file_ctime + export CCACHE_DIR=$HOME/.ccache/pg/$(basename $PWD) + mkdir -p "$CCACHE_DIR" + + # Development tools in PATH + export PATH=${pkgs.clang-tools}/bin:$PATH + export PATH=${pkgs.cppcheck}/bin:$PATH + ''; + + # Tail shared by every devShell: PG env vars, GDB, tool PATH, per-process + # setup and alias load. Kernel core_pattern is NOT touched here -- + # run 'pg-enable-cores' explicitly if you need per-PID cores in CWD. + commonHookTail = label: '' + # PostgreSQL environment + export PG_SOURCE_DIR="${config.pgSourceDir}" + export PG_BUILD_DIR="${config.pgBuildDir}" + export PG_BUILD_DIR_VALGRIND="${config.pgBuildDirValgrind}" + export PG_BUILD_DIR_ASAN="${config.pgBuildDirAsan}" + export PG_INSTALL_DIR="${config.pgInstallDir}" + export PG_DATA_DIR="${config.pgDataDir}" + export PG_BENCH_DIR="${config.pgBenchDir}" + export PG_FLAME_DIR="${config.pgFlameDir}" + export PERL_CORE_DIR=$(find ${pkgs.perl} -maxdepth 5 -path "*/CORE" -type d) + + # GDB configuration + export GDBINIT="${gdbConfig}" + + # libumem: LD_PRELOAD malloc, the umem/umemctl CLI, and debugger helpers. + # The CLI and 'pg-umem-preload' are already on PATH (buildInputs). Export + # the preload object and helper paths; do NOT preload by default -- opt in + # per-command with 'pg-umem-preload ' or export + # LD_PRELOAD="$PG_UMEM_PRELOAD". + export PG_UMEM_PRELOAD="${umemPreload}" + export PG_UMEM_GDB_HELPER="${umemGdbHelper}" + export PG_UMEM_LLDB_HELPER="${umemLldbHelper}" + + # Performance tools in PATH + export PATH="${flameGraphScript}/bin:${pgbenchScript}/bin:${umemPreloadScript}/bin:$PATH" + + # Create output directories + mkdir -p "$PG_BENCH_DIR" "$PG_FLAME_DIR" + + # Per-process core dump size limit. Kernel core_pattern is NOT + # touched here -- run 'pg-enable-cores' explicitly when you need + # per-PID cores in CWD. + ulimit -c unlimited + + # Local git excludes + git config core.excludesFile .local-gitignore 2>/dev/null || true + + # Load PostgreSQL development aliases + if [ -f ./pg-aliases.sh ]; then + source ./pg-aliases.sh + else + echo "Warning: pg-aliases.sh not found in current directory" + fi + + echo "" + echo "PostgreSQL Development Environment Ready (${label})" + echo "Run 'pg-info' for available commands" + echo "libumem: umem/umemctl on PATH; 'pg-umem-preload ' to LD_PRELOAD libumem_malloc" + ''; + + # Development shell (GCC 16 + glibc) + devShell = pkgs.mkShell { + name = "postgresql-dev"; + # Meson probes compile with -O0 -Werror; glibc's fortify-without-O + # warning would fail them. Drop fortify rather than carry a patched + # glibc, which collided with the glibc gcc16 (unstable) links against. + hardeningDisable = ["fortify" "fortify3"]; + buildInputs = + postgreSQLDeps + ++ [ + flameGraphScript + pgbenchScript + umemPreloadScript + ]; + + shellHook = + (commonHookHead "f121") + + '' + # LLVM configuration + export LLVM_CONFIG="${llvmPkgs.llvm}/bin/llvm-config" + export PATH="${llvmPkgs.llvm}/bin:$PATH" + export PKG_CONFIG_PATH="${llvmPkgs.llvm.dev}/lib/pkgconfig:$PKG_CONFIG_PATH" + export LLVM_DIR="${llvmPkgs.llvm.dev}/lib/cmake/llvm" + export LLVM_ROOT="${llvmPkgs.llvm}" + + # PostgreSQL Development CFLAGS + export CFLAGS="" + export CXXFLAGS="" + + # Python UV + UV_PYTHON_DOWNLOADS=never + + # GCC 16 (default compiler) + export CC="${gccPkg}/bin/gcc" + export CXX="${gccPkg}/bin/g++" + + echo "Environment configured:" + echo " Compiler: $CC ($($CC -dumpversion 2>/dev/null))" + echo " libc: glibc" + echo " LLVM: $(llvm-config --version 2>/dev/null || echo 'not available')" + '' + + (commonHookTail "GCC 16 + glibc"); + }; + + # Clang 22 + glibc variant + clangDevShell = pkgs.mkShell { + name = "postgresql-clang-glibc"; + hardeningDisable = ["fortify" "fortify3"]; + buildInputs = + postgreSQLDeps + ++ [ + llvmPkgs.clang + llvmPkgs.lld + llvmPkgs.compiler-rt + flameGraphScript + pgbenchScript + umemPreloadScript + ]; + + shellHook = + (commonHookHead "f121") + + '' + # LLVM configuration + export LLVM_CONFIG="${llvmPkgs.llvm}/bin/llvm-config" + export PATH="${llvmPkgs.llvm}/bin:$PATH" + export PKG_CONFIG_PATH="${llvmPkgs.llvm.dev}/lib/pkgconfig:$PKG_CONFIG_PATH" + export LLVM_DIR="${llvmPkgs.llvm.dev}/lib/cmake/llvm" + export LLVM_ROOT="${llvmPkgs.llvm}" + + # Clang 22 + glibc + export CC="${llvmPkgs.clang}/bin/clang" + export CXX="${llvmPkgs.clang}/bin/clang++" + + echo "Environment configured:" + echo " Compiler: $CC ($($CC -dumpversion 2>/dev/null))" + echo " libc: glibc" + echo " LLVM: $(llvm-config --version 2>/dev/null || echo 'not available')" + '' + + (commonHookTail "Clang 22 + glibc"); + }; +in { + inherit devShell clangDevShell gdbConfig flameGraphScript pgbenchScript; +} diff --git a/src/backend/access/gist/gist.c b/src/backend/access/gist/gist.c index 8565e225be7fd..f6c4592a1f5ff 100644 --- a/src/backend/access/gist/gist.c +++ b/src/backend/access/gist/gist.c @@ -93,6 +93,7 @@ gisthandler(PG_FUNCTION_ARGS) .ambulkdelete = gistbulkdelete, .amvacuumcleanup = gistvacuumcleanup, .amcanreturn = gistcanreturn, + .amcanreturnorderby = gistcanreturnorderby, .amcostestimate = gistcostestimate, .amgettreeheight = NULL, .amoptions = gistoptions, diff --git a/src/backend/access/gist/gistget.c b/src/backend/access/gist/gistget.c index 111c310f9df5b..9aa2f47485604 100644 --- a/src/backend/access/gist/gistget.c +++ b/src/backend/access/gist/gistget.c @@ -23,6 +23,8 @@ #include "pgstat.h" #include "storage/predicate.h" #include "utils/float.h" +#include "utils/fmgroids.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" @@ -813,3 +815,24 @@ gistcanreturn(Relation index, int attno) else return false; } + +/* + * Are the distances the opclass reports for 'orderbyop' on a leaf entry, + * when it does not set *recheck, the operator's own result? + * + * The GiST contract only requires a non-rechecked distance to *sort* like + * the operator's result, so we cannot assume this of an arbitrary opclass. + * point_ops computes a leaf's distance by calling point_distance(), the + * function behind <->(point, point), so the executor may return it in place + * of evaluating the operator. box_ops doesn't qualify: its leaf distance is + * exact arithmetic on the box, while <->(box, point) treats points within + * EPSILON of an edge's line as on it, so the two can differ in the last + * digits. Other opclasses do not qualify. + */ +bool +gistcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop) +{ + return (get_opfamily_proc(opfamily, opcintype, opcintype, + GIST_DISTANCE_PROC) == F_GIST_POINT_DISTANCE && + get_opcode(orderbyop) == F_POINT_DISTANCE); +} diff --git a/src/backend/access/spgist/spgscan.c b/src/backend/access/spgist/spgscan.c index 2cc5f06f5d779..c9b50be348403 100644 --- a/src/backend/access/spgist/spgscan.c +++ b/src/backend/access/spgist/spgscan.c @@ -24,6 +24,7 @@ #include "storage/bufmgr.h" #include "utils/datum.h" #include "utils/float.h" +#include "utils/fmgroids.h" #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/rel.h" @@ -1088,3 +1089,23 @@ spgcanreturn(Relation index, int attno) return cache->config.canReturnData; } + +/* + * Are the distances the opclass reports for 'orderbyop' on a leaf tuple, + * when it does not set recheckDistances, the operator's own result? + * + * As for GiST, the contract only requires them to sort like the operator's + * result. quad_point_ops and kd_point_ops (whose leaf function is + * spg_quad_leaf_consistent()) compute a leaf's distance with + * point_distance(), the function behind <->(point, point). box_ops doesn't + * qualify, for the reason given in gistcanreturnorderby(); polygon sets + * recheckDistances. + */ +bool +spgcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop) +{ + return (get_opfamily_proc(opfamily, opcintype, opcintype, + SPGIST_LEAF_CONSISTENT_PROC) == + F_SPG_QUAD_LEAF_CONSISTENT && + get_opcode(orderbyop) == F_POINT_DISTANCE); +} diff --git a/src/backend/access/spgist/spgutils.c b/src/backend/access/spgist/spgutils.c index f2ee333f60d84..d07e197a594cf 100644 --- a/src/backend/access/spgist/spgutils.c +++ b/src/backend/access/spgist/spgutils.c @@ -78,6 +78,7 @@ spghandler(PG_FUNCTION_ARGS) .ambulkdelete = spgbulkdelete, .amvacuumcleanup = spgvacuumcleanup, .amcanreturn = spgcanreturn, + .amcanreturnorderby = spgcanreturnorderby, .amcostestimate = spgcostestimate, .amgettreeheight = NULL, .amoptions = spgoptions, diff --git a/src/backend/commands/explain.c b/src/backend/commands/explain.c index 4b1ad4227acb4..afc45c0969280 100644 --- a/src/backend/commands/explain.c +++ b/src/backend/commands/explain.c @@ -1974,6 +1974,10 @@ ExplainNode(PlanState *planstate, List *ancestors, planstate, es); show_scan_qual(((IndexScan *) plan)->indexorderbyorig, "Order By", planstate, ancestors, es); + if (es->verbose && ((IndexScan *) plan)->indexorderbynused > 0) + ExplainPropertyInteger("Order By Values Used", NULL, + ((IndexScan *) plan)->indexorderbynused, + es); show_scan_qual(plan->qual, "Filter", planstate, ancestors, es); if (plan->qual) show_instrumentation_count("Rows Removed by Filter", 1, diff --git a/src/backend/executor/nodeIndexscan.c b/src/backend/executor/nodeIndexscan.c index 129d005f187dd..6f11c1938fd3b 100644 --- a/src/backend/executor/nodeIndexscan.c +++ b/src/backend/executor/nodeIndexscan.c @@ -163,6 +163,46 @@ IndexNext(IndexScanState *node) return ExecClearTuple(slot); } +/* + * Hand the ORDER BY values of the row about to be returned to the projection, + * when the targetlist asks for them (indexorderbynused > 0, set by setrefs.c + * only for keys whose opclass passes amcanreturnorderby). vals and nulls + * are what the caller established as this row's ordering values: the AM's + * when it reported them exact, the recomputed ones when it asked for a + * recheck, or a reorder queue entry's copy. Never a lossy AM's lower bound. + * The targetlist reads only float8 and float4 keys (see setrefs.c's + * replace_orderby_tlist_refs()), which are passed by value, so the slot can + * keep them after the caller frees its copies. Another key may be of a + * by-reference type, such as btree_gist's interval-valued <->; nothing reads + * it here, so store a null rather than a pointer that is about to dangle. + */ +static void +IndexStoreOrderByValues(IndexScanState *node, const Datum *vals, + const bool *nulls) +{ + TupleTableSlot *oslot = node->iss_OrderBySlot; + int i; + + if (oslot == NULL) + return; + ExecClearTuple(oslot); + for (i = 0; i < node->iss_NumOrderByKeys; i++) + { + if (node->iss_OrderByTypByVals[i]) + { + oslot->tts_values[i] = vals[i]; + oslot->tts_isnull[i] = nulls[i]; + } + else + { + oslot->tts_values[i] = (Datum) 0; + oslot->tts_isnull[i] = true; + } + } + ExecStoreVirtualTuple(oslot); + node->ss.ps.ps_ExprContext->ecxt_innertuple = oslot; +} + /* ---------------------------------------------------------------- * IndexNextWithReorder * @@ -251,6 +291,9 @@ IndexNextWithReorder(IndexScanState *node) { HeapTuple tuple; + /* the values are by-value; see IndexStoreOrderByValues() */ + IndexStoreOrderByValues(node, topmost->orderbyvals, + topmost->orderbynulls); tuple = reorderqueue_pop(node); /* Pass 'true', as the tuple in the queue is a palloc'd copy */ @@ -352,6 +395,7 @@ IndexNextWithReorder(IndexScanState *node) else { /* Can return this tuple immediately. */ + IndexStoreOrderByValues(node, lastfetched_vals, lastfetched_nulls); return slot; } } @@ -404,7 +448,22 @@ IndexRecheck(IndexScanState *node, TupleTableSlot *slot) /* Does the tuple meet the indexqual condition? */ econtext->ecxt_scantuple = slot; - return ExecQualAndReset(node->indexqualorig, econtext); + if (!ExecQualAndReset(node->indexqualorig, econtext)) + return false; + + /* + * The EPQ test tuple never went through IndexNext, so the ORDER BY values + * the targetlist reads (if any) are not those of this row. Recompute + * them from the tuple, as we would for a rechecked row. + */ + if (node->iss_OrderBySlot != NULL) + { + EvalOrderByExpressions(node, econtext); + IndexStoreOrderByValues(node, node->iss_OrderByValues, + node->iss_OrderByNulls); + } + + return true; } @@ -954,7 +1013,21 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) * Initialize result type and projection. */ ExecInitResultTypeTL(&indexstate->ss.ps); - ExecAssignScanProjectionInfo(&indexstate->ss); + + /* + * A targetlist that reads the ORDER BY values (INNER_VAR Vars, see + * setrefs.c) is never the scan tuple's physical layout, so it always + * projects; ExecConditionalAssignProjectionInfo would assert on the + * INNER_VAR while testing whether it can skip projection. + */ + if (node->indexorderbynused > 0) + { + ExecInitResultSlot(&indexstate->ss.ps, &TTSOpsVirtual); + ExecAssignProjectionInfo(&indexstate->ss.ps, + indexstate->ss.ss_ScanTupleSlot->tts_tupleDescriptor); + } + else + ExecAssignScanProjectionInfo(&indexstate->ss); /* * initialize child expressions @@ -1024,6 +1097,16 @@ ExecInitIndexScan(IndexScan *node, EState *estate, int eflags) NULL, /* no ArrayKeys */ NULL); + /* + * A targetlist that reads the ORDER BY values needs a slot to read them + * from, typed as the ORDER BY expressions (setrefs.c built it so). + */ + if (node->indexorderbynused > 0) + indexstate->iss_OrderBySlot = + ExecAllocTableSlot(&estate->es_tupleTable, + ExecTypeFromExprList(node->indexorderbyorig), + &TTSOpsVirtual, 0); + /* Initialize sort support, if we need to re-check ORDER BY exprs */ if (indexstate->iss_NumOrderByKeys > 0) { diff --git a/src/backend/optimizer/path/allpaths.c b/src/backend/optimizer/path/allpaths.c index 295e3509c740b..89543166572cb 100644 --- a/src/backend/optimizer/path/allpaths.c +++ b/src/backend/optimizer/path/allpaths.c @@ -103,6 +103,9 @@ static void set_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, static void set_plain_rel_size(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte); static void create_plain_partial_paths(PlannerInfo *root, RelOptInfo *rel); +static PathTarget *gather_target(RelOptInfo *rel, Path *subpath); +static void add_extras_append_path(PlannerInfo *root, RelOptInfo *rel, + List *live_childrels); static void set_rel_consider_parallel(PlannerInfo *root, RelOptInfo *rel, RangeTblEntry *rte); static void set_plain_rel_pathlist(PlannerInfo *root, RelOptInfo *rel, @@ -1404,6 +1407,252 @@ set_grouped_rel_pathlist(PlannerInfo *root, RelOptInfo *rel) } +/* + * extras_child_path + * The cheapest path of 'childrel' from 'paths' that emits every value + * in 'extras' (in the parent's terms) and is ordered by 'pathkeys' + * (NIL for no requirement), or NULL. Partial paths are sorted by total + * cost too, so the same search serves partial_pathlist. + */ +static Path * +extras_child_path(PlannerInfo *root, RelOptInfo *childrel, List *paths, + List *extras, List *pathkeys) +{ + List *child_extras; + ListCell *lp; + + child_extras = (List *) + adjust_appendrel_attrs_multilevel(root, (Node *) extras, + childrel, childrel->top_parent); + foreach(lp, paths) + { + Path *path = (Path *) lfirst(lp); + + if (path->param_info == NULL && + pathkeys_contained_in(pathkeys, path->pathkeys) && + list_difference(child_extras, path_extra_exprs(path)) == NIL) + return path; + } + return NULL; +} + +/* + * add_extras_append_path + * Build Appends over child paths that all emit the same extra values, + * if there are any worth emitting. + * + * A partitioned table's index on an expression is an index on that + * expression in every partition, so each child may have an index-only scan + * that reads it (see indexonly_path_target()). The Append can pass such a + * value up, as a join does, but only if every child emits it: they must all + * return the Append's targetlist. The candidates are the parent's + * partitioned indexes' expressions that the query's targetlist uses; for + * each, translated to the child, we take the child's cheapest path that + * emits it. create_append_child_plan() puts each child's values in the + * Append's order. + * + * We build an unordered Append, a MergeAppend for each ordering some child + * provides (or an ordered Append if the partitions are ordered that way, as + * generate_orderedappend_paths() does), and a parallel Append over the + * children's partial paths. + */ +static void +add_extras_append_path(PlannerInfo *root, RelOptInfo *rel, + List *live_childrels) +{ + List *extras = NIL; + List *tlist_exprs; + List *orderings = NIL; + List *partition_pathkeys = NIL; + bool partition_pathkeys_partial = true; + PathTarget *target; + ListCell *lc; + + if (rel->reloptkind != RELOPT_BASEREL || live_childrels == NIL || + root->processed_tlist == NIL) + return; + + tlist_exprs = query_tlist_exprs(root, rel); + foreach(lc, rel->indexlist) + { + IndexOptInfo *index = (IndexOptInfo *) lfirst(lc); + ListCell *le; + + foreach(le, index->indexprs) + { + Node *expr = (Node *) lfirst(le); + ListCell *lt; + + if (!expr_worth_emitting(root, expr)) + continue; + foreach(lt, tlist_exprs) + { + if (expr_contains(lfirst(lt), expr)) + { + extras = list_append_unique(extras, expr); + break; + } + } + } + } + if (extras == NIL) + return; + + target = copy_pathtarget(rel->reltarget); + foreach(lc, extras) + add_new_column_to_pathtarget(target, (Expr *) lfirst(lc)); + target = set_pathtarget_cost_width(root, target); + + /* + * Unordered Append; also collect the orderings children offer. If some + * child has no path that emits the values, we can't build this or an + * ordered one, but may still build the parallel Append below. + */ + { + AppendPathInput input = {0}; + AppendPath *apath; + bool ok = true; + + foreach(lc, live_childrels) + { + RelOptInfo *childrel = (RelOptInfo *) lfirst(lc); + Path *best = extras_child_path(root, childrel, + childrel->pathlist, + extras, NIL); + ListCell *lp; + + if (best == NULL) + { + ok = false; + orderings = NIL; + break; + } + accumulate_append_subpath(best, &input.subpaths, NULL, + &input.child_append_relid_sets); + foreach(lp, childrel->pathlist) + { + Path *path = (Path *) lfirst(lp); + + if (path->pathkeys != NIL && path_emits_extras(path) && + !list_member(orderings, path->pathkeys)) + orderings = lappend(orderings, path->pathkeys); + } + } + if (ok) + { + apath = create_append_path(root, rel, input, NIL, NULL, 0, false, + -1); + apath->path.pathtarget = target; + add_path(rel, (Path *) apath); + } + } + + /* Ordered: MergeAppend, or Append if the partitions are in that order */ + if (rel->part_scheme != NULL && + partitions_are_ordered(rel->boundinfo, rel->live_parts)) + partition_pathkeys = build_partition_pathkeys(root, rel, + ForwardScanDirection, + &partition_pathkeys_partial); + foreach(lc, orderings) + { + List *pathkeys = (List *) lfirst(lc); + AppendPathInput input = {0}; + bool match_partition_order; + bool ok = true; + ListCell *lcr; + + /* only orderings the parent's paths could be using */ + pathkeys = truncate_useless_pathkeys(root, rel, pathkeys); + if (pathkeys == NIL) + continue; + match_partition_order = + pathkeys_contained_in(pathkeys, partition_pathkeys) || + (!partition_pathkeys_partial && + pathkeys_contained_in(partition_pathkeys, pathkeys)); + + foreach(lcr, live_childrels) + { + RelOptInfo *childrel = (RelOptInfo *) lfirst(lcr); + Path *best = extras_child_path(root, childrel, + childrel->pathlist, + extras, pathkeys); + + if (best == NULL) + { + ok = false; + break; + } + + /* as generate_orderedappend_paths() collects them */ + if (match_partition_order) + input.subpaths = lappend(input.subpaths, + get_singleton_append_subpath(best, + &input.child_append_relid_sets)); + else + accumulate_append_subpath(best, &input.subpaths, NULL, + &input.child_append_relid_sets); + } + if (!ok) + continue; + + if (match_partition_order) + { + AppendPath *apath; + + apath = create_append_path(root, rel, input, pathkeys, NULL, 0, + false, -1); + apath->path.pathtarget = target; + add_path(rel, (Path *) apath); + } + else + { + MergeAppendPath *mapath; + + mapath = create_merge_append_path(root, rel, input.subpaths, + input.child_append_relid_sets, + pathkeys, NULL); + mapath->path.pathtarget = target; + add_path(rel, (Path *) mapath); + } + } + + /* Parallel Append over the children's partial paths */ + if (rel->consider_parallel) + { + AppendPathInput input = {0}; + AppendPath *apath; + int parallel_workers = 0; + + foreach(lc, live_childrels) + { + RelOptInfo *childrel = (RelOptInfo *) lfirst(lc); + Path *best = extras_child_path(root, childrel, + childrel->partial_pathlist, + extras, NIL); + + if (best == NULL) + return; + parallel_workers = Max(parallel_workers, best->parallel_workers); + accumulate_append_subpath(best, &input.partial_subpaths, NULL, + &input.child_append_relid_sets); + } + if (enable_parallel_append) + { + parallel_workers = Max(parallel_workers, + pg_leftmost_one_pos32(list_length(live_childrels)) + 1); + parallel_workers = Min(parallel_workers, + max_parallel_workers_per_gather); + } + if (parallel_workers <= 0) + return; + apath = create_append_path(root, rel, input, NIL, NULL, + parallel_workers, enable_parallel_append, + -1); + apath->path.pathtarget = target; + add_partial_path(rel, (Path *) apath); + } +} + /* * add_paths_to_append_rel * Generate paths for the given append relation given the set of non-dummy @@ -1628,6 +1877,14 @@ add_paths_to_append_rel(PlannerInfo *root, RelOptInfo *rel, NIL, NULL, 0, false, -1)); + /* + * Also consider an Append that emits extra values (see + * path_extra_exprs()) its children read from their indexes, so a join + * above it doesn't compute them. + */ + if (unparameterized_valid) + add_extras_append_path(root, rel, live_childrels); + /* build an AppendPath for the cheap startup paths, if valid */ if (startup_valid) add_path(rel, (Path *) create_append_path(root, rel, startup, @@ -3275,10 +3532,32 @@ generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows) cheapest_partial_path = linitial(rel->partial_pathlist); rows = compute_gather_rows(cheapest_partial_path); simple_gather_path = (Path *) - create_gather_path(root, rel, cheapest_partial_path, rel->reltarget, + create_gather_path(root, rel, cheapest_partial_path, + gather_target(rel, cheapest_partial_path), NULL, rowsp); add_path(rel, simple_gather_path); + /* + * A partial path that emits extra values (see path_extra_exprs()) is + * rarely the cheapest; gather the cheapest such path too, passing its + * values up, so whatever is above can use them. + */ + foreach(lc, rel->partial_pathlist) + { + Path *subpath = (Path *) lfirst(lc); + + if (subpath == cheapest_partial_path) + continue; + if (path_emits_extras(subpath)) + { + rows = compute_gather_rows(subpath); + add_path(rel, (Path *) + create_gather_path(root, rel, subpath, + subpath->pathtarget, NULL, rowsp)); + break; + } + } + /* * For each useful ordering, we can consider an order-preserving Gather * Merge. @@ -3292,12 +3571,31 @@ generate_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_rows) continue; rows = compute_gather_rows(subpath); - path = create_gather_merge_path(root, rel, subpath, rel->reltarget, + path = create_gather_merge_path(root, rel, subpath, + gather_target(rel, subpath), subpath->pathkeys, NULL, rowsp); add_path(rel, &path->path); } } +/* + * gather_target + * The target for a Gather or Gather Merge over 'subpath': the rel's + * reltarget, or the subpath's own if it emits extra values (see + * path_extra_exprs()), so they pass up through the Gather. + * + * A Gather over a projection to the final scan/join target, as built by + * apply_scanjoin_target_to_paths(), takes rel->reltarget, which by then is + * that target. + */ +static PathTarget * +gather_target(RelOptInfo *rel, Path *subpath) +{ + if (subpath->pathtarget != rel->reltarget && path_emits_extras(subpath)) + return subpath->pathtarget; + return rel->reltarget; +} + /* * get_useful_pathkeys_for_relation * Determine which orderings of a relation might be useful. @@ -3486,7 +3784,7 @@ generate_useful_gather_paths(PlannerInfo *root, RelOptInfo *rel, bool override_r rows = compute_gather_rows(subpath); path = create_gather_merge_path(root, rel, subpath, - rel->reltarget, + gather_target(rel, subpath), subpath->pathkeys, NULL, rowsp); diff --git a/src/backend/optimizer/path/costsize.c b/src/backend/optimizer/path/costsize.c index 7bbddb8bee4fa..4bd912d0811d6 100644 --- a/src/backend/optimizer/path/costsize.c +++ b/src/backend/optimizer/path/costsize.c @@ -564,6 +564,7 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count, Cost min_IO_cost, max_IO_cost; QualCost qpqual_cost; + QualCost tlist_cost; Cost cpu_per_tuple; double tuples_fetched; double pages_fetched; @@ -794,14 +795,26 @@ cost_index(IndexPath *path, PlannerInfo *root, double loop_count, */ cost_qual_eval(&qpqual_cost, qpquals, root); + /* + * An index-only scan reads returnable index expressions in its quals from + * the index, as it does in its targetlist; see path_target_cost(). + */ + if (indexonly) + qpqual_cost = indexonly_qual_cost(root, path, qpquals, qpqual_cost); + startup_cost += qpqual_cost.startup; cpu_per_tuple = cpu_tuple_cost + qpqual_cost.per_tuple; cpu_run_cost += cpu_per_tuple * tuples_fetched; - /* tlist eval costs are paid per output row, not per tuple scanned */ - startup_cost += path->path.pathtarget->cost.startup; - cpu_run_cost += path->path.pathtarget->cost.per_tuple * path->path.rows; + /* + * tlist eval costs are paid per output row, not per tuple scanned. An + * index path doesn't pay for target entries it reads from the index; see + * path_target_cost(). + */ + tlist_cost = path_target_cost(root, (Path *) path, path->path.pathtarget); + startup_cost += tlist_cost.startup; + cpu_run_cost += tlist_cost.per_tuple * path->path.rows; /* Adjust costing for parallelism, if used. */ if (path->path.parallel_workers > 0) @@ -3646,9 +3659,15 @@ final_cost_nestloop(PlannerInfo *root, NestPath *path, cpu_per_tuple = cpu_tuple_cost + restrict_qual_cost.per_tuple; run_cost += cpu_per_tuple * ntuples; - /* tlist eval costs are paid per output row, not per tuple scanned */ - startup_cost += path->jpath.path.pathtarget->cost.startup; - run_cost += path->jpath.path.pathtarget->cost.per_tuple * path->jpath.path.rows; + /* + * tlist eval costs are paid per output row, not per tuple scanned. The + * join may also emit extra values its inputs emit (see + * join_path_target()), but it doesn't evaluate those, so charge only for + * the joinrel's reltarget. + */ + startup_cost += path->jpath.path.parent->reltarget->cost.startup; + run_cost += path->jpath.path.parent->reltarget->cost.per_tuple * + path->jpath.path.rows; path->jpath.path.startup_cost = startup_cost; path->jpath.path.total_cost = startup_cost + run_cost; @@ -4236,9 +4255,15 @@ final_cost_mergejoin(PlannerInfo *root, MergePath *path, cpu_per_tuple = cpu_tuple_cost + qp_qual_cost.per_tuple; run_cost += cpu_per_tuple * mergejointuples; - /* tlist eval costs are paid per output row, not per tuple scanned */ - startup_cost += path->jpath.path.pathtarget->cost.startup; - run_cost += path->jpath.path.pathtarget->cost.per_tuple * path->jpath.path.rows; + /* + * tlist eval costs are paid per output row, not per tuple scanned. The + * join may also emit extra values its inputs emit (see + * join_path_target()), but it doesn't evaluate those, so charge only for + * the joinrel's reltarget. + */ + startup_cost += path->jpath.path.parent->reltarget->cost.startup; + run_cost += path->jpath.path.parent->reltarget->cost.per_tuple * + path->jpath.path.rows; path->jpath.path.startup_cost = startup_cost; path->jpath.path.total_cost = startup_cost + run_cost; @@ -4755,9 +4780,15 @@ final_cost_hashjoin(PlannerInfo *root, HashPath *path, run_cost += cpu_per_tuple * hashjointuples; } - /* tlist eval costs are paid per output row, not per tuple scanned */ - startup_cost += path->jpath.path.pathtarget->cost.startup; - run_cost += path->jpath.path.pathtarget->cost.per_tuple * path->jpath.path.rows; + /* + * tlist eval costs are paid per output row, not per tuple scanned. The + * join may also emit extra values its inputs emit (see + * join_path_target()), but it doesn't evaluate those, so charge only for + * the joinrel's reltarget. + */ + startup_cost += path->jpath.path.parent->reltarget->cost.startup; + run_cost += path->jpath.path.parent->reltarget->cost.per_tuple * + path->jpath.path.rows; path->jpath.path.startup_cost = startup_cost; path->jpath.path.total_cost = startup_cost + run_cost; diff --git a/src/backend/optimizer/path/joinpath.c b/src/backend/optimizer/path/joinpath.c index dfd08e7aeb1b8..8ee38d0eb9afc 100644 --- a/src/backend/optimizer/path/joinpath.c +++ b/src/backend/optimizer/path/joinpath.c @@ -45,6 +45,7 @@ join_path_setup_hook_type join_path_setup_hook = NULL; #define PATH_PARAM_BY_REL(path, rel) \ (PATH_PARAM_BY_REL_SELF(path, rel) || PATH_PARAM_BY_PARENT(path, rel)) +static Path *cheapest_extras_path(RelOptInfo *rel); static void try_partial_mergejoin_path(PlannerInfo *root, RelOptInfo *joinrel, Path *outer_path, @@ -149,6 +150,8 @@ add_paths_to_joinrel(PlannerInfo *root, extra.restrictlist = restrictlist; extra.mergeclause_list = NIL; extra.sjinfo = sjinfo; + extra.extras_outer = cheapest_extras_path(outerrel); + extra.extras_inner = cheapest_extras_path(innerrel); extra.param_source_rels = NULL; extra.pgs_mask = joinrel->pgs_mask; @@ -872,6 +875,51 @@ get_memoize_path(PlannerInfo *root, RelOptInfo *innerrel, return NULL; } +/* + * inputs_emit_extras + * Does either input path emit values beyond its reltarget? + * + * A join over such an input emits them too (see join_path_target()), which + * add_path() counts in its favor like better pathkeys. add_path_precheck() + * can't know that before the path exists, so skip the precheck for it. + */ +static inline bool +inputs_emit_extras(Path *outer_path, Path *inner_path) +{ + /* cheap test first: most paths share their rel's reltarget */ + return (outer_path->pathtarget != outer_path->parent->reltarget && + path_emits_extras(outer_path)) || + (inner_path->pathtarget != inner_path->parent->reltarget && + path_emits_extras(inner_path)); +} + +/* + * cheapest_extras_path + * The cheapest unparameterized path of 'rel' that emits values beyond its + * reltarget (see path_extra_exprs()), or NULL. + * + * Such a path is kept by add_path() but is rarely the rel's cheapest, and + * hash and sort-merge joins look only at the cheapest. It is worth trying + * as well: a join over it doesn't pay for evaluating those values. + * pathlist is sorted by total cost, so the first one found is the cheapest. + */ +static Path * +cheapest_extras_path(RelOptInfo *rel) +{ + ListCell *lc; + + foreach(lc, rel->pathlist) + { + Path *path = (Path *) lfirst(lc); + + if (path->param_info == NULL && path != rel->cheapest_total_path && + path->pathtarget != rel->reltarget && + path_emits_extras(path)) + return path; + } + return NULL; +} + /* * try_nestloop_path * Consider a nestloop join path; if it appears useful, push it into @@ -970,7 +1018,8 @@ try_nestloop_path(PlannerInfo *root, nestloop_subtype | PGS_CONSIDER_NONPARTIAL, outer_path, inner_path, extra); - if (add_path_precheck(joinrel, workspace.disabled_nodes, + if (inputs_emit_extras(outer_path, inner_path) || + add_path_precheck(joinrel, workspace.disabled_nodes, workspace.startup_cost, workspace.total_cost, pathkeys, required_outer)) { @@ -1055,7 +1104,8 @@ try_partial_nestloop_path(PlannerInfo *root, */ initial_cost_nestloop(root, &workspace, jointype, nestloop_subtype, outer_path, inner_path, extra); - if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes, + if (!inputs_emit_extras(outer_path, inner_path) && + !add_partial_path_precheck(joinrel, workspace.disabled_nodes, workspace.startup_cost, workspace.total_cost, pathkeys)) return; @@ -1163,7 +1213,8 @@ try_mergejoin_path(PlannerInfo *root, outer_presorted_keys, extra); - if (add_path_precheck(joinrel, workspace.disabled_nodes, + if (inputs_emit_extras(outer_path, inner_path) || + add_path_precheck(joinrel, workspace.disabled_nodes, workspace.startup_cost, workspace.total_cost, pathkeys, required_outer)) { @@ -1245,7 +1296,8 @@ try_partial_mergejoin_path(PlannerInfo *root, outer_presorted_keys, extra); - if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes, + if (!inputs_emit_extras(outer_path, inner_path) && + !add_partial_path_precheck(joinrel, workspace.disabled_nodes, workspace.startup_cost, workspace.total_cost, pathkeys)) return; @@ -1317,7 +1369,8 @@ try_hashjoin_path(PlannerInfo *root, initial_cost_hashjoin(root, &workspace, jointype, hashclauses, outer_path, inner_path, extra, false); - if (add_path_precheck(joinrel, workspace.disabled_nodes, + if (inputs_emit_extras(outer_path, inner_path) || + add_path_precheck(joinrel, workspace.disabled_nodes, workspace.startup_cost, workspace.total_cost, NIL, required_outer)) { @@ -1378,7 +1431,8 @@ try_partial_hashjoin_path(PlannerInfo *root, */ initial_cost_hashjoin(root, &workspace, jointype, hashclauses, outer_path, inner_path, extra, parallel_hash); - if (!add_partial_path_precheck(joinrel, workspace.disabled_nodes, + if (!inputs_emit_extras(outer_path, inner_path) && + !add_partial_path_precheck(joinrel, workspace.disabled_nodes, workspace.startup_cost, workspace.total_cost, NIL)) return; @@ -1419,6 +1473,8 @@ sort_inner_and_outer(PlannerInfo *root, { Path *outer_path; Path *inner_path; + Path *extras_outer; + Path *extras_inner; Path *cheapest_partial_outer = NULL; Path *cheapest_safe_inner = NULL; List *all_pathkeys; @@ -1453,6 +1509,8 @@ sort_inner_and_outer(PlannerInfo *root, if (PATH_PARAM_BY_REL(outer_path, innerrel) || PATH_PARAM_BY_REL(inner_path, outerrel)) return; + extras_outer = extra->extras_outer; + extras_inner = extra->extras_inner; /* * If the joinrel is parallel-safe, we may be able to consider a partial @@ -1561,6 +1619,16 @@ sort_inner_and_outer(PlannerInfo *root, extra, false); + /* Likewise with inputs that emit extra values; see hash join */ + if (extras_outer != NULL) + try_mergejoin_path(root, joinrel, extras_outer, inner_path, + merge_pathkeys, cur_mergeclauses, + outerkeys, innerkeys, jointype, extra, false); + if (extras_inner != NULL) + try_mergejoin_path(root, joinrel, outer_path, extras_inner, + merge_pathkeys, cur_mergeclauses, + outerkeys, innerkeys, jointype, extra, false); + /* * If we have partial outer and parallel safe inner path then try * partial mergejoin path. @@ -1845,6 +1913,7 @@ match_unsorted_outer(PlannerInfo *root, bool useallclauses; Path *inner_cheapest_total = innerrel->cheapest_total_path; Path *matpath = NULL; + Path *extras_matpath = NULL; ListCell *lc1; /* @@ -1917,10 +1986,21 @@ match_unsorted_outer(PlannerInfo *root, if ((extra->pgs_mask & (PGS_NESTLOOP_MATERIALIZE | PGS_CONSIDER_NONPARTIAL)) == (PGS_NESTLOOP_MATERIALIZE | PGS_CONSIDER_NONPARTIAL) && - inner_cheapest_total != NULL && - !ExecMaterializesOutput(inner_cheapest_total->pathtype)) - matpath = (Path *) - create_material_path(innerrel, inner_cheapest_total, true); + inner_cheapest_total != NULL) + { + Path *extras_inner = extra->extras_inner; + + if (!ExecMaterializesOutput(inner_cheapest_total->pathtype)) + matpath = (Path *) + create_material_path(innerrel, inner_cheapest_total, true); + + /* likewise the cheapest inner path that emits extra values */ + if (extras_inner != NULL && + !PATH_PARAM_BY_REL(extras_inner, outerrel) && + !ExecMaterializesOutput(extras_inner->pathtype)) + extras_matpath = (Path *) + create_material_path(innerrel, extras_inner, true); + } } foreach(lc1, outerrel->pathlist) @@ -1994,6 +2074,17 @@ match_unsorted_outer(PlannerInfo *root, jointype, PGS_NESTLOOP_MATERIALIZE, extra); + + /* And of an inner path that emits extra values; see hash join */ + if (extras_matpath != NULL) + try_nestloop_path(root, + joinrel, + outerpath, + extras_matpath, + merge_pathkeys, + jointype, + PGS_NESTLOOP_MATERIALIZE, + extra); } /* Can't do anything else if inner rel is parameterized by outer */ @@ -2283,6 +2374,24 @@ hash_inner_and_outer(PlannerInfo *root, jointype, extra); + /* + * Also try an input that emits values the query needs above the join, + * on either side; see cheapest_extras_path(). + */ + { + Path *extras_outer = extra->extras_outer; + Path *extras_inner = extra->extras_inner; + + if (extras_outer != NULL) + try_hashjoin_path(root, joinrel, + extras_outer, cheapest_total_inner, + hashclauses, jointype, extra); + if (extras_inner != NULL) + try_hashjoin_path(root, joinrel, + cheapest_total_outer, extras_inner, + hashclauses, jointype, extra); + } + foreach(lc1, outerrel->cheapest_parameterized_paths) { Path *outerpath = (Path *) lfirst(lc1); diff --git a/src/backend/optimizer/plan/createplan.c b/src/backend/optimizer/plan/createplan.c index 5bc3a8e76d5d4..1ce86e352d379 100644 --- a/src/backend/optimizer/plan/createplan.c +++ b/src/backend/optimizer/plan/createplan.c @@ -24,6 +24,7 @@ #include "nodes/extensible.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "optimizer/appendinfo.h" #include "optimizer/clauses.h" #include "optimizer/cost.h" #include "optimizer/optimizer.h" @@ -83,6 +84,8 @@ static Plan *create_gating_plan(PlannerInfo *root, Path *path, Plan *plan, List *gating_quals); static Plan *create_join_plan(PlannerInfo *root, JoinPath *best_path); static bool mark_async_capable_plan(Plan *plan, Path *path); +static Plan *create_append_child_plan(PlannerInfo *root, Path *apath, + Path *subpath); static Plan *create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags); static Plan *create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path, @@ -938,6 +941,27 @@ use_physical_tlist(PlannerInfo *root, Path *path, int flags) return false; } + /* + * Nor if the path emits extra values (see path_extra_exprs()) that a + * physical tlist lacks, such as an index scan's ORDER BY values. An + * index-only scan's are its own expression columns (see + * indexonly_path_target()), which its physical tlist, the index's + * indextlist, already holds. + */ + if (path_emits_extras(path)) + { + List *indextlist; + + if (path->pathtype != T_IndexOnlyScan) + return false; + indextlist = ((IndexPath *) path)->indexinfo->indextlist; + foreach(lc, path_extra_exprs(path)) + { + if (!tlist_member((Expr *) lfirst(lc), indextlist)) + return false; + } + } + /* * For an index-only scan, the "physical tlist" is the index's indextlist. * We can only return that without a projection if all the index's columns @@ -1208,6 +1232,59 @@ mark_async_capable_plan(Plan *plan, Path *path) return true; } +/* + * create_append_child_plan + * Build the plan for a child of an Append or MergeAppend. + * + * Every child must return the Append's tlist: its rel's reltarget, plus any + * extra values the Append emits (see add_extras_append_path()), translated + * to the child. A child path may emit extra values of its own beyond its + * reltarget (see path_extra_exprs()), in some order; keep those the Append + * emits, in the Append's order, and drop the rest. + */ +static Plan * +create_append_child_plan(PlannerInfo *root, Path *apath, Path *subpath) +{ + Plan *subplan = create_plan_recurse(root, subpath, CP_EXACT_TLIST); + List *append_extras = path_extra_exprs(apath); + List *child_extras; + List *tlist; + int nrel; + ListCell *lc; + + if (!path_emits_extras(subpath)) + { + Assert(append_extras == NIL); + return subplan; + } + + nrel = list_length(subpath->parent->reltarget->exprs); + tlist = list_copy_head(subplan->targetlist, nrel); + child_extras = (List *) + adjust_appendrel_attrs_multilevel(root, (Node *) append_extras, + subpath->parent, + apath->parent); + foreach(lc, child_extras) + { + TargetEntry *tle = tlist_member((Expr *) lfirst(lc), + subplan->targetlist); + + if (tle == NULL) + elog(ERROR, "child of Append doesn't emit one of its extra values"); + tlist = lappend(tlist, makeTargetEntry(tle->expr, + list_length(tlist) + 1, + NULL, false)); + } + if (tlist_same_exprs(tlist, subplan->targetlist)) + return subplan; + if (is_projection_capable_plan(subplan)) + { + subplan->targetlist = tlist; + return subplan; + } + return inject_projection_plan(subplan, tlist, subplan->parallel_safe); +} + /* * create_append_plan * Create an Append plan for 'best_path' and (recursively) plans @@ -1309,7 +1386,7 @@ create_append_plan(PlannerInfo *root, AppendPath *best_path, int flags) Plan *subplan; /* Must insist that all children return the same tlist */ - subplan = create_plan_recurse(root, subpath, CP_EXACT_TLIST); + subplan = create_append_child_plan(root, (Path *) best_path, subpath); /* * For ordered Appends, we must insert a Sort node if subplan isn't @@ -1525,7 +1602,7 @@ create_merge_append_plan(PlannerInfo *root, MergeAppendPath *best_path, /* Build the child plan */ /* Must insist that all children return the same tlist */ - subplan = create_plan_recurse(root, subpath, CP_EXACT_TLIST); + subplan = create_append_child_plan(root, (Path *) best_path, subpath); /* Compute sort column info, and adjust subplan's tlist as needed */ subplan = prepare_sort_from_pathkeys(root, subplan, pathkeys, @@ -3037,6 +3114,21 @@ create_indexscan_plan(PlannerInfo *root, indexorderbyops, best_path->indexscandir); + if (!indexonly && indexorderbys != NIL) + { + List *exact = NIL; + ListCell *lo, + *lc; + + /* must agree with path_target_cost() in pathnode.c */ + forboth(lo, best_path->indexorderbys, lc, best_path->indexorderbycols) + exact = lappend_int(exact, + index_orderby_returnable(indexinfo, + lfirst_int(lc), + (Expr *) lfirst(lo))); + ((IndexScan *) scan_plan)->indexorderbyexact = exact; + } + copy_generic_path_info(&scan_plan->plan, &best_path->path); return scan_plan; diff --git a/src/backend/optimizer/plan/planner.c b/src/backend/optimizer/plan/planner.c index edaa42d713af0..c2fe38d22114f 100644 --- a/src/backend/optimizer/plan/planner.c +++ b/src/backend/optimizer/plan/planner.c @@ -8190,6 +8190,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, bool tlist_same_exprs) { bool rel_is_partitioned = IS_PARTITIONED_REL(rel); + bool projected = false; PathTarget *scanjoin_target; ListCell *lc; @@ -8259,8 +8260,17 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, * If the tlist exprs are the same, we can just inject the sortgroupref * information into the existing pathtargets. Otherwise, replace each * path with a projection path that generates the SRF-free scan/join - * target. This can't change the ordering of paths within rel->pathlist, - * so we just modify the list in place. + * target. We modify the lists in place. + * + * A path may emit extra values beyond rel->reltarget (see + * path_extra_exprs()). Such a path needs a projection even when the + * exprs are the same, since its target has more columns than + * scanjoin_target's sortgrouprefs describe. + * + * A projection usually adds the same cost to every path, but not always: + * an index path isn't charged for target entries it reads from the index, + * and a path that can't project gets a Result node. So when any path was + * replaced, restore the cost ordering afterwards. */ foreach(lc, rel->pathlist) { @@ -8269,7 +8279,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, /* Shouldn't have any parameterized paths anymore */ Assert(subpath->param_info == NULL); - if (tlist_same_exprs) + if (tlist_same_exprs && !path_emits_extras(subpath)) subpath->pathtarget->sortgrouprefs = scanjoin_target->sortgrouprefs; else @@ -8279,8 +8289,12 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, newpath = (Path *) create_projection_path(root, rel, subpath, scanjoin_target); lfirst(lc) = newpath; + projected = true; } } + if (projected) + sort_pathlist_by_cost(rel->pathlist); + projected = false; /* Likewise adjust the targets for any partial paths. */ foreach(lc, rel->partial_pathlist) @@ -8290,7 +8304,7 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, /* Shouldn't have any parameterized paths anymore */ Assert(subpath->param_info == NULL); - if (tlist_same_exprs) + if (tlist_same_exprs && !path_emits_extras(subpath)) subpath->pathtarget->sortgrouprefs = scanjoin_target->sortgrouprefs; else @@ -8300,8 +8314,11 @@ apply_scanjoin_target_to_paths(PlannerInfo *root, newpath = (Path *) create_projection_path(root, rel, subpath, scanjoin_target); lfirst(lc) = newpath; + projected = true; } } + if (projected) + sort_pathlist_by_cost(rel->partial_pathlist); /* * Now, if final scan/join target contains SRFs, insert ProjectSetPath(s) diff --git a/src/backend/optimizer/plan/setrefs.c b/src/backend/optimizer/plan/setrefs.c index af78892fafc4e..86e28971707be 100644 --- a/src/backend/optimizer/plan/setrefs.c +++ b/src/backend/optimizer/plan/setrefs.c @@ -19,6 +19,7 @@ #include "catalog/pg_type.h" #include "nodes/makefuncs.h" #include "nodes/nodeFuncs.h" +#include "optimizer/clauses.h" #include "optimizer/optimizer.h" #include "optimizer/pathnode.h" #include "optimizer/planmain.h" @@ -51,6 +52,12 @@ typedef struct int num_vars; /* number of plain Var tlist entries */ bool has_ph_vars; /* are there PlaceHolderVar entries? */ bool has_non_vars; /* are there other entries? */ + + /* + * non-Var entries without nullingrels, built on demand by + * search_indexed_tlist_for_nulled_expr(); not freed with the struct + */ + List *nulled_tles; tlist_vinfo vars[FLEXIBLE_ARRAY_MEMBER]; /* has num_vars entries */ } indexed_tlist; @@ -134,6 +141,7 @@ static bool flatten_rtes_walker(Node *node, flatten_rtes_walker_context *cxt); static void add_rte_to_flat_rtable(PlannerGlobal *glob, List *rteperminfos, RangeTblEntry *rte); static Plan *set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset); +static void replace_orderby_tlist_refs(IndexScan *splan); static Plan *set_indexonlyscan_references(PlannerInfo *root, IndexOnlyScan *plan, int rtoffset); @@ -179,6 +187,10 @@ static Var *search_indexed_tlist_for_phv(PlaceHolderVar *phv, static Var *search_indexed_tlist_for_non_var(Expr *node, indexed_tlist *itlist, int newvarno); +static Var *search_indexed_tlist_for_nulled_expr(PlannerInfo *root, + Expr *node, + indexed_tlist *itlist, + int newvarno); static Var *search_indexed_tlist_for_sortgroupref(Expr *node, Index sortgroupref, indexed_tlist *itlist, @@ -704,6 +716,8 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) splan->indexorderbyorig = fix_scan_list(root, splan->indexorderbyorig, rtoffset, NUM_EXEC_QUAL(plan)); + /* after both are fixed, so equal() compares like with like */ + replace_orderby_tlist_refs(splan); } break; case T_IndexOnlyScan: @@ -1367,6 +1381,65 @@ set_plan_refs(PlannerInfo *root, Plan *plan, int rtoffset) return plan; } +/* + * replace_orderby_tlist_refs + * Let an ordering IndexScan hand its ORDER BY values to its targetlist. + * + * For each ORDER BY key whose index column's opclass promises that the + * distances it reports for an exact (non-rechecked) row are the ordering + * operator's own result (indexorderbyexact[i], from amcanreturnorderby), + * replace each top-level targetlist expression that is the i'th + * indexorderbyorig expression (equal() to it, or its commuted form; see + * orderby_tlist_match()) with Var(INNER_VAR, i + 1). nodeIndexscan.c + * then fills a virtual slot with the ORDER BY values of every returned row -- + * the AM's for an exact row, the recomputed ones for a rechecked row -- and + * installs it as the projection's inner tuple. A scan node has no inner + * plan, so INNER_VAR is otherwise unused here. + * + * indexorderbyexact[i] comes from index_orderby_returnable(), which also + * limits this to float8/float4, the only types + * index_store_float8_orderby_distances() can deliver. The planner costs + * the scan with the same test (path_target_cost() in pathnode.c), so what + * is costed is what is built. + */ +static void +replace_orderby_tlist_refs(IndexScan *splan) +{ + ListCell *lc; + Bitmapset *used = NULL; + + if (splan->indexorderbyexact == NIL) + return; + Assert(list_length(splan->indexorderbyexact) == + list_length(splan->indexorderbyorig)); + + foreach(lc, splan->scan.plan.targetlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lc); + ListCell *lo, + *le; + int i = 0; + + forboth(lo, splan->indexorderbyorig, le, splan->indexorderbyexact) + { + Expr *orig = (Expr *) lfirst(lo); + Oid typ = exprType((Node *) orig); + + i++; + if (lfirst_int(le) && orderby_tlist_match(tle->expr, orig)) + { + tle->expr = (Expr *) makeVar(INNER_VAR, i, typ, + exprTypmod((Node *) orig), + exprCollation((Node *) orig), + 0); + used = bms_add_member(used, i); + break; + } + } + } + splan->indexorderbynused = bms_num_members(used); +} + /* * set_indexonlyscan_references * Do set_plan_references processing on an IndexOnlyScan @@ -2861,7 +2934,8 @@ set_dummy_tlist_references(Plan *plan, int rtoffset) * * The result of this function is an indexed_tlist struct to pass to * search_indexed_tlist_for_var() and siblings. - * When done, the indexed_tlist may be freed with a single pfree(). + * When done, the indexed_tlist may be freed with a single pfree(); a + * nulled_tles list built for it later stays in the planner's context. */ static indexed_tlist * build_tlist_index(List *tlist) @@ -2878,6 +2952,7 @@ build_tlist_index(List *tlist) itlist->tlist = tlist; itlist->has_ph_vars = false; itlist->has_non_vars = false; + itlist->nulled_tles = NIL; /* Find the Vars and fill in the index array */ vinfo = itlist->vars; @@ -2929,6 +3004,7 @@ build_tlist_index_other_vars(List *tlist, int ignore_rel) itlist->tlist = tlist; itlist->has_ph_vars = false; itlist->has_non_vars = false; + itlist->nulled_tles = NIL; /* Find the desired Vars and fill in the index array */ vinfo = itlist->vars; @@ -3115,6 +3191,108 @@ search_indexed_tlist_for_non_var(Expr *node, return NULL; /* no match */ } +/* + * expr_nullingrels + * The union of the nullingrels of the Vars and PlaceHolderVars in 'node'. + */ +static Relids +expr_nullingrels(Node *node) +{ + Relids result = NULL; + ListCell *lc; + + foreach(lc, pull_var_clause(node, PVC_INCLUDE_PLACEHOLDERS | + PVC_RECURSE_AGGREGATES | + PVC_RECURSE_WINDOWFUNCS)) + { + Node *v = (Node *) lfirst(lc); + + if (IsA(v, Var)) + result = bms_add_members(result, ((Var *) v)->varnullingrels); + else + result = bms_add_members(result, + ((PlaceHolderVar *) v)->phnullingrels); + } + return result; +} + +/* + * search_indexed_tlist_for_nulled_expr + * Like search_indexed_tlist_for_non_var(), for an expression above an + * outer join whose Vars may carry nullingrels the input's don't. + * + * An input of an outer join may emit a non-Var expression it read from an + * index (see path_extra_exprs()). Above the join, the query's copy of that + * expression has the join's bit added to its Vars' nullingrels, so equal() + * won't match it; the join adds the bit because it null-extends those Vars. + * It null-extends the input's copy of the expression too, so the two agree + * on every row if the expression yields NULL when its Vars are NULL, that + * is if it is strict. Then match it as we match Vars here (NRM_SUPERSET): + * equal but for nullingrels, with the query's copy's nullingrels a superset + * of the input's. The planner relies on this when it carries such a value + * up through an outer join (see join_path_target()). + * + * The first call for an itlist saves its non-Var entries without + * nullingrels, each paired with the nullingrels it had, so that later calls + * don't strip them again. Callers only search an itlist that has_non_vars, + * so that list is never empty once built. + */ +static Var * +search_indexed_tlist_for_nulled_expr(PlannerInfo *root, Expr *node, + indexed_tlist *itlist, int newvarno) +{ + Node *stripped = NULL; + Relids nullingrels = NULL; + ListCell *lc; + + Assert(itlist->has_non_vars); + if (IsA(node, Const) || IsA(node, Param) || + bms_is_empty(root->outer_join_rels)) + return NULL; + + if (itlist->nulled_tles == NIL) + { + foreach(lc, itlist->tlist) + { + TargetEntry *tle = (TargetEntry *) lfirst(lc); + TargetEntry *ntle; + + if (IsA(tle->expr, Var) || IsA(tle->expr, PlaceHolderVar)) + continue; + ntle = flatCopyTargetEntry(tle); + ntle->expr = (Expr *) strip_nullingrels((Node *) tle->expr); + itlist->nulled_tles = lappend(itlist->nulled_tles, ntle); + itlist->nulled_tles = lappend(itlist->nulled_tles, + expr_nullingrels((Node *) tle->expr)); + } + Assert(itlist->nulled_tles != NIL); + } + + for (int i = 0; i < list_length(itlist->nulled_tles); i += 2) + { + TargetEntry *tle = list_nth_node(TargetEntry, itlist->nulled_tles, i); + + if (stripped == NULL) + { + if (contain_nonstrict_functions((Node *) node) || + !contain_var_clause((Node *) node)) + return NULL; + stripped = strip_nullingrels((Node *) node); + nullingrels = expr_nullingrels((Node *) node); + } + if (equal(stripped, tle->expr) && + bms_is_subset(list_nth(itlist->nulled_tles, i + 1), nullingrels)) + { + Var *newvar = makeVarFromTargetEntry(newvarno, tle); + + newvar->varnosyn = 0; /* wasn't ever a plain Var */ + newvar->varattnosyn = 0; + return newvar; + } + } + return NULL; +} + /* * search_indexed_tlist_for_sortgroupref --- find a sort/group expression * @@ -3322,6 +3500,11 @@ fix_join_expr_mutator(Node *node, fix_join_expr_context *context) newvar = search_indexed_tlist_for_non_var((Expr *) node, context->outer_itlist, OUTER_VAR); + if (newvar == NULL && context->nrm_match == NRM_SUPERSET) + newvar = search_indexed_tlist_for_nulled_expr(context->root, + (Expr *) node, + context->outer_itlist, + OUTER_VAR); if (newvar) return (Node *) newvar; } @@ -3330,6 +3513,11 @@ fix_join_expr_mutator(Node *node, fix_join_expr_context *context) newvar = search_indexed_tlist_for_non_var((Expr *) node, context->inner_itlist, INNER_VAR); + if (newvar == NULL && context->nrm_match == NRM_SUPERSET) + newvar = search_indexed_tlist_for_nulled_expr(context->root, + (Expr *) node, + context->inner_itlist, + INNER_VAR); if (newvar) return (Node *) newvar; } diff --git a/src/backend/optimizer/util/clauses.c b/src/backend/optimizer/util/clauses.c index 95134bfeda3ee..86a979982d328 100644 --- a/src/backend/optimizer/util/clauses.c +++ b/src/backend/optimizer/util/clauses.c @@ -377,6 +377,71 @@ contain_subplans_walker(Node *node, void *context) } +/* + * expr_contains + * Is 'expr', or some subexpression of it, equal() to 'sub'? + */ +static bool +expr_contains_walker(Node *node, void *context) +{ + if (node == NULL) + return false; + if (equal(node, context)) + return true; + return expression_tree_walker(node, expr_contains_walker, context); +} + +bool +expr_contains(Node *expr, Node *sub) +{ + return expr_contains_walker(expr, sub); +} + +/* + * strip_nullingrels + * A copy of 'node' with every Var's and PlaceHolderVar's nullingrels + * cleared. + * + * Used to compare an expression read below an outer join with the query's + * copy above it (see query_tlist_exprs() in pathnode.c and + * search_indexed_tlist_for_nulled_expr() in setrefs.c). + * remove_nulling_relids() can't do this: it tests each Var's varno with + * bms_is_member(), which errors out on the negative special varnos, such as + * the ROWID_VAR a MERGE's targetlist contains. + */ +static Node * +strip_nullingrels_mutator(Node *node, void *context) +{ + if (node == NULL) + return NULL; + if (IsA(node, Var)) + { + Var *var = (Var *) node; + + if (bms_is_empty(var->varnullingrels)) + return node; + var = copyObject(var); + var->varnullingrels = NULL; + return (Node *) var; + } + if (IsA(node, PlaceHolderVar)) + { + PlaceHolderVar *phv; + + phv = (PlaceHolderVar *) + expression_tree_mutator(node, strip_nullingrels_mutator, context); + phv->phnullingrels = NULL; + return (Node *) phv; + } + return expression_tree_mutator(node, strip_nullingrels_mutator, context); +} + +Node * +strip_nullingrels(Node *node) +{ + return strip_nullingrels_mutator(node, NULL); +} + /***************************************************************************** * Check clauses for mutable functions *****************************************************************************/ diff --git a/src/backend/optimizer/util/pathnode.c b/src/backend/optimizer/util/pathnode.c index 67c47fd7c6c48..062c8d7476ea1 100644 --- a/src/backend/optimizer/util/pathnode.c +++ b/src/backend/optimizer/util/pathnode.c @@ -15,10 +15,12 @@ #include "postgres.h" #include "access/htup_details.h" +#include "catalog/pg_type.h" #include "executor/nodeSetOp.h" #include "foreign/fdwapi.h" #include "miscadmin.h" #include "nodes/extensible.h" +#include "nodes/nodeFuncs.h" #include "optimizer/appendinfo.h" #include "optimizer/clauses.h" #include "optimizer/cost.h" @@ -28,6 +30,7 @@ #include "optimizer/planmain.h" #include "optimizer/tlist.h" #include "parser/parsetree.h" +#include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/selfuncs.h" @@ -383,6 +386,204 @@ set_cheapest(RelOptInfo *parent_rel) parent_rel->cheapest_parameterized_paths = parameterized_paths; } +/* + * path_extras_start + * Where the values a path emits beyond its rel's reltarget start in its + * pathtarget's exprs, or NULL if it emits none; *nextras is their number. + * + * A path normally emits rel->reltarget, the Vars and PlaceHolderVars the + * rest of the query needs. An index path may also emit a non-Var + * expression the query needs that it has for free: an index-only scan reads + * a returnable index expression column (see indexonly_path_target()), and an + * ordering IndexScan has its ORDER BY values (see + * index_path_orderby_target()). A join emits whatever its inputs emit (see + * join_path_target()), and so may an Append whose children all emit the + * same values (see add_extras_append_path()). Such a target is a copy of + * rel->reltarget with the extra expressions appended, so they're its tail. + * + * Sort, Material and Memoize don't project and share their input's target, + * so they emit what it emits; so may a Gather or Gather Merge given its + * input's target (see generate_gather_paths()). Nothing else emits extra + * values: other path types build their own targets, and an upper rel's + * targets differ from its reltarget for reasons of their own. + */ +static ListCell * +path_extras_start(Path *path, int *nextras) +{ + RelOptInfo *rel = path->parent; + PathTarget *reltarget = rel->reltarget; + int nrel; + + *nextras = 0; + if (path->pathtarget == reltarget) + return NULL; + + switch (nodeTag(path)) + { + case T_SortPath: + case T_IncrementalSortPath: + case T_MaterialPath: + case T_MemoizePath: + case T_GatherPath: + case T_GatherMergePath: + { + Path *subpath; + + if (IsA(path, MaterialPath)) + subpath = ((MaterialPath *) path)->subpath; + else if (IsA(path, MemoizePath)) + subpath = ((MemoizePath *) path)->subpath; + else if (IsA(path, GatherPath)) + subpath = ((GatherPath *) path)->subpath; + else if (IsA(path, GatherMergePath)) + subpath = ((GatherMergePath *) path)->subpath; + else + subpath = ((SortPath *) path)->subpath; + if (subpath->parent != rel || + subpath->pathtarget != path->pathtarget) + return NULL; + return path_extras_start(subpath, nextras); + } + case T_IndexPath: + if (!IS_SIMPLE_REL(rel)) + return NULL; + break; + case T_NestPath: + case T_MergePath: + case T_HashPath: + if (!IS_JOIN_REL(rel)) + return NULL; + break; + case T_AppendPath: + case T_MergeAppendPath: + /* see add_extras_append_path() */ + if (rel->reloptkind != RELOPT_BASEREL) + return NULL; + break; + default: + return NULL; + } + + /* + * Our targets are a copy of reltarget with the values appended. Anything + * else (say, a final target that apply_projection_to_path() put into an + * index path) isn't ours. + */ + nrel = list_length(reltarget->exprs); + if (list_length(path->pathtarget->exprs) <= nrel) + return NULL; + for (int i = 0; i < nrel; i++) + { + if (list_nth(path->pathtarget->exprs, i) != + list_nth(reltarget->exprs, i)) + return NULL; + } + *nextras = list_length(path->pathtarget->exprs) - nrel; + return list_nth_cell(path->pathtarget->exprs, nrel); +} + +/* + * path_emits_extras + * Does the path emit any values beyond its rel's reltarget? + */ +bool +path_emits_extras(Path *path) +{ + int n; + + return path_extras_start(path, &n) != NULL; +} + +/* + * path_extra_exprs + * The values a path emits beyond its rel's reltarget (see + * path_extras_start()), as a new list. + */ +List * +path_extra_exprs(Path *path) +{ + int n; + ListCell *start = path_extras_start(path, &n); + + if (start == NULL) + return NIL; + return list_copy_tail(path->pathtarget->exprs, + list_length(path->pathtarget->exprs) - n); +} + +/* + * extras_subset + * Is each of the 'n1' values from 'c1' among the 'n2' from 'c2'? + */ +static bool +extras_subset(List *l1, ListCell *c1, int n1, List *l2, ListCell *c2, int n2) +{ + int i1 = list_cell_number(l1, c1); + int i2 = list_cell_number(l2, c2); + + for (int a = 0; a < n1; a++) + { + Node *x = list_nth(l1, i1 + a); + bool found = false; + + for (int b = 0; b < n2 && !found; b++) + found = equal(x, list_nth(l2, i2 + b)); + if (!found) + return false; + } + return true; +} + +/* + * compare_path_extras + * Compare the extra values two paths emit (see path_extra_exprs()). + * + * A path that emits a superset of what the other emits is "better" in the + * same sense as better-sorted: whatever needs the other path's values can + * use this one's instead. The result uses the PathKeysComparison codes so + * add_path() can combine it with the pathkeys comparison. + */ +static PathKeysComparison +compare_path_extras(Path *path1, Path *path2) +{ + int n1, + n2; + ListCell *c1 = path_extras_start(path1, &n1); + ListCell *c2 = path_extras_start(path2, &n2); + List *l1 = path1->pathtarget->exprs; + List *l2 = path2->pathtarget->exprs; + bool sub12, + sub21; + + if (n1 == 0 && n2 == 0) + return PATHKEYS_EQUAL; + sub12 = n1 == 0 || (n2 > 0 && extras_subset(l1, c1, n1, l2, c2, n2)); + sub21 = n2 == 0 || (n1 > 0 && extras_subset(l2, c2, n2, l1, c1, n1)); + if (sub12 && sub21) + return PATHKEYS_EQUAL; + if (sub21) + return PATHKEYS_BETTER1; + if (sub12) + return PATHKEYS_BETTER2; + return PATHKEYS_DIFFERENT; +} + +/* + * combine_comparisons + * Combine two PathKeysComparison results into one. + * + * One path is better overall only if it's better or equal on both counts. + */ +static PathKeysComparison +combine_comparisons(PathKeysComparison a, PathKeysComparison b) +{ + if (a == PATHKEYS_EQUAL) + return b; + if (b == PATHKEYS_EQUAL || a == b) + return a; + return PATHKEYS_DIFFERENT; +} + /* * add_path * Consider a potential implementation path for the specified parent rel, @@ -391,7 +592,11 @@ set_cheapest(RelOptInfo *parent_rel) * A path is worthy if it has a better sort order (better pathkeys) or * cheaper cost (as defined below), or generates fewer rows, than any * existing path that has the same or superset parameterization rels. We - * also consider parallel-safe paths more worthy than others. + * also consider parallel-safe paths more worthy than others. And a path + * that emits extra values the query needs (see path_extra_exprs()) is + * better, like a better-sorted one, than a path that emits a subset of + * them: whatever would compute those values above the other path can + * read them from this one. * * Cheaper cost can mean either a cheaper total cost or a cheaper startup * cost; if one path is cheaper in one of these aspects and another is @@ -510,6 +715,8 @@ add_path(RelOptInfo *parent_rel, Path *new_path) old_path_pathkeys = old_path->param_info ? NIL : old_path->pathkeys; keyscmp = compare_pathkeys(new_path_pathkeys, old_path_pathkeys); + keyscmp = combine_comparisons(keyscmp, + compare_path_extras(new_path, old_path)); if (keyscmp != PATHKEYS_DIFFERENT) { switch (costcmp) @@ -815,8 +1022,10 @@ add_partial_path(RelOptInfo *parent_rel, Path *new_path) bool remove_old = false; /* unless new proves superior */ PathKeysComparison keyscmp; - /* Compare pathkeys. */ + /* Compare pathkeys, and extra values as in add_path(). */ keyscmp = compare_pathkeys(new_path->pathkeys, old_path->pathkeys); + keyscmp = combine_comparisons(keyscmp, + compare_path_extras(new_path, old_path)); /* * Unless pathkeys are incompatible, see if one of the paths dominates @@ -1067,6 +1276,212 @@ create_samplescan_path(PlannerInfo *root, RelOptInfo *rel, Relids required_outer return pathnode; } +/* + * index_path_orderby_target + * Choose the PathTarget for an ordering plain IndexScan path. + * + * Normally an IndexPath emits rel->reltarget, which holds only the Vars (and + * PlaceHolderVars) the rest of the query needs. An ORDER BY expression is + * not in it: it is computed by whatever plan node first needs it. When the + * scan is the top of the scan/join tree that is the scan itself, and + * setrefs.c lets it take the value from its ORDER BY values. But when the + * scan is below a join, the join computes the expression from the Vars, + * once per joined row, though the scan already had the value. + * + * So if the scan can return any of its ORDER BY values (see + * index_orderby_returnable()), and the query needs that expression above + * the scan (it is in the final targetlist, which includes sort columns), + * give the path a copy of rel->reltarget with those expressions appended. + * They cost the scan nothing (path_target_cost() exempts them), and + * setrefs.c's fix_join_expr() matches them in the parent join's expressions, + * replacing its copy with a reference to the scan's output. A join passes + * them on up (see join_path_target()). + * + * Restricted to plain base relations: an appendrel child's ORDER BY values + * reach the scan/join target through the child's own projection (see + * apply_scanjoin_target_to_paths()). Returns rel->reltarget unchanged when + * there's nothing to add, which is the common case, so most paths share it + * as before. + */ +static PathTarget * +index_path_orderby_target(PlannerInfo *root, IndexOptInfo *index, + List *indexorderbys, List *indexorderbycols) +{ + RelOptInfo *rel = index->rel; + PathTarget *target = NULL; + ListCell *lo, + *lcol; + + if (indexorderbys == NIL || rel->reloptkind != RELOPT_BASEREL || + root->processed_tlist == NIL) + return rel->reltarget; + + forboth(lo, indexorderbys, lcol, indexorderbycols) + { + Expr *orderby = (Expr *) lfirst(lo); + bool needed = false; + ListCell *lt; + + if (!index_orderby_returnable(index, lfirst_int(lcol), orderby)) + continue; + + /* + * Not if it's cheap: a join would drop it (see join_path_target()), + * and emitting it would still keep extra paths at this level (see + * expr_worth_emitting()). + */ + if (!expr_worth_emitting(root, (Node *) orderby)) + continue; + + /* + * Only if some top-level targetlist entry is this expression; an + * expression that merely contains it would not be rewritten, and + * emitting the value would just widen the tuple. + */ + foreach(lt, root->processed_tlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lt); + + if (orderby_tlist_match(tle->expr, orderby)) + { + needed = true; + break; + } + } + if (!needed) + continue; + + if (target == NULL) + target = copy_pathtarget(rel->reltarget); + add_new_column_to_pathtarget(target, orderby); + } + + if (target == NULL) + return rel->reltarget; + + /* add_new_column_to_pathtarget doesn't maintain cost and width */ + return set_pathtarget_cost_width(root, target); +} + +/* + * expr_worth_emitting + * Is 'expr' expensive enough to be worth emitting as an extra value? + * + * A path that emits an extra value makes add_path() keep it, and every join + * path built on it, alongside paths it would otherwise have discarded (see + * compare_path_extras()). That costs planning time at every join level + * above. For a cheap expression, computing it again costs less than that, + * so emit only values that cost more than 10 times cpu_operator_cost per + * evaluation, the same test make_sort_input_target() uses to decide that an + * expression is worth postponing past a sort. + */ +bool +expr_worth_emitting(PlannerInfo *root, Node *expr) +{ + QualCost cost; + + cost_qual_eval_node(&cost, expr, root); + return cost.per_tuple > 10 * cpu_operator_cost; +} + +/* + * query_tlist_exprs + * The query's final targetlist expressions, in terms of 'rel'. + * + * root->processed_tlist is in terms of the parent of an appendrel; translate + * it for a child, so it can be compared with the child's index expressions. + */ +List * +query_tlist_exprs(PlannerInfo *root, RelOptInfo *rel) +{ + List *exprs = get_tlist_exprs(root->processed_tlist, true); + + if (rel->reloptkind == RELOPT_OTHER_MEMBER_REL) + exprs = (List *) adjust_appendrel_attrs_multilevel(root, (Node *) exprs, + rel, + rel->top_parent); + + /* + * Above an outer join that null-extends the rel, the query's Vars carry + * nullingrels that a scan's don't. The scan may still emit the value; + * whether it can be used above the join is join_path_target()'s call. + */ + if (!bms_is_empty(root->outer_join_rels)) + exprs = (List *) strip_nullingrels((Node *) exprs); + return exprs; +} + +/* + * indexonly_path_target + * Choose the PathTarget for an index-only scan path. + * + * An index-only scan reads a returnable index expression column instead of + * computing it (set_indexonlyscan_references()), and isn't charged for it + * (path_target_cost()). But that only helps if the path survives + * add_path(), and with rel->reltarget, which has only Vars, nothing + * distinguishes it from a seq scan that will have to compute the expression + * later; it loses on cost before the expression is charged to anyone. So + * if the query's targetlist contains a returnable index expression, give + * the path a copy of rel->reltarget with that expression appended, making + * it an extra value the path emits (see path_extra_exprs()). add_path() + * then won't let a path that lacks it dominate this one on cost alone. + * The value travels up through joins (see join_path_target()), and + * setrefs.c's fix_join_expr() and fix_upper_expr() replace each occurrence, + * at any depth, with a reference to it; the paths that must compute it + * instead pay for it when the expression is charged. + * + * The query's targetlist is compared without nullingrels (see + * query_tlist_exprs()); whether a value read below an outer join can be used + * above it is decided as it's passed up (see relabel_extra_for_join()). + * + * A partial path may emit a value that isn't parallel-safe: a worker reads + * the stored value, it never evaluates the expression, and nothing above + * the scan does either, since it references the scan's output column. + */ +static PathTarget * +indexonly_path_target(PlannerInfo *root, IndexOptInfo *index) +{ + RelOptInfo *rel = index->rel; + PathTarget *target = NULL; + List *tlist_exprs; + ListCell *lt; + int i = 0; + + if (index->indexprs == NIL || !IS_SIMPLE_REL(rel) || + root->processed_tlist == NIL) + return rel->reltarget; + + tlist_exprs = query_tlist_exprs(root, rel); + + foreach(lt, index->indextlist) + { + TargetEntry *itle = lfirst_node(TargetEntry, lt); + ListCell *lq; + + if (!index->canreturn[i++] || IsA(itle->expr, Var)) + continue; + if (!expr_worth_emitting(root, (Node *) itle->expr)) + continue; + + foreach(lq, tlist_exprs) + { + if (expr_contains(lfirst(lq), (Node *) itle->expr)) + { + if (target == NULL) + target = copy_pathtarget(rel->reltarget); + add_new_column_to_pathtarget(target, itle->expr); + break; + } + } + } + + if (target == NULL) + return rel->reltarget; + + /* add_new_column_to_pathtarget doesn't maintain cost and width */ + return set_pathtarget_cost_width(root, target); +} + /* * create_index_path * Creates a path node for an index scan. @@ -1106,7 +1521,10 @@ create_index_path(PlannerInfo *root, pathnode->path.pathtype = indexonly ? T_IndexOnlyScan : T_IndexScan; pathnode->path.parent = rel; - pathnode->path.pathtarget = rel->reltarget; + pathnode->path.pathtarget = indexonly ? + indexonly_path_target(root, index) : + index_path_orderby_target(root, index, indexorderbys, + indexorderbycols); pathnode->path.param_info = get_baserel_parampathinfo(root, rel, required_outer); pathnode->path.parallel_aware = false; @@ -1719,7 +2137,8 @@ create_material_path(RelOptInfo *rel, Path *subpath, bool enabled) pathnode->path.pathtype = T_Material; pathnode->path.parent = rel; - pathnode->path.pathtarget = rel->reltarget; + /* Material doesn't project, so use source path's pathtarget */ + pathnode->path.pathtarget = subpath->pathtarget; pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && @@ -1755,7 +2174,8 @@ create_memoize_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, pathnode->path.pathtype = T_Memoize; pathnode->path.parent = rel; - pathnode->path.pathtarget = rel->reltarget; + /* Memoize doesn't project, so use source path's pathtarget */ + pathnode->path.pathtarget = subpath->pathtarget; pathnode->path.param_info = subpath->param_info; pathnode->path.parallel_aware = false; pathnode->path.parallel_safe = rel->consider_parallel && @@ -2337,6 +2757,180 @@ calc_non_nestloop_required_outer(Path *outer_path, Path *inner_path) return required_outer; } +/* state for relabel_extra_for_join()'s walker and mutator */ +typedef struct +{ + List *joinvars; + bool changed; + bool missing; +} relabel_context; + +static Var * +relabel_find_joinvar(Var *var, List *joinvars) +{ + ListCell *lc; + + foreach(lc, joinvars) + { + Var *jv = (Var *) lfirst(lc); + + if (IsA(jv, Var) && jv->varno == var->varno && + jv->varattno == var->varattno && jv->varlevelsup == 0) + return jv; + } + return NULL; +} + +/* First pass: does anything need relabeling, or is a Var missing? */ +static bool +relabel_check_walker(Node *node, relabel_context *context) +{ + if (node == NULL) + return false; + if (IsA(node, Var) && ((Var *) node)->varlevelsup == 0) + { + Var *var = (Var *) node; + Var *jv = relabel_find_joinvar(var, context->joinvars); + + if (jv == NULL) + context->missing = true; + else if (!bms_equal(jv->varnullingrels, var->varnullingrels)) + context->changed = true; + return context->missing; + } + return expression_tree_walker(node, relabel_check_walker, context); +} + +static Node * +relabel_extra_mutator(Node *node, relabel_context *context) +{ + if (node == NULL) + return NULL; + if (IsA(node, Var) && ((Var *) node)->varlevelsup == 0) + { + Var *var = copyObject((Var *) node); + Var *jv = relabel_find_joinvar(var, context->joinvars); + + var->varnullingrels = bms_copy(jv->varnullingrels); + return (Node *) var; + } + return expression_tree_mutator(node, relabel_extra_mutator, context); +} + +/* + * relabel_extra_for_join + * Express an input's extra value in terms of the joinrel's Vars. + * + * Above an outer join, the joinrel's reltarget has each Var of the nullable + * side with the join's bit added to its nullingrels, and the query's copy of + * an expression over those Vars has it too. The input's copy doesn't. The + * join null-extends the input's value along with the Vars, which gives the + * expression's value for null-extended rows only if the expression is + * strict, so for a non-strict one we can't pass the value up (return NULL). + * Otherwise return the expression with each Var replaced by the joinrel's + * version of it, which setrefs.c matches with the input's + * (search_indexed_tlist_for_nulled_expr()). If some Var isn't in the + * joinrel's reltarget, the value isn't needed above, so return NULL too. + */ +static Node * +relabel_extra_for_join(RelOptInfo *joinrel, Node *expr) +{ + relabel_context context; + + context.joinvars = joinrel->reltarget->exprs; + context.changed = false; + context.missing = false; + (void) relabel_check_walker(expr, &context); + if (context.missing) + return NULL; + if (!context.changed) + return expr; + if (contain_nonstrict_functions(expr)) + return NULL; + return relabel_extra_mutator(expr, &context); +} + +/* + * join_path_target + * Choose the PathTarget for a join path. + * + * A join normally emits joinrel->reltarget. If an input emits extra values + * (see path_extra_exprs()), the join passes them up too, so that a higher + * join or the final scan/join target can use them instead of computing them + * (fix_join_expr() matches them in the inputs' targetlists, and + * join_free_exprs() credits them). Without that, an index-only scan or + * ordering IndexScan below the topmost join would read the value for + * nothing. + * + * Above an outer join a value from the nullable side is relabeled with the + * join's nullingrels, and passed up only if that gives the right answer for + * null-extended rows; see relabel_extra_for_join(). + * + * A value needn't be parallel-safe for a partial join path: the join only + * references its input's output column. Every value a scan emits passed + * expr_worth_emitting() there, so we don't look up its cost again for each + * join path. + */ +static PathTarget * +join_path_target(PlannerInfo *root, RelOptInfo *joinrel, + Path *outer_path, Path *inner_path) +{ + List *outer_extras = path_extra_exprs(outer_path); + List *inner_extras = path_extra_exprs(inner_path); + int nextras PG_USED_FOR_ASSERTS_ONLY; + PathTarget *target; + bool all_kept = true; + ListCell *lc; + + if (outer_extras == NIL && inner_extras == NIL) + return joinrel->reltarget; + + target = copy_pathtarget(joinrel->reltarget); + nextras = list_length(outer_extras) + list_length(inner_extras); + foreach(lc, list_concat(outer_extras, inner_extras)) + { + Node *expr = relabel_extra_for_join(joinrel, (Node *) lfirst(lc)); + + if (expr != NULL) + add_new_column_to_pathtarget(target, (Expr *) expr); + else + all_kept = false; + } + if (list_length(target->exprs) == list_length(joinrel->reltarget->exprs)) + return joinrel->reltarget; + + /* + * The values' cost and width are what they added to the inputs' targets, + * since the two inputs' values refer to different rels and can't overlap. + * Working that out is much cheaper than set_pathtarget_cost_width(), + * which looks up each function's cost again. + */ + if (all_kept) + { + Path *inputs[2] = {outer_path, inner_path}; + + /* + * Each one added a column: an input's values are distinct, the two + * inputs' refer to different rels, and a join rel's reltarget holds + * only Vars and PlaceHolderVars. + */ + Assert(list_length(target->exprs) == + list_length(joinrel->reltarget->exprs) + nextras); + + for (int i = 0; i < 2; i++) + { + PathTarget *pt = inputs[i]->pathtarget; + PathTarget *rt = inputs[i]->parent->reltarget; + + target->cost.startup += pt->cost.startup - rt->cost.startup; + target->cost.per_tuple += pt->cost.per_tuple - rt->cost.per_tuple; + target->width += pt->width - rt->width; + } + return target; + } + return set_pathtarget_cost_width(root, target); +} + /* * create_nestloop_path * Creates a pathnode corresponding to a nestloop join between two @@ -2405,7 +2999,9 @@ create_nestloop_path(PlannerInfo *root, pathnode->jpath.path.pathtype = T_NestLoop; pathnode->jpath.path.parent = joinrel; - pathnode->jpath.path.pathtarget = joinrel->reltarget; + pathnode->jpath.path.pathtarget = join_path_target(root, joinrel, + outer_path, + inner_path); pathnode->jpath.path.param_info = get_joinrel_parampathinfo(root, joinrel, @@ -2471,7 +3067,9 @@ create_mergejoin_path(PlannerInfo *root, pathnode->jpath.path.pathtype = T_MergeJoin; pathnode->jpath.path.parent = joinrel; - pathnode->jpath.path.pathtarget = joinrel->reltarget; + pathnode->jpath.path.pathtarget = join_path_target(root, joinrel, + outer_path, + inner_path); pathnode->jpath.path.param_info = get_joinrel_parampathinfo(root, joinrel, @@ -2536,7 +3134,9 @@ create_hashjoin_path(PlannerInfo *root, pathnode->jpath.path.pathtype = T_HashJoin; pathnode->jpath.path.parent = joinrel; - pathnode->jpath.path.pathtarget = joinrel->reltarget; + pathnode->jpath.path.pathtarget = join_path_target(root, joinrel, + outer_path, + inner_path); pathnode->jpath.path.param_info = get_joinrel_parampathinfo(root, joinrel, @@ -2577,6 +3177,364 @@ create_hashjoin_path(PlannerInfo *root, return pathnode; } +/* + * sort_pathlist_by_cost + * Restore the ordering add_path() and add_partial_path() maintain + * (fewest disabled nodes, then lowest total cost) after the costs of + * paths already in the list were changed in place. + * + * This is a stable insertion sort: it costs one pass when the list is still + * in order, which is the usual case, and keeps equal-cost paths in the order + * they were added. + */ +void +sort_pathlist_by_cost(List *pathlist) +{ + for (int i = 1; i < list_length(pathlist); i++) + { + Path *path = list_nth(pathlist, i); + int j; + + for (j = i; j > 0; j--) + { + Path *prev = list_nth(pathlist, j - 1); + + if (prev->disabled_nodes < path->disabled_nodes || + (prev->disabled_nodes == path->disabled_nodes && + prev->total_cost <= path->total_cost)) + break; + lfirst(list_nth_cell(pathlist, j)) = prev; + } + lfirst(list_nth_cell(pathlist, j)) = path; + } +} + +/* + * index_orderby_returnable + * Will an IndexScan on 'index' hand back the value of ORDER BY expression + * 'orderby', bound to index column 'indexcol', to its targetlist? + * + * create_indexscan_plan() records this per key for setrefs.c, and + * path_target_cost() uses it to cost the path, so that the cost we assign to + * a path is the cost of the plan we build from it. The opclass must promise + * exact values (amcanreturnorderby), and the expression must be float8 or + * float4: index_store_float8_orderby_distances() hands the executor float8 + * distances, rounding them for a float4 key, and can't deliver other types. + */ +bool +index_orderby_returnable(IndexOptInfo *index, int indexcol, Expr *orderby) +{ + Oid typ = exprType((Node *) orderby); + + if (typ != FLOAT8OID && typ != FLOAT4OID) + return false; + return (index->amcanreturnorderby != NULL && + index->amcanreturnorderby(index->opfamily[indexcol], + index->opcintype[indexcol], + castNode(OpExpr, orderby)->opno)); +} + +/* + * orderby_tlist_match + * Is targetlist expression 'expr' the value of ORDER BY expression + * 'orderby'? + * + * 'orderby' is an IndexPath's indexorderbys entry, which has the index key + * on the left; match_clause_to_ordering_op() commutes "const OP key" into + * that form. So a targetlist entry that the user wrote in the original + * "const OP key" form is the same value but not equal() to it. Accept it + * if it is the commuted operator applied to the swapped arguments; an + * operator's commutator yields the same result by definition. + * + * Shared by path_target_cost() and setrefs.c's replace_orderby_tlist_refs() + * so that what is costed is what is built. + */ +bool +orderby_tlist_match(Expr *expr, Expr *orderby) +{ + OpExpr *a, + *b; + + if (equal(expr, orderby)) + return true; + + if (!IsA(expr, OpExpr) || !IsA(orderby, OpExpr)) + return false; + a = (OpExpr *) expr; + b = (OpExpr *) orderby; + if (list_length(a->args) != 2 || list_length(b->args) != 2) + return false; + if (a->opresulttype != b->opresulttype || + a->opcollid != b->opcollid || + a->inputcollid != b->inputcollid || + get_commutator(a->opno) != b->opno) + return false; + + return equal(linitial(a->args), lsecond(b->args)) && + equal(lsecond(a->args), linitial(b->args)); +} + +/* state for credit_free_exprs_walker() */ +typedef struct +{ + PlannerInfo *root; + List *free; + bool nulled_ok; /* see free_expr_match() */ + List *free_stripped; /* 'free' without nullingrels, on demand */ + QualCost cost; +} credit_free_context; + +/* + * free_expr_match + * Is 'node' one of context->free? + * + * At an outer join (context->nulled_ok), also accept a strict 'node' that is + * one of them but for nullingrels, as setrefs.c does. + */ +static bool +free_expr_match(Node *node, credit_free_context *context) +{ + Node *stripped; + ListCell *lc; + + if (list_member(context->free, node)) + return true; + if (!context->nulled_ok || bms_is_empty(context->root->outer_join_rels)) + return false; + if (context->free_stripped == NIL) + context->free_stripped = (List *) strip_nullingrels((Node *) context->free); + stripped = strip_nullingrels(node); + foreach(lc, context->free_stripped) + { + if (equal(stripped, lfirst(lc))) + return !contain_nonstrict_functions(node); + } + return false; +} + +/* + * credit_free_exprs_walker + * Subtract from context->cost the cost of each outermost subexpression + * of 'node' equal() to one of context->free. + * + * setrefs.c's fix_upper_expr() and fix_join_expr() try to match every + * non-Var subexpression against the input's targetlist before descending + * into it, and replace a match with a reference to the input's column. So + * an expression costs nothing where an input emits it, wherever it appears + * in the expression, and its own subexpressions aren't evaluated either. + */ +static bool +credit_free_exprs_walker(Node *node, credit_free_context *context) +{ + if (node == NULL || IsA(node, Var) || IsA(node, Const)) + return false; + if (free_expr_match(node, context)) + { + QualCost ecost; + + cost_qual_eval_node(&ecost, node, context->root); + context->cost.startup -= ecost.startup; + context->cost.per_tuple -= ecost.per_tuple; + return false; /* don't count its subexpressions twice */ + } + return expression_tree_walker(node, credit_free_exprs_walker, context); +} + +/* + * free_exprs_cost + * target->cost, less the cost of the values in 'free' that 'target' + * uses (see credit_free_exprs_walker()). + */ +static QualCost +free_exprs_cost(PlannerInfo *root, PathTarget *target, List *free, + bool nulled_ok) +{ + credit_free_context context; + + context.root = root; + context.free = free; + context.nulled_ok = nulled_ok; + context.free_stripped = NIL; + context.cost = target->cost; + if (free != NIL) + (void) credit_free_exprs_walker((Node *) target->exprs, &context); + return context.cost; +} + +/* + * indexonly_free_exprs + * The returnable expression columns of an index-only scan's index. + * + * set_indexonlyscan_references() replaces each occurrence of one of these + * in the scan's targetlist and quals with a reference to the index column. + */ +static List * +indexonly_free_exprs(IndexPath *ipath) +{ + IndexOptInfo *index = ipath->indexinfo; + List *result = NIL; + ListCell *lt; + int i = 0; + + if (index->indexprs == NIL) + return NIL; + foreach(lt, index->indextlist) + { + TargetEntry *tle = lfirst_node(TargetEntry, lt); + + if (index->canreturn[i++] && !IsA(tle->expr, Var)) + result = lappend(result, tle->expr); + } + return result; +} + +/* + * indexonly_qual_cost + * 'cost', the cost of an index-only scan's qpquals, less the returnable + * index expressions they use. + * + * set_indexonlyscan_references() rewrites the scan's quals as it does its + * targetlist. + */ +QualCost +indexonly_qual_cost(PlannerInfo *root, IndexPath *ipath, List *qpquals, + QualCost cost) +{ + credit_free_context context; + ListCell *lc; + + context.root = root; + context.free = indexonly_free_exprs(ipath); + context.nulled_ok = false; + context.free_stripped = NIL; + context.cost = cost; + if (context.free == NIL) + return cost; + foreach(lc, qpquals) + { + Node *qual = (Node *) lfirst(lc); + + if (IsA(qual, RestrictInfo)) + qual = (Node *) ((RestrictInfo *) qual)->clause; + (void) credit_free_exprs_walker(qual, &context); + } + return context.cost; +} + +/* + * join_free_exprs + * The values a join's inputs emit beyond their reltargets. + * + * fix_join_expr() replaces each occurrence of one of these in the join's + * targetlist with a reference to the input's output. Material and Memoize + * pass their input's targetlist up unchanged; path_extra_exprs() already + * looks through them. + */ +static List * +join_free_exprs(JoinPath *jpath) +{ + return list_concat_unique(path_extra_exprs(jpath->outerjoinpath), + path_extra_exprs(jpath->innerjoinpath)); +} + +/* + * path_target_cost + * Return the cost 'path' pays to evaluate 'target'. + * + * Normally that's just target->cost. But a path doesn't pay for a value + * it has for free: + * + * - a join, for values one of its inputs emits (join_free_exprs()); + * - an index-only scan, for returnable index expression columns + * (indexonly_free_exprs()); + * - a Gather or Gather Merge, for values its input emits; + * + * in either case wherever the value appears in the target, since setrefs.c + * replaces every occurrence (see credit_free_exprs_walker()). And + * + * - a plain IndexScan, for a top-level target entry that is one of its + * returnable ORDER BY expressions (see orderby_tlist_match()), which + * setrefs.c's replace_orderby_tlist_refs() takes from its ORDER BY values. + * Only top-level entries of the scan's own targetlist are rewritten there, + * so only those count here; above the scan, a join or upper node that + * reads the value is credited for it at any depth, as above. + */ +QualCost +path_target_cost(PlannerInfo *root, Path *path, PathTarget *target) +{ + QualCost cost = target->cost; + IndexPath *ipath; + ListCell *lc; + + if (IsA(path, NestPath) || IsA(path, MergePath) || IsA(path, HashPath)) + { + JoinPath *jpath = (JoinPath *) path; + bool forms_oj; + + /* + * A join that forms outer joins matches nulled copies too, as + * set_join_references() does when the Join's ojrelids isn't empty. + * The inputs' relids are disjoint and both within the joinrel's, so + * it forms one exactly when the joinrel has more relids than they do. + */ + forms_oj = bms_num_members(path->parent->relids) > + bms_num_members(jpath->outerjoinpath->parent->relids) + + bms_num_members(jpath->innerjoinpath->parent->relids); + return free_exprs_cost(root, target, join_free_exprs(jpath), + forms_oj); + } + + if (IsA(path, IndexPath) && path->pathtype == T_IndexOnlyScan) + return free_exprs_cost(root, target, + indexonly_free_exprs((IndexPath *) path), + false); + + /* set_upper_references() matches them in a Gather's subplan's tlist */ + if (IsA(path, GatherPath)) + return free_exprs_cost(root, target, + path_extra_exprs(((GatherPath *) path)->subpath), + false); + if (IsA(path, GatherMergePath)) + return free_exprs_cost(root, target, + path_extra_exprs(((GatherMergePath *) path)->subpath), + false); + + if (!IsA(path, IndexPath) || path->pathtype != T_IndexScan) + return cost; + ipath = (IndexPath *) path; + if (ipath->indexorderbys == NIL) + return cost; + + foreach(lc, target->exprs) + { + Node *expr = (Node *) lfirst(lc); + ListCell *lo, + *lcol; + + if (IsA(expr, Var)) + continue; /* costs nothing anyway */ + + forboth(lo, ipath->indexorderbys, lcol, ipath->indexorderbycols) + { + Expr *orderby = (Expr *) lfirst(lo); + + if (index_orderby_returnable(ipath->indexinfo, lfirst_int(lcol), + orderby) && + orderby_tlist_match((Expr *) expr, orderby)) + { + QualCost ecost; + + cost_qual_eval_node(&ecost, expr, root); + cost.startup -= ecost.startup; + cost.per_tuple -= ecost.per_tuple; + break; + } + } + } + + return cost; +} + /* * create_projection_path * Creates a pathnode that represents performing a projection. @@ -2637,6 +3595,9 @@ create_projection_path(PlannerInfo *root, if (is_projection_capable_path(subpath) || equal(oldtarget->exprs, target->exprs)) { + QualCost oldcost = path_target_cost(root, subpath, oldtarget); + QualCost newcost = path_target_cost(root, subpath, target); + /* No separate Result node needed */ pathnode->dummypp = true; @@ -2646,10 +3607,10 @@ create_projection_path(PlannerInfo *root, pathnode->path.rows = subpath->rows; pathnode->path.disabled_nodes = subpath->disabled_nodes; pathnode->path.startup_cost = subpath->startup_cost + - (target->cost.startup - oldtarget->cost.startup); + (newcost.startup - oldcost.startup); pathnode->path.total_cost = subpath->total_cost + - (target->cost.startup - oldtarget->cost.startup) + - (target->cost.per_tuple - oldtarget->cost.per_tuple) * subpath->rows; + (newcost.startup - oldcost.startup) + + (newcost.per_tuple - oldcost.per_tuple) * subpath->rows; } else { @@ -2701,6 +3662,7 @@ apply_projection_to_path(PlannerInfo *root, PathTarget *target) { QualCost oldcost; + QualCost newcost; /* * If given path can't project, we might need a Result node, so make a @@ -2713,12 +3675,13 @@ apply_projection_to_path(PlannerInfo *root, * We can just jam the desired tlist into the existing path, being sure to * update its cost estimates appropriately. */ - oldcost = path->pathtarget->cost; + oldcost = path_target_cost(root, path, path->pathtarget); + newcost = path_target_cost(root, path, target); path->pathtarget = target; - path->startup_cost += target->cost.startup - oldcost.startup; - path->total_cost += target->cost.startup - oldcost.startup + - (target->cost.per_tuple - oldcost.per_tuple) * path->rows; + path->startup_cost += newcost.startup - oldcost.startup; + path->total_cost += newcost.startup - oldcost.startup + + (newcost.per_tuple - oldcost.per_tuple) * path->rows; /* * If the path happens to be a Gather or GatherMerge path, we'd like to diff --git a/src/backend/optimizer/util/plancat.c b/src/backend/optimizer/util/plancat.c index 22e0e8b11f8d5..bd496d71a2ca2 100644 --- a/src/backend/optimizer/util/plancat.c +++ b/src/backend/optimizer/util/plancat.c @@ -316,6 +316,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, amroutine->amrestrpos != NULL); info->amcostestimate = amroutine->amcostestimate; Assert(info->amcostestimate != NULL); + info->amcanreturnorderby = amroutine->amcanreturnorderby; /* Fetch index opclass options */ info->opclassoptions = RelationGetIndexAttOptions(indexRelation, true); @@ -414,6 +415,7 @@ get_relation_info(PlannerInfo *root, Oid relationObjectId, bool inhparent, info->amhasgetbitmap = false; info->amcanmarkpos = false; info->amcostestimate = NULL; + info->amcanreturnorderby = NULL; info->sortopfamily = NULL; info->reverse_sort = NULL; diff --git a/src/backend/utils/adt/ruleutils.c b/src/backend/utils/adt/ruleutils.c index ccfd7f4728f32..173c6211c53b6 100644 --- a/src/backend/utils/adt/ruleutils.c +++ b/src/backend/utils/adt/ruleutils.c @@ -7999,6 +7999,24 @@ resolve_special_varno(Node *node, deparse_context *context, context->appendparents = save_appendparents; return; } + else if (var->varno == INNER_VAR && dpns->plan != NULL && + IsA(dpns->plan, IndexScan) && + ((IndexScan *) dpns->plan)->indexorderbynused > 0) + { + /* + * An IndexScan whose targetlist reads its own ORDER BY values (see + * replace_orderby_tlist_refs in setrefs.c) uses INNER_VAR Vars + * numbered by ORDER BY key; they deparse as the ORDER BY expressions. + */ + List *orderbys = ((IndexScan *) dpns->plan)->indexorderbyorig; + + if (var->varattno < 1 || var->varattno > list_length(orderbys)) + elog(ERROR, "bogus varattno for ORDER BY value var: %d", + var->varattno); + resolve_special_varno((Node *) list_nth(orderbys, var->varattno - 1), + context, callback, callback_arg); + return; + } else if (var->varno == INNER_VAR && dpns->inner_tlist) { TargetEntry *tle; diff --git a/src/include/access/amapi.h b/src/include/access/amapi.h index 7924033353031..f520ba06e6a77 100644 --- a/src/include/access/amapi.h +++ b/src/include/access/amapi.h @@ -144,6 +144,14 @@ typedef IndexBulkDeleteResult *(*amvacuumcleanup_function) (IndexVacuumInfo *inf /* can indexscan return IndexTuples? */ typedef bool (*amcanreturn_function) (Relation indexRelation, int attno); +/* + * Are the ORDER BY distances an opclass reports for ordering operator + * 'orderbyop' on an exact (xs_recheckorderby = false) leaf entry the + * operator's own result? + */ +typedef bool (*amcanreturnorderby_function) (Oid opfamily, Oid opcintype, + Oid orderbyop); + /* estimate cost of an indexscan */ typedef void (*amcostestimate_function) (PlannerInfo *root, IndexPath *path, @@ -300,6 +308,7 @@ typedef struct IndexAmRoutine ambulkdelete_function ambulkdelete; amvacuumcleanup_function amvacuumcleanup; amcanreturn_function amcanreturn; /* can be NULL */ + amcanreturnorderby_function amcanreturnorderby; /* can be NULL */ amcostestimate_function amcostestimate; amgettreeheight_function amgettreeheight; /* can be NULL */ amoptions_function amoptions; diff --git a/src/include/access/gist_private.h b/src/include/access/gist_private.h index 39fdd27f5b97c..d2b3b5d7ea36d 100644 --- a/src/include/access/gist_private.h +++ b/src/include/access/gist_private.h @@ -462,6 +462,7 @@ extern void gistkillitems(IndexScanDesc scan); extern bool gistgettuple(IndexScanDesc scan, ScanDirection dir); extern int64 gistgetbitmap(IndexScanDesc scan, TIDBitmap *tbm); extern bool gistcanreturn(Relation index, int attno); +extern bool gistcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop); /* gistvalidate.c */ extern bool gistvalidate(Oid opclassoid); diff --git a/src/include/access/spgist.h b/src/include/access/spgist.h index 083d93f8ffdc0..f293c14455b68 100644 --- a/src/include/access/spgist.h +++ b/src/include/access/spgist.h @@ -210,6 +210,7 @@ extern void spgrescan(IndexScanDesc scan, ScanKey scankey, int nscankeys, extern int64 spggetbitmap(IndexScanDesc scan, TIDBitmap *tbm); extern bool spggettuple(IndexScanDesc scan, ScanDirection dir); extern bool spgcanreturn(Relation index, int attno); +extern bool spgcanreturnorderby(Oid opfamily, Oid opcintype, Oid orderbyop); /* spgvacuum.c */ extern IndexBulkDeleteResult *spgbulkdelete(IndexVacuumInfo *info, diff --git a/src/include/nodes/execnodes.h b/src/include/nodes/execnodes.h index fe8681ba642e7..729a39761d94a 100644 --- a/src/include/nodes/execnodes.h +++ b/src/include/nodes/execnodes.h @@ -1722,6 +1722,7 @@ typedef struct * OrderByTypByVals is the datatype of order by expression pass-by-value? * OrderByTypLens typlens of the datatypes of order by expressions * PscanLen size of parallel index scan descriptor + * OrderBySlot returned row's ORDER BY values, for the targetlist * ---------------- */ typedef struct IndexScanState @@ -1751,6 +1752,13 @@ typedef struct IndexScanState bool *iss_OrderByTypByVals; int16 *iss_OrderByTypLens; Size iss_PscanLen; + + /* + * When the plan's targetlist reads the ORDER BY values (indexorderbynused + * > 0), the values of the row being returned, as a virtual slot installed + * as the projection's inner tuple. + */ + TupleTableSlot *iss_OrderBySlot; } IndexScanState; /* ---------------- diff --git a/src/include/nodes/pathnodes.h b/src/include/nodes/pathnodes.h index 8615db98a551b..8f833c40f4e19 100644 --- a/src/include/nodes/pathnodes.h +++ b/src/include/nodes/pathnodes.h @@ -1447,6 +1447,8 @@ typedef struct IndexOptInfo /* AM's cost estimator */ /* Rather than include amapi.h here, we declare amcostestimate like this */ void (*amcostestimate) (struct PlannerInfo *, struct IndexPath *, double, Cost *, Cost *, Selectivity *, double *, double *) pg_node_attr(read_write_ignore); + /* AM's amcanreturnorderby, or NULL */ + bool (*amcanreturnorderby) (Oid, Oid, Oid) pg_node_attr(read_write_ignore); } IndexOptInfo; /* @@ -3617,6 +3619,9 @@ typedef struct SemiAntiJoinFactors * RIGHT_ANTI/inner_unique joins) * param_source_rels are OK targets for parameterization of result paths * pgs_mask is a bitmask of PGS_* constants to limit the join strategy + * extras_outer and extras_inner are each input rel's cheapest unparameterized + * path that emits extra values, if that isn't its cheapest path anyway + * (see cheapest_extras_path() in joinpath.c), else NULL */ typedef struct JoinPathExtraData { @@ -3627,6 +3632,8 @@ typedef struct JoinPathExtraData SemiAntiJoinFactors semifactors; Relids param_source_rels; uint64 pgs_mask; + Path *extras_outer; + Path *extras_inner; } JoinPathExtraData; /* diff --git a/src/include/nodes/plannodes.h b/src/include/nodes/plannodes.h index 9e538f3c44a3c..d15967e614a8f 100644 --- a/src/include/nodes/plannodes.h +++ b/src/include/nodes/plannodes.h @@ -616,6 +616,19 @@ typedef struct IndexScan List *indexorderbyops; /* forward or backward or don't care */ ScanDirection indexorderdir; + + /* + * Per indexorderbyorig entry: may setrefs.c replace a target-list + * occurrence of that expression with an INNER_VAR reference to the scan's + * ORDER BY value? True when the index column's opclass reports exact + * distances that are the operator's result (amcanreturnorderby). Only + * setrefs.c reads it, so it isn't written out with the plan. + * indexorderbynused counts the ORDER BY values the targetlist reads (0 if + * none); the executor sets up the ORDER BY slot and projection when it's + * nonzero, and EXPLAIN shows it. + */ + List *indexorderbyexact pg_node_attr(read_write_ignore, read_as(NIL)); + int indexorderbynused; } IndexScan; /* ---------------- diff --git a/src/include/optimizer/clauses.h b/src/include/optimizer/clauses.h index e929789762be9..f88278f10d98f 100644 --- a/src/include/optimizer/clauses.h +++ b/src/include/optimizer/clauses.h @@ -45,6 +45,8 @@ extern bool contain_subplans(Node *clause); extern char max_parallel_hazard(Query *parse); extern bool is_parallel_safe(PlannerInfo *root, Node *node); extern bool contain_nonstrict_functions(Node *clause); +extern bool expr_contains(Node *expr, Node *sub); +extern Node *strip_nullingrels(Node *node); extern bool contain_exec_param(Node *clause, List *param_ids); extern bool contain_leaked_vars(Node *clause); diff --git a/src/include/optimizer/pathnode.h b/src/include/optimizer/pathnode.h index da2d9b384b596..c1b3a22d3598f 100644 --- a/src/include/optimizer/pathnode.h +++ b/src/include/optimizer/pathnode.h @@ -225,6 +225,18 @@ extern HashPath *create_hashjoin_path(PlannerInfo *root, Relids required_outer, List *hashclauses); +extern void sort_pathlist_by_cost(List *pathlist); +extern bool index_orderby_returnable(IndexOptInfo *index, int indexcol, + Expr *orderby); +extern bool orderby_tlist_match(Expr *expr, Expr *orderby); +extern QualCost path_target_cost(PlannerInfo *root, Path *path, + PathTarget *target); +extern List *path_extra_exprs(Path *path); +extern bool path_emits_extras(Path *path); +extern bool expr_worth_emitting(PlannerInfo *root, Node *expr); +extern List *query_tlist_exprs(PlannerInfo *root, RelOptInfo *rel); +extern QualCost indexonly_qual_cost(PlannerInfo *root, IndexPath *ipath, + List *qpquals, QualCost cost); extern ProjectionPath *create_projection_path(PlannerInfo *root, RelOptInfo *rel, Path *subpath, diff --git a/src/test/isolation/expected/eval-plan-qual-knn.out b/src/test/isolation/expected/eval-plan-qual-knn.out new file mode 100644 index 0000000000000..3c1c4794b64f4 --- /dev/null +++ b/src/test/isolation/expected/eval-plan-qual-knn.out @@ -0,0 +1,40 @@ +Parsed test spec with 2 sessions + +starting permutation: s2e s1b s1u s2s s1c +step s2e: + EXPLAIN (VERBOSE, COSTS OFF) + SELECT id, p <-> point(0, 0) AS d FROM epq_knn + WHERE p <@ box(point(0, 0), point(10, 10)) + ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE; + +QUERY PLAN +------------------------------------------------------------- +Limit + Output: id, ((p <-> '(0,0)'::point)), ctid + -> LockRows + Output: id, ((p <-> '(0,0)'::point)), ctid + -> Index Scan using epq_knn_p on public.epq_knn + Output: id, ((p <-> '(0,0)'::point)), ctid + Index Cond: (epq_knn.p <@ '(10,10),(0,0)'::box) + Order By: (epq_knn.p <-> '(0,0)'::point) + Order By Values Used: 1 +(9 rows) + +step s1b: BEGIN; +step s1u: UPDATE epq_knn SET p = point(3.5, 3.5) WHERE id = 3; +step s2s: + SELECT id, p <-> point(0, 0) AS d, (p <-> point(0, 0)) = d AS same + FROM (SELECT id, p, p <-> point(0, 0) AS d FROM epq_knn + WHERE p <@ box(point(0, 0), point(10, 10)) + ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE) s; + +step s1c: COMMIT; +step s2s: <... completed> +id| d|same +--+------------------+---- + 1|1.4142135623730951|t + 2|2.8284271247461903|t + 3| 4.949747468305833|t + 4| 5.656854249492381|t +(4 rows) + diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule index bf9a2037c705a..1703a3ec50bd7 100644 --- a/src/test/isolation/isolation_schedule +++ b/src/test/isolation/isolation_schedule @@ -44,6 +44,7 @@ test: subxid-overflow test: eval-plan-qual test: eval-plan-qual-trigger test: eval-plan-qual-partition-prune +test: eval-plan-qual-knn test: inplace-inval test: intra-grant-inplace test: intra-grant-inplace-db diff --git a/src/test/isolation/specs/eval-plan-qual-knn.spec b/src/test/isolation/specs/eval-plan-qual-knn.spec new file mode 100644 index 0000000000000..b485e88dbc3c1 --- /dev/null +++ b/src/test/isolation/specs/eval-plan-qual-knn.spec @@ -0,0 +1,44 @@ +# An ordering index scan that returns its ORDER BY values (amcanreturnorderby) +# must not hand back the value it computed for the old row version when +# EvalPlanQual re-checks a concurrently updated row: the value is recomputed +# from the new version. + +setup +{ + CREATE TABLE epq_knn (id int, p point); + INSERT INTO epq_knn SELECT i, point(i, i) FROM generate_series(1, 20) i; + CREATE INDEX epq_knn_p ON epq_knn USING gist (p); +} + +teardown +{ + DROP TABLE epq_knn; +} + +session s1 +setup { SET enable_seqscan = off; SET enable_bitmapscan = off; } +step s1b { BEGIN; } +step s1u { UPDATE epq_knn SET p = point(3.5, 3.5) WHERE id = 3; } +step s1c { COMMIT; } + +session s2 +setup { SET enable_seqscan = off; SET enable_bitmapscan = off; } +step s2e +{ + EXPLAIN (VERBOSE, COSTS OFF) + SELECT id, p <-> point(0, 0) AS d FROM epq_knn + WHERE p <@ box(point(0, 0), point(10, 10)) + ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE; +} +step s2s +{ + SELECT id, p <-> point(0, 0) AS d, (p <-> point(0, 0)) = d AS same + FROM (SELECT id, p, p <-> point(0, 0) AS d FROM epq_knn + WHERE p <@ box(point(0, 0), point(10, 10)) + ORDER BY p <-> point(0, 0) LIMIT 4 FOR UPDATE) s; +} + +# s2's scan computes id 3's distance from (3,3), then waits on s1's lock; +# after s1 commits, EvalPlanQual re-checks id 3 with p = (3.5,3.5), and the +# value returned must be that row's distance. +permutation s2e s1b s1u s2s s1c diff --git a/src/test/regress/expected/gist.out b/src/test/regress/expected/gist.out index ac79f94aa8028..be68194751ff7 100644 --- a/src/test/regress/expected/gist.out +++ b/src/test/regress/expected/gist.out @@ -387,6 +387,946 @@ select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; ERROR: lossy distance functions are not supported in index-only scans +drop index gist_tbl_multi_index; +-- Test that an ordering Index Scan returns the AM's ORDER BY values to a +-- targetlist entry equal to the ORDER BY expression, instead of evaluating +-- the expression again, for an opclass that passes amcanreturnorderby. +set enable_indexonlyscan = off; +-- An expression index over a counting function proves the operator is not +-- re-evaluated: the count stays at zero for the rows returned. +create sequence gist_cnt_seq; +create function gist_cnt_pt(point) returns point language plpgsql immutable + as $$ begin perform nextval('public.gist_cnt_seq'); return $1; end $$; +create index gist_tbl_cnt_index on gist_tbl using gist (gist_cnt_pt(p)); +explain (verbose, costs off) +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; + QUERY PLAN +------------------------------------------------------------------------ + Limit + Output: p, ((gist_cnt_pt(p) <-> '(0.201,0.201)'::point)) + -> Index Scan using gist_tbl_cnt_index on public.gist_tbl + Output: p, ((gist_cnt_pt(p) <-> '(0.201,0.201)'::point)) + Order By: (gist_cnt_pt(gist_tbl.p) <-> '(0.201,0.201)'::point) + Order By Values Used: 1 +(6 rows) + +select setval('gist_cnt_seq', 1, false); + setval +-------- + 1 +(1 row) + +-- the distance numbers are not exactly the same across platforms +set extra_float_digits = 0; +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; + p | dist +-------------+-------------------- + (0.2,0.2) | 0.0014142135623731 + (0.25,0.25) | 0.0692964645562816 + (0.15,0.15) | 0.0721248916810279 +(3 rows) + +reset extra_float_digits; +select nextval('gist_cnt_seq') - 1 as calls_during_scan; + calls_during_scan +------------------- + 0 +(1 row) + +-- Every copy of the ORDER BY expression is served from the index; an +-- expression that merely contains it is not. +explain (verbose, costs off) +select gist_cnt_pt(p) <-> point(0,0) as d1, + (gist_cnt_pt(p) <-> point(0,0)) * 2 as d2, + gist_cnt_pt(p) <-> point(0,0) as d3 + from gist_tbl order by gist_cnt_pt(p) <-> point(0,0) limit 2; + QUERY PLAN +------------------------------------------------------------------------------------------------------------------------------------------------------------- + Limit + Output: ((gist_cnt_pt(p) <-> '(0,0)'::point)), (((gist_cnt_pt(p) <-> '(0,0)'::point) * '2'::double precision)), ((gist_cnt_pt(p) <-> '(0,0)'::point)) + -> Index Scan using gist_tbl_cnt_index on public.gist_tbl + Output: ((gist_cnt_pt(p) <-> '(0,0)'::point)), ((gist_cnt_pt(p) <-> '(0,0)'::point) * '2'::double precision), ((gist_cnt_pt(p) <-> '(0,0)'::point)) + Order By: (gist_cnt_pt(gist_tbl.p) <-> '(0,0)'::point) + Order By Values Used: 1 +(6 rows) + +drop index gist_tbl_cnt_index; +drop function gist_cnt_pt(point); +drop sequence gist_cnt_seq; +-- The values must equal the operator's, also for a row re-fetched under +-- row locking, and for a rescan with a new ORDER BY argument (LATERAL). +create index gist_tbl_point_index on gist_tbl using gist (p); +select count(*) filter (where dist = p <-> point(5.2, 5.91)) as same, + count(*) as total + from (select p, p <-> point(5.2, 5.91) as dist from gist_tbl + order by p <-> point(5.2, 5.91) limit 200) ss; + same | total +------+------- + 200 | 200 +(1 row) + +set extra_float_digits = 0; +select p, p <-> point(5.2, 5.91) as dist from gist_tbl + where p <@ box(point(5,5), point(6,6)) order by p <-> point(5.2, 5.91) + limit 3 for update; + p | dist +-------------+------------------- + (5.55,5.55) | 0.502095608425328 + (5.6,5.6) | 0.5060632371552 + (5.5,5.5) | 0.508035431835221 +(3 rows) + +reset extra_float_digits; +explain (verbose, costs off) +select v.q, n.dist + from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q), + lateral (select p <-> v.q as dist from gist_tbl + order by p <-> v.q limit 2) n; + QUERY PLAN +---------------------------------------------------------------------- + Nested Loop + Output: "*VALUES*".column1, ((gist_tbl.p <-> "*VALUES*".column1)) + -> Values Scan on "*VALUES*" + Output: "*VALUES*".column1 + -> Limit + Output: ((gist_tbl.p <-> "*VALUES*".column1)) + -> Index Scan using gist_tbl_point_index on public.gist_tbl + Output: ((gist_tbl.p <-> "*VALUES*".column1)) + Order By: (gist_tbl.p <-> "*VALUES*".column1) + Order By Values Used: 1 +(10 rows) + +select count(*) filter (where n.dist = n.p <-> v.q) as same, count(*) as total + from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q), + lateral (select p, p <-> v.q as dist from gist_tbl + order by p <-> v.q limit 50) n; + same | total +------+------- + 150 | 150 +(1 row) + +-- A multi-column index ordered by an exact key and a lossy one: the lossy +-- one's distances are rechecked, rows pass through the reorder queue, and +-- the exact key's value is still the operator's. +create index gist_tbl_pc_index on gist_tbl using gist (p, c); +explain (verbose, costs off) +select p <-> point(1, 1) as dp, c <-> point(2, 2) as dc from gist_tbl + order by p <-> point(1, 1), c <-> point(2, 2) limit 5; + QUERY PLAN +----------------------------------------------------------------------------------------- + Limit + Output: ((p <-> '(1,1)'::point)), ((c <-> '(2,2)'::point)) + -> Index Scan using gist_tbl_pc_index on public.gist_tbl + Output: ((p <-> '(1,1)'::point)), (c <-> '(2,2)'::point) + Order By: ((gist_tbl.p <-> '(1,1)'::point) AND (gist_tbl.c <-> '(2,2)'::point)) + Order By Values Used: 1 +(6 rows) + +select count(*) filter (where dp = p <-> point(1, 1)) as same_p, + count(*) filter (where dc = c <-> point(2, 2)) as same_c, + count(*) as total + from (select p, c, p <-> point(1, 1) as dp, c <-> point(2, 2) as dc + from gist_tbl + order by p <-> point(1, 1), c <-> point(2, 2) limit 200) ss; + same_p | same_c | total +--------+--------+------- + 200 | 200 | 200 +(1 row) + +drop index gist_tbl_pc_index; +drop index gist_tbl_point_index; +-- The box opclass computes a leaf's distance exactly, but the operator treats +-- a point within EPSILON of an edge's line as on it, so the two can differ in +-- the last digits; it doesn't qualify, and the operator is evaluated. +create index gist_tbl_box_index on gist_tbl using gist (b); +explain (verbose, costs off) +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91); + QUERY PLAN +-------------------------------------------------------- + Index Scan using gist_tbl_box_index on public.gist_tbl + Output: b, (b <-> '(5.2,5.91)'::point) + Index Cond: (gist_tbl.b <@ '(6,6),(5,5)'::box) + Order By: (gist_tbl.b <-> '(5.2,5.91)'::point) +(4 rows) + +-- For this point, the index's distance is 1 and the operator's is +-- 1.00000000000008: dist is the operator's only if the scan doesn't hand +-- back its own. +create temp table gist_box_eps (b box); +insert into gist_box_eps values ('((0,0),(1,1))'); +create index on gist_box_eps using gist (b); +select count(*) filter (where dist = b <-> point(0.9999996, 2)) as same, + count(*) as total + from (select b, b <-> point(0.9999996, 2) as dist from gist_box_eps + order by b <-> point(0.9999996, 2) limit 1) ss; + same | total +------+------- + 1 | 1 +(1 row) + +drop table gist_box_eps; +drop index gist_tbl_box_index; +-- The circle opclass's distance is a lower bound that is always rechecked, +-- and its opclass does not pass amcanreturnorderby: the targetlist is left +-- alone and the operator is evaluated as before. +create index gist_tbl_circle_index on gist_tbl using gist (c); +explain (verbose, costs off) +select c <-> point(5.2, 5.91) as dist from gist_tbl + order by c <-> point(5.2, 5.91) limit 3; + QUERY PLAN +----------------------------------------------------------------- + Limit + Output: ((c <-> '(5.2,5.91)'::point)) + -> Index Scan using gist_tbl_circle_index on public.gist_tbl + Output: (c <-> '(5.2,5.91)'::point) + Order By: (gist_tbl.c <-> '(5.2,5.91)'::point) +(5 rows) + +select count(*) filter (where dist = c <-> point(5.2, 5.91)) as same, + count(*) as total + from (select c, c <-> point(5.2, 5.91) as dist from gist_tbl + order by c <-> point(5.2, 5.91) limit 200) ss; + same | total +------+------- + 200 | 200 +(1 row) + +drop index gist_tbl_circle_index; +reset enable_indexonlyscan; +-- SP-GiST's quad and k-d point opclasses qualify as well; its box opclass +-- doesn't, for the same reason as GiST's. +create temp table spg_orderby (p point, b box); +insert into spg_orderby + select point(i % 97 * 0.31, i / 97 * 0.29), + box(point(i % 97 * 0.31, i / 97 * 0.29), + point(i % 97 * 0.31 + 0.2, i / 97 * 0.29 + 0.1)) + from generate_series(1, 5000) i; +create index spg_orderby_quad on spg_orderby using spgist (p); +set enable_indexonlyscan = off; +explain (verbose, costs off) +select p <-> point(3, 4) as dist from spg_orderby + order by p <-> point(3, 4) limit 3; + QUERY PLAN +---------------------------------------------------------------- + Limit + Output: ((p <-> '(3,4)'::point)) + -> Index Scan using spg_orderby_quad on pg_temp.spg_orderby + Output: ((p <-> '(3,4)'::point)) + Order By: (spg_orderby.p <-> '(3,4)'::point) + Order By Values Used: 1 +(6 rows) + +select count(*) filter (where dist = p <-> point(3, 4)) as same, + count(*) as total + from (select p, p <-> point(3, 4) as dist from spg_orderby + order by p <-> point(3, 4) limit 300) ss; + same | total +------+------- + 300 | 300 +(1 row) + +drop index spg_orderby_quad; +create index spg_orderby_kd on spg_orderby using spgist (p kd_point_ops); +select count(*) filter (where dist = p <-> point(3, 4)) as same, + count(*) as total + from (select p, p <-> point(3, 4) as dist from spg_orderby + order by p <-> point(3, 4) limit 300) ss; + same | total +------+------- + 300 | 300 +(1 row) + +drop index spg_orderby_kd; +create index spg_orderby_box on spg_orderby using spgist (b); +explain (verbose, costs off) +select b <-> point(3, 4) as dist from spg_orderby + order by b <-> point(3, 4) limit 3; + QUERY PLAN +--------------------------------------------------------------- + Limit + Output: ((b <-> '(3,4)'::point)) + -> Index Scan using spg_orderby_box on pg_temp.spg_orderby + Output: (b <-> '(3,4)'::point) + Order By: (spg_orderby.b <-> '(3,4)'::point) +(5 rows) + +reset enable_indexonlyscan; +drop table spg_orderby; +-- The planner must not charge an ordering Index Scan for evaluating a +-- targetlist entry it takes from its ORDER BY values. With a costly ORDER BY +-- expression, the kNN scan should win over Seq Scan + Sort even when the +-- filter is selective, since the Sort plan really does evaluate it per row. +create function gist_costly_pt(point) returns point + language plpgsql immutable strict cost 10000 + as $$ begin return $1; end $$; +create temp table gist_costly (id int, p point, grp int); +insert into gist_costly + select g, point(g % 101, g % 103), g % 100 from generate_series(1, 10000) g; +create index on gist_costly using gist (gist_costly_pt(p)); +create index on gist_costly (grp); +vacuum analyze gist_costly; +set enable_bitmapscan = off; +-- (Seq Scan + Sort is a real choice here; the earlier tests turned it off.) +set enable_seqscan = on; +explain (costs off) +select id, gist_costly_pt(p) <-> point(0,0) as dist from gist_costly + where grp < 5 order by gist_costly_pt(p) <-> point(0,0); + QUERY PLAN +------------------------------------------------------------------ + Index Scan using gist_costly_gist_costly_pt_p_idx on gist_costly + Order By: (gist_costly_pt(p) <-> '(0,0)'::point) + Filter: (grp < 5) +(3 rows) + +set enable_seqscan = off; +-- The ORDER BY value is in every plan's target (as a sort column), so the +-- kNN scan used to be charged rows * cost(gist_costly_pt) for it, ~2.5e5 +-- here. It is now charged nothing for it; an expression that merely +-- contains it is still evaluated, and charged. +create function gist_costly_total(q text) returns float8 language plpgsql as +$$ declare j json; begin + execute 'explain (format json) ' || q into j; + return (j->0->'Plan'->>'Total Cost')::float8; + end $$; +set enable_sort = off; +select gist_costly_total('select id, gist_costly_pt(p) <-> point(0,0) + from gist_costly order by gist_costly_pt(p) <-> point(0,0)') < 10000 + as returned_is_free, + gist_costly_total('select id, (gist_costly_pt(p) <-> point(0,0)) * 2 + from gist_costly order by gist_costly_pt(p) <-> point(0,0)') > 100000 + as nested_is_charged; + returned_is_free | nested_is_charged +------------------+------------------- + t | t +(1 row) + +reset enable_sort; +drop function gist_costly_total(text); +reset enable_bitmapscan; +drop table gist_costly; +drop function gist_costly_pt(point); +-- The ORDER BY expression written as "const OP key" is commuted into +-- "key OP const" for the index; a targetlist entry in the original form is +-- still the same value, and is still taken from the scan. +create temp table gist_commute as + select g as id, point(g % 101, g % 103) as p from generate_series(1, 1000) g; +create index on gist_commute using gist (p); +vacuum analyze gist_commute; +set enable_seqscan = off; +set enable_bitmapscan = off; +set enable_indexonlyscan = off; +explain (verbose, costs off) +select point(5,5) <-> p as d from gist_commute order by point(5,5) <-> p limit 3; + QUERY PLAN +------------------------------------------------------------------- + Limit + Output: ((p <-> '(5,5)'::point)) + -> Index Scan using gist_commute_p_idx on pg_temp.gist_commute + Output: ((p <-> '(5,5)'::point)) + Order By: (gist_commute.p <-> '(5,5)'::point) + Order By Values Used: 1 +(6 rows) + +set extra_float_digits = 0; +select point(5,5) <-> p as d from gist_commute order by point(5,5) <-> p limit 3; + d +----------------- + 0 + 1.4142135623731 + 1.4142135623731 +(3 rows) + +reset extra_float_digits; +reset enable_seqscan; +reset enable_bitmapscan; +reset enable_indexonlyscan; +drop table gist_commute; +-- An ordering Index Scan below a join emits the ORDER BY value it returns, +-- so the join above reads it instead of evaluating the expression again. +-- The counting function shows the expression is evaluated zero times. +create sequence gist_join_cnt; +create function gist_join_pt(point) returns point + language plpgsql immutable strict cost 1000 + as $$ begin perform nextval('public.gist_join_cnt'); return $1; end $$; +create temp table gist_join_fact as + select g as id, point(g % 101, g % 103) as p from generate_series(1, 1000) g; +create index on gist_join_fact using gist (gist_join_pt(p)); +create temp table gist_join_dim as select g as id from generate_series(1, 1000, 2) g; +create index on gist_join_dim (id); +vacuum analyze gist_join_fact; +vacuum analyze gist_join_dim; +set enable_seqscan = off; +set enable_bitmapscan = off; +set enable_indexonlyscan = off; +set enable_hashjoin = off; +set enable_mergejoin = off; +set enable_sort = off; +explain (verbose, costs off) +select f.id, gist_join_pt(f.p) <-> point(5,5) as d + from gist_join_fact f join gist_join_dim j on j.id = f.id + order by gist_join_pt(f.p) <-> point(5,5) limit 3; + QUERY PLAN +-------------------------------------------------------------------------------------------- + Limit + Output: f.id, ((gist_join_pt(f.p) <-> '(5,5)'::point)) + -> Nested Loop + Output: f.id, ((gist_join_pt(f.p) <-> '(5,5)'::point)) + -> Index Scan using gist_join_fact_gist_join_pt_p_idx on pg_temp.gist_join_fact f + Output: f.id, f.p, ((gist_join_pt(f.p) <-> '(5,5)'::point)) + Order By: (gist_join_pt(f.p) <-> '(5,5)'::point) + Order By Values Used: 1 + -> Index Scan using gist_join_dim_id_idx on pg_temp.gist_join_dim j + Output: j.id + Index Cond: (j.id = f.id) +(11 rows) + +select setval('gist_join_cnt', 1, false); + setval +-------- + 1 +(1 row) + +set extra_float_digits = 0; +select f.id, gist_join_pt(f.p) <-> point(5,5) as d + from gist_join_fact f join gist_join_dim j on j.id = f.id + order by gist_join_pt(f.p) <-> point(5,5) limit 3; + id | d +-----+------------------ + 5 | 0 + 107 | 1.4142135623731 + 209 | 2.82842712474619 +(3 rows) + +reset extra_float_digits; +select nextval('gist_join_cnt') - 1 as calls_during_join; + calls_during_join +------------------- + 0 +(1 row) + +-- Nothing extra is emitted when the query doesn't need the value above the +-- scan. +explain (verbose, costs off) +select f.id from gist_join_fact f join gist_join_dim j on j.id = f.id + order by gist_join_pt(f.p) <-> point(5,5) limit 3; + QUERY PLAN +-------------------------------------------------------------------------------------------- + Limit + Output: f.id, ((gist_join_pt(f.p) <-> '(5,5)'::point)) + -> Nested Loop + Output: f.id, ((gist_join_pt(f.p) <-> '(5,5)'::point)) + -> Index Scan using gist_join_fact_gist_join_pt_p_idx on pg_temp.gist_join_fact f + Output: f.id, f.p, ((gist_join_pt(f.p) <-> '(5,5)'::point)) + Order By: (gist_join_pt(f.p) <-> '(5,5)'::point) + Order By Values Used: 1 + -> Index Scan using gist_join_dim_id_idx on pg_temp.gist_join_dim j + Output: j.id + Index Cond: (j.id = f.id) +(11 rows) + +-- Nor for a cheap ORDER BY expression: the plain point distance is computed +-- again above the join, which costs less than carrying it up. +create index on gist_join_fact using gist (p); +explain (verbose, costs off) +select f.id, f.p <-> point(5,5) as d + from gist_join_fact f join gist_join_dim j on j.id = f.id + order by f.p <-> point(5,5) limit 3; + QUERY PLAN +------------------------------------------------------------------------------- + Limit + Output: f.id, ((f.p <-> '(5,5)'::point)) + -> Nested Loop + Output: f.id, (f.p <-> '(5,5)'::point) + -> Index Scan using gist_join_fact_p_idx on pg_temp.gist_join_fact f + Output: f.id, f.p + Order By: (f.p <-> '(5,5)'::point) + -> Index Scan using gist_join_dim_id_idx on pg_temp.gist_join_dim j + Output: j.id + Index Cond: (j.id = f.id) +(10 rows) + +reset enable_seqscan; +reset enable_bitmapscan; +reset enable_indexonlyscan; +reset enable_hashjoin; +reset enable_mergejoin; +reset enable_sort; +drop table gist_join_fact; +drop table gist_join_dim; +drop function gist_join_pt(point); +drop sequence gist_join_cnt; +-- An index-only scan isn't charged for evaluating a target expression it +-- reads from a returnable index expression column: it reads the stored +-- value instead. (Not GiST-specific; btree returns expression columns.) +create function gist_ios_costly(int) returns int + language plpgsql immutable strict cost 100000 + as $$ begin return $1; end $$; +create temp table gist_ios_cost (i int); +insert into gist_ios_cost select g from generate_series(1, 10000) g; +create index on gist_ios_cost (i, gist_ios_costly(i)); +vacuum analyze gist_ios_cost; +create function gist_ios_total(q text) returns float8 language plpgsql as +$$ declare j json; begin + execute 'explain (format json) ' || q into j; + return (j->0->'Plan'->>'Total Cost')::float8; + end $$; +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost order by i; + QUERY PLAN +-------------------------------------------------------------------------------------- + Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost + Output: (gist_ios_costly(i)), i +(2 rows) + +select gist_ios_total('select gist_ios_costly(i) from gist_ios_cost order by i') + < 10000 as ios_expr_is_free; + ios_expr_is_free +------------------ + t +(1 row) + +-- With nothing else to recommend it (no qual, no useful order), an +-- index-only scan that reads the expression still beats a seq scan that has +-- to compute it: add_path() keeps the path that emits the value. +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost order by gist_ios_costly(i); + QUERY PLAN +-------------------------------------------------------------------------------------------- + Sort + Output: (gist_ios_costly(i)) + Sort Key: (gist_ios_costly(gist_ios_cost.i)) + -> Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost + Output: (gist_ios_costly(i)) +(5 rows) + +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost; + QUERY PLAN +-------------------------------------------------------------------------------------- + Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost + Output: (gist_ios_costly(i)) +(2 rows) + +-- Ordered by the index, the scan emits the final targetlist directly and +-- still reads the expression from the index. +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost order by i desc limit 3; + QUERY PLAN +----------------------------------------------------------------------------------------------------- + Limit + Output: (gist_ios_costly(i)), i + -> Index Only Scan Backward using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost + Output: (gist_ios_costly(i)), i +(4 rows) + +-- The expression may be part of a larger one, or appear in a qual. +explain (verbose, costs off) +select gist_ios_costly(i) + 1 from gist_ios_cost + where gist_ios_costly(i) % 3 = 0; + QUERY PLAN +-------------------------------------------------------------------------------------- + Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost + Output: ((gist_ios_costly(i)) + 1) + Filter: (((gist_ios_costly(gist_ios_cost.i)) % 3) = 0) +(3 rows) + +-- Below a join, the scan emits it and the join passes it up, through more +-- than one join level. +create temp table gist_ios_dim (i int, tag text); +insert into gist_ios_dim select g, 't' || g from generate_series(1, 10000, 7) g; +create index on gist_ios_dim (i); +create temp table gist_ios_dim2 (k int, i int); +insert into gist_ios_dim2 select g, g * 3 from generate_series(1, 3000) g; +vacuum analyze gist_ios_dim, gist_ios_dim2; +-- (Each join method is pinned; it's the value passing up that's tested.) +set enable_hashjoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(a.i), d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Merge Join + Output: (gist_ios_costly(a.i)), d.tag + Merge Cond: (a.i = d.i) + -> Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost a + Output: a.i, (gist_ios_costly(a.i)) + -> Index Scan using gist_ios_dim_i_idx on pg_temp.gist_ios_dim d + Output: d.i, d.tag +(7 rows) + +reset enable_nestloop; +set enable_mergejoin = off; +explain (verbose, costs off) +select gist_ios_costly(a.i), d.tag, n.k from gist_ios_cost a + join gist_ios_dim d on d.i = a.i join gist_ios_dim2 n on n.i = a.i; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Nested Loop + Output: (gist_ios_costly(a.i)), d.tag, n.k + Join Filter: (a.i = d.i) + -> Nested Loop + Output: d.tag, d.i, n.k, n.i + -> Seq Scan on pg_temp.gist_ios_dim2 n + Output: n.k, n.i + -> Index Scan using gist_ios_dim_i_idx on pg_temp.gist_ios_dim d + Output: d.i, d.tag + Index Cond: (d.i = n.i) + -> Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost a + Output: a.i, (gist_ios_costly(a.i)) + Index Cond: (a.i = n.i) +(13 rows) + +reset enable_hashjoin; +reset enable_mergejoin; +-- When everything the scan emits is an index column, it returns the +-- index's own tlist without projecting (here including k, which nothing +-- needs); the value is still read above the join. +create temp table gist_ios_wide (k int, i int); +insert into gist_ios_wide select g, g from generate_series(1, 10000) g; +create index on gist_ios_wide (k, gist_ios_costly(i)) include (i); +vacuum analyze gist_ios_wide; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(w.i), d.tag from gist_ios_wide w + join gist_ios_dim d on d.i = w.i; + QUERY PLAN +------------------------------------------------------------------------------------------------ + Hash Join + Output: (gist_ios_costly(w.i)), d.tag + Hash Cond: (w.i = d.i) + -> Index Only Scan using gist_ios_wide_k_gist_ios_costly_i_i_idx on pg_temp.gist_ios_wide w + Output: w.k, (gist_ios_costly(w.i)), w.i + -> Hash + Output: d.tag, d.i + -> Seq Scan on pg_temp.gist_ios_dim d + Output: d.tag, d.i +(9 rows) + +reset enable_mergejoin; +reset enable_nestloop; +drop table gist_ios_wide; +-- Also from the nullable side of an outer join, because the function is +-- strict: a null-extended row gets NULL for the value, which is what the +-- function returns for a NULL argument. The value is used inside a larger, +-- non-strict expression above the join. +set enable_hashjoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select coalesce(gist_ios_costly(a.i), -1) from gist_ios_dim d + left join gist_ios_cost a on a.i = d.i * 2; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Merge Left Join + Output: COALESCE((gist_ios_costly(a.i)), '-1'::integer) + Merge Cond: (((d.i * 2)) = a.i) + -> Sort + Output: d.i, ((d.i * 2)) + Sort Key: ((d.i * 2)) + -> Seq Scan on pg_temp.gist_ios_dim d + Output: d.i, (d.i * 2) + -> Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost a + Output: a.i, (gist_ios_costly(a.i)) +(10 rows) + +reset enable_hashjoin; +reset enable_nestloop; +-- And from both sides' nullable rows of a FULL JOIN. +set enable_hashjoin = off; +explain (verbose, costs off) +select gist_ios_costly(a.i), d.tag from gist_ios_dim d + full join gist_ios_cost a on a.i = d.i * 2; + QUERY PLAN +---------------------------------------------------------------------------------------------- + Merge Full Join + Output: (gist_ios_costly(a.i)), d.tag + Merge Cond: (((d.i * 2)) = a.i) + -> Sort + Output: d.tag, d.i, ((d.i * 2)) + Sort Key: ((d.i * 2)) + -> Seq Scan on pg_temp.gist_ios_dim d + Output: d.tag, d.i, (d.i * 2) + -> Index Only Scan using gist_ios_cost_i_gist_ios_costly_i_idx on pg_temp.gist_ios_cost a + Output: a.i, (gist_ios_costly(a.i)) +(10 rows) + +reset enable_hashjoin; +select count(*) as total, count(v) as with_value, + count(*) filter (where v = i) as same + from (select gist_ios_costly(a.i) as v, a.i from gist_ios_dim d + full join gist_ios_cost a on a.i = d.i * 2) s; + total | with_value | same +-------+------------+------- + 10714 | 10000 | 10000 +(1 row) + +-- A function that isn't strict may return something else for NULL, so it's +-- computed above the join. +create function gist_ios_nonstrict(int) returns int + language plpgsql immutable called on null input cost 100000 + as $$ begin return coalesce($1, -1); end $$; +create index on gist_ios_cost (i, gist_ios_nonstrict(i)); +vacuum analyze gist_ios_cost; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_nonstrict(a.i) from gist_ios_dim d + left join gist_ios_cost a on a.i = d.i * 2; + QUERY PLAN +------------------------------------------------- + Hash Left Join + Output: gist_ios_nonstrict(a.i) + Hash Cond: ((d.i * 2) = a.i) + -> Seq Scan on pg_temp.gist_ios_dim d + Output: d.i, d.tag + -> Hash + Output: a.i + -> Seq Scan on pg_temp.gist_ios_cost a + Output: a.i +(9 rows) + +reset enable_mergejoin; +reset enable_nestloop; +select count(*) filter (where v = -1) as null_extended + from (select gist_ios_nonstrict(a.i) as v from gist_ios_dim d + left join gist_ios_cost a on a.i = d.i * 2) s; + null_extended +--------------- + 714 +(1 row) + +drop function gist_ios_nonstrict(int) cascade; +NOTICE: drop cascades to index gist_ios_cost_i_gist_ios_nonstrict_i_idx +-- A parallel-restricted function: the value is read from the index inside +-- the workers, not evaluated there, and passed up through the Gather. (Not +-- a temp table, which can't be scanned in parallel.) +create function gist_ios_restricted(int) returns int + language plpgsql immutable strict parallel restricted cost 100000 + as $$ begin return $1; end $$; +create table gist_ios_par (i int); +insert into gist_ios_par select g from generate_series(1, 10000) g; +create index on gist_ios_par (i, gist_ios_restricted(i)); +vacuum analyze gist_ios_par; +set parallel_setup_cost = 0; +set parallel_tuple_cost = 0; +set min_parallel_index_scan_size = 0; +set max_parallel_workers_per_gather = 2; +set parallel_leader_participation = off; +-- (Pin the scan type: these tables aren't temp, so VACUUM may not get to +-- mark them all-visible while other sessions hold snapshots.) +set enable_seqscan = off; +set enable_bitmapscan = off; +explain (verbose, costs off) +select gist_ios_restricted(i) from gist_ios_par; + QUERY PLAN +------------------------------------------------------------------------------------------------------ + Gather + Output: (gist_ios_restricted(i)) + Workers Planned: 2 + -> Parallel Index Only Scan using gist_ios_par_i_gist_ios_restricted_i_idx on public.gist_ios_par + Output: i, (gist_ios_restricted(i)) +(5 rows) + +select count(*), sum(v) from (select gist_ios_restricted(i) as v from gist_ios_par) s; + count | sum +-------+---------- + 10000 | 50005000 +(1 row) + +-- Also through a Gather Merge, which sorts the workers' rows by a column +-- the index doesn't lead with. +create table gist_ios_gm (k int, i int); +insert into gist_ios_gm select g % 97, g from generate_series(1, 20000) g; +create index on gist_ios_gm (i, gist_ios_restricted(i)) include (k); +vacuum analyze gist_ios_gm; +explain (verbose, costs off) +select k, gist_ios_restricted(i) from gist_ios_gm where i > 100 order by k; + QUERY PLAN +------------------------------------------------------------------------------------------------------------ + Gather Merge + Output: k, (gist_ios_restricted(i)) + Workers Planned: 2 + -> Sort + Output: k, i, (gist_ios_restricted(i)) + Sort Key: gist_ios_gm.k + -> Parallel Index Only Scan using gist_ios_gm_i_gist_ios_restricted_i_k_idx on public.gist_ios_gm + Output: k, i, (gist_ios_restricted(i)) + Index Cond: (gist_ios_gm.i > 100) +(9 rows) + +select count(*) filter (where v = i) as same, count(*) as total + from (select k, i, gist_ios_restricted(i) as v from gist_ios_gm + where i > 100 order by k) s; + same | total +-------+------- + 19900 | 19900 +(1 row) + +drop table gist_ios_gm; +-- And through a Parallel Append over partitions that all have the index. +-- (Different sizes, so that the children's cost order, which is the plan's +-- order, doesn't depend on how many pages VACUUM could mark all-visible +-- when other sessions hold snapshots.) +create table gist_ios_ppart (i int) partition by range (i); +create table gist_ios_ppart1 partition of gist_ios_ppart + for values from (1) to (2001); +create table gist_ios_ppart2 partition of gist_ios_ppart + for values from (2001) to (10001); +insert into gist_ios_ppart select g from generate_series(1, 10000) g; +create index on gist_ios_ppart (i, gist_ios_restricted(i)); +vacuum analyze gist_ios_ppart; +explain (verbose, costs off) +select gist_ios_restricted(i) from gist_ios_ppart; + QUERY PLAN +----------------------------------------------------------------------------------------------------------------------------------- + Gather + Output: (gist_ios_restricted(gist_ios_ppart.i)) + Workers Planned: 2 + -> Parallel Append + -> Parallel Index Only Scan using gist_ios_ppart2_i_gist_ios_restricted_i_idx on public.gist_ios_ppart2 gist_ios_ppart_2 + Output: gist_ios_ppart_2.i, (gist_ios_restricted(gist_ios_ppart_2.i)) + -> Parallel Index Only Scan using gist_ios_ppart1_i_gist_ios_restricted_i_idx on public.gist_ios_ppart1 gist_ios_ppart_1 + Output: gist_ios_ppart_1.i, (gist_ios_restricted(gist_ios_ppart_1.i)) +(8 rows) + +select count(*), sum(v) from (select gist_ios_restricted(i) as v from gist_ios_ppart) s; + count | sum +-------+---------- + 10000 | 50005000 +(1 row) + +drop table gist_ios_ppart; +reset enable_seqscan; +reset enable_bitmapscan; +reset parallel_setup_cost; +reset parallel_tuple_cost; +reset min_parallel_index_scan_size; +reset max_parallel_workers_per_gather; +reset parallel_leader_participation; +drop table gist_ios_par; +drop function gist_ios_restricted(int); +-- A partitioned table whose partitions all have the index: each child reads +-- the value and the Append passes it up, even when a partition's columns +-- are in a different order. +create temp table gist_ios_part (i int, pad text) partition by range (i); +create temp table gist_ios_part1 (pad text, i int); +alter table gist_ios_part attach partition gist_ios_part1 + for values from (1) to (5001); +create temp table gist_ios_part2 partition of gist_ios_part + for values from (5001) to (10001); +insert into gist_ios_part select g, 'p' from generate_series(1, 10000) g; +create index on gist_ios_part (i, gist_ios_costly(i)); +vacuum analyze gist_ios_part; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(p.i), d.tag from gist_ios_part p + join gist_ios_dim d on d.i = p.i; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Hash Join + Output: (gist_ios_costly(p.i)), d.tag + Hash Cond: (p.i = d.i) + -> Append + -> Index Only Scan using gist_ios_part1_i_gist_ios_costly_i_idx on pg_temp.gist_ios_part1 p_1 + Output: p_1.i, (gist_ios_costly(p_1.i)) + -> Index Only Scan using gist_ios_part2_i_gist_ios_costly_i_idx on pg_temp.gist_ios_part2 p_2 + Output: p_2.i, (gist_ios_costly(p_2.i)) + -> Hash + Output: d.tag, d.i + -> Seq Scan on pg_temp.gist_ios_dim d + Output: d.tag, d.i +(12 rows) + +reset enable_mergejoin; +reset enable_nestloop; +-- An ordered Append passes the value up as well, here into a merge join. +set enable_hashjoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(p.i), d.tag from gist_ios_part p + join gist_ios_dim d on d.i = p.i order by p.i; + QUERY PLAN +-------------------------------------------------------------------------------------------------------- + Merge Join + Output: (gist_ios_costly(p.i)), d.tag, p.i + Merge Cond: (p.i = d.i) + -> Append + -> Index Only Scan using gist_ios_part1_i_gist_ios_costly_i_idx on pg_temp.gist_ios_part1 p_1 + Output: p_1.i, (gist_ios_costly(p_1.i)) + -> Index Only Scan using gist_ios_part2_i_gist_ios_costly_i_idx on pg_temp.gist_ios_part2 p_2 + Output: p_2.i, (gist_ios_costly(p_2.i)) + -> Index Scan using gist_ios_dim_i_idx on pg_temp.gist_ios_dim d + Output: d.i, d.tag +(10 rows) + +reset enable_hashjoin; +reset enable_nestloop; +-- An index created on only one partition doesn't count: every child must +-- emit the value for the Append to. +create temp table gist_ios_part_x (i int) partition by range (i); +create temp table gist_ios_part_x1 partition of gist_ios_part_x + for values from (1) to (5001); +create temp table gist_ios_part_x2 partition of gist_ios_part_x + for values from (5001) to (10001); +insert into gist_ios_part_x select g from generate_series(1, 10000) g; +create index on gist_ios_part_x1 (i, gist_ios_costly(i)); +vacuum analyze gist_ios_part_x; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(p.i), d.tag from gist_ios_part_x p + join gist_ios_dim d on d.i = p.i; + QUERY PLAN +------------------------------------------------------ + Hash Join + Output: gist_ios_costly(p.i), d.tag + Hash Cond: (p.i = d.i) + -> Append + -> Seq Scan on pg_temp.gist_ios_part_x1 p_1 + Output: p_1.i + -> Seq Scan on pg_temp.gist_ios_part_x2 p_2 + Output: p_2.i + -> Hash + Output: d.tag, d.i + -> Seq Scan on pg_temp.gist_ios_dim d + Output: d.tag, d.i +(12 rows) + +reset enable_mergejoin; +reset enable_nestloop; +-- Each of these returns what the same query does without index-only scans. +create temp table gist_ios_check as +select gist_ios_costly(a.i) as e, d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i; +set enable_indexonlyscan = off; +select count(*) as mismatches from ( + (select gist_ios_costly(a.i) as e, d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i + except all select * from gist_ios_check) + union all + (select * from gist_ios_check + except all select gist_ios_costly(a.i), d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i)) s; + mismatches +------------ + 0 +(1 row) + +reset enable_indexonlyscan; +drop table gist_ios_check, gist_ios_part, gist_ios_part_x, gist_ios_dim, + gist_ios_dim2; +drop function gist_ios_total(text); +drop table gist_ios_cost; +drop function gist_ios_costly(int); +-- (The tests above reset these to their defaults; the rest of this file +-- wants them as set at the top.) +set enable_seqscan = off; +set enable_bitmapscan = off; -- Test that an index-only scan deforms the tuple it reconstructs with the -- descriptor the AM formed it with, not the scan slot's descriptor. create temp table gist_ios_tupdesc (a inet, r numrange); diff --git a/src/test/regress/pg_regress.c b/src/test/regress/pg_regress.c index 13944701bc7b0..278cbfb735ce0 100644 --- a/src/test/regress/pg_regress.c +++ b/src/test/regress/pg_regress.c @@ -1243,7 +1243,7 @@ spawn_process(const char *cmdline) char *cmdline2; cmdline2 = psprintf("exec %s", cmdline); - execl(shellprog, shellprog, "-c", cmdline2, (char *) NULL); + execlp(shellprog, shellprog, "-c", cmdline2, (char *) NULL); /* Not using the normal bail() here as we want _exit */ bail_noatexit("could not exec \"%s\": %m", shellprog); } diff --git a/src/test/regress/sql/gist.sql b/src/test/regress/sql/gist.sql index 57dcc08245037..9c62f38ddce8c 100644 --- a/src/test/regress/sql/gist.sql +++ b/src/test/regress/sql/gist.sql @@ -169,6 +169,512 @@ explain (verbose, costs off) select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; select p from gist_tbl order by circle(p,1) <-> point(0,0) limit 1; +drop index gist_tbl_multi_index; + +-- Test that an ordering Index Scan returns the AM's ORDER BY values to a +-- targetlist entry equal to the ORDER BY expression, instead of evaluating +-- the expression again, for an opclass that passes amcanreturnorderby. +set enable_indexonlyscan = off; + +-- An expression index over a counting function proves the operator is not +-- re-evaluated: the count stays at zero for the rows returned. +create sequence gist_cnt_seq; +create function gist_cnt_pt(point) returns point language plpgsql immutable + as $$ begin perform nextval('public.gist_cnt_seq'); return $1; end $$; +create index gist_tbl_cnt_index on gist_tbl using gist (gist_cnt_pt(p)); + +explain (verbose, costs off) +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; + +select setval('gist_cnt_seq', 1, false); +-- the distance numbers are not exactly the same across platforms +set extra_float_digits = 0; +select p, gist_cnt_pt(p) <-> point(0.201, 0.201) as dist + from gist_tbl order by gist_cnt_pt(p) <-> point(0.201, 0.201) limit 3; +reset extra_float_digits; +select nextval('gist_cnt_seq') - 1 as calls_during_scan; + +-- Every copy of the ORDER BY expression is served from the index; an +-- expression that merely contains it is not. +explain (verbose, costs off) +select gist_cnt_pt(p) <-> point(0,0) as d1, + (gist_cnt_pt(p) <-> point(0,0)) * 2 as d2, + gist_cnt_pt(p) <-> point(0,0) as d3 + from gist_tbl order by gist_cnt_pt(p) <-> point(0,0) limit 2; + +drop index gist_tbl_cnt_index; +drop function gist_cnt_pt(point); +drop sequence gist_cnt_seq; + +-- The values must equal the operator's, also for a row re-fetched under +-- row locking, and for a rescan with a new ORDER BY argument (LATERAL). +create index gist_tbl_point_index on gist_tbl using gist (p); + +select count(*) filter (where dist = p <-> point(5.2, 5.91)) as same, + count(*) as total + from (select p, p <-> point(5.2, 5.91) as dist from gist_tbl + order by p <-> point(5.2, 5.91) limit 200) ss; + +set extra_float_digits = 0; +select p, p <-> point(5.2, 5.91) as dist from gist_tbl + where p <@ box(point(5,5), point(6,6)) order by p <-> point(5.2, 5.91) + limit 3 for update; +reset extra_float_digits; + +explain (verbose, costs off) +select v.q, n.dist + from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q), + lateral (select p <-> v.q as dist from gist_tbl + order by p <-> v.q limit 2) n; +select count(*) filter (where n.dist = n.p <-> v.q) as same, count(*) as total + from (values (point(0.3, 0.3)), (point(7.1, 7.2)), (point(42, 42))) v(q), + lateral (select p, p <-> v.q as dist from gist_tbl + order by p <-> v.q limit 50) n; + +-- A multi-column index ordered by an exact key and a lossy one: the lossy +-- one's distances are rechecked, rows pass through the reorder queue, and +-- the exact key's value is still the operator's. +create index gist_tbl_pc_index on gist_tbl using gist (p, c); +explain (verbose, costs off) +select p <-> point(1, 1) as dp, c <-> point(2, 2) as dc from gist_tbl + order by p <-> point(1, 1), c <-> point(2, 2) limit 5; +select count(*) filter (where dp = p <-> point(1, 1)) as same_p, + count(*) filter (where dc = c <-> point(2, 2)) as same_c, + count(*) as total + from (select p, c, p <-> point(1, 1) as dp, c <-> point(2, 2) as dc + from gist_tbl + order by p <-> point(1, 1), c <-> point(2, 2) limit 200) ss; +drop index gist_tbl_pc_index; +drop index gist_tbl_point_index; + +-- The box opclass computes a leaf's distance exactly, but the operator treats +-- a point within EPSILON of an edge's line as on it, so the two can differ in +-- the last digits; it doesn't qualify, and the operator is evaluated. +create index gist_tbl_box_index on gist_tbl using gist (b); + +explain (verbose, costs off) +select b, b <-> point(5.2, 5.91) as dist from gist_tbl + where b <@ box(point(5,5), point(6,6)) order by b <-> point(5.2, 5.91); + +-- For this point, the index's distance is 1 and the operator's is +-- 1.00000000000008: dist is the operator's only if the scan doesn't hand +-- back its own. +create temp table gist_box_eps (b box); +insert into gist_box_eps values ('((0,0),(1,1))'); +create index on gist_box_eps using gist (b); +select count(*) filter (where dist = b <-> point(0.9999996, 2)) as same, + count(*) as total + from (select b, b <-> point(0.9999996, 2) as dist from gist_box_eps + order by b <-> point(0.9999996, 2) limit 1) ss; +drop table gist_box_eps; + +drop index gist_tbl_box_index; + +-- The circle opclass's distance is a lower bound that is always rechecked, +-- and its opclass does not pass amcanreturnorderby: the targetlist is left +-- alone and the operator is evaluated as before. +create index gist_tbl_circle_index on gist_tbl using gist (c); + +explain (verbose, costs off) +select c <-> point(5.2, 5.91) as dist from gist_tbl + order by c <-> point(5.2, 5.91) limit 3; + +select count(*) filter (where dist = c <-> point(5.2, 5.91)) as same, + count(*) as total + from (select c, c <-> point(5.2, 5.91) as dist from gist_tbl + order by c <-> point(5.2, 5.91) limit 200) ss; + +drop index gist_tbl_circle_index; +reset enable_indexonlyscan; + +-- SP-GiST's quad and k-d point opclasses qualify as well; its box opclass +-- doesn't, for the same reason as GiST's. +create temp table spg_orderby (p point, b box); +insert into spg_orderby + select point(i % 97 * 0.31, i / 97 * 0.29), + box(point(i % 97 * 0.31, i / 97 * 0.29), + point(i % 97 * 0.31 + 0.2, i / 97 * 0.29 + 0.1)) + from generate_series(1, 5000) i; +create index spg_orderby_quad on spg_orderby using spgist (p); +set enable_indexonlyscan = off; +explain (verbose, costs off) +select p <-> point(3, 4) as dist from spg_orderby + order by p <-> point(3, 4) limit 3; +select count(*) filter (where dist = p <-> point(3, 4)) as same, + count(*) as total + from (select p, p <-> point(3, 4) as dist from spg_orderby + order by p <-> point(3, 4) limit 300) ss; +drop index spg_orderby_quad; +create index spg_orderby_kd on spg_orderby using spgist (p kd_point_ops); +select count(*) filter (where dist = p <-> point(3, 4)) as same, + count(*) as total + from (select p, p <-> point(3, 4) as dist from spg_orderby + order by p <-> point(3, 4) limit 300) ss; +drop index spg_orderby_kd; +create index spg_orderby_box on spg_orderby using spgist (b); +explain (verbose, costs off) +select b <-> point(3, 4) as dist from spg_orderby + order by b <-> point(3, 4) limit 3; +reset enable_indexonlyscan; +drop table spg_orderby; + +-- The planner must not charge an ordering Index Scan for evaluating a +-- targetlist entry it takes from its ORDER BY values. With a costly ORDER BY +-- expression, the kNN scan should win over Seq Scan + Sort even when the +-- filter is selective, since the Sort plan really does evaluate it per row. +create function gist_costly_pt(point) returns point + language plpgsql immutable strict cost 10000 + as $$ begin return $1; end $$; +create temp table gist_costly (id int, p point, grp int); +insert into gist_costly + select g, point(g % 101, g % 103), g % 100 from generate_series(1, 10000) g; +create index on gist_costly using gist (gist_costly_pt(p)); +create index on gist_costly (grp); +vacuum analyze gist_costly; +set enable_bitmapscan = off; +-- (Seq Scan + Sort is a real choice here; the earlier tests turned it off.) +set enable_seqscan = on; + +explain (costs off) +select id, gist_costly_pt(p) <-> point(0,0) as dist from gist_costly + where grp < 5 order by gist_costly_pt(p) <-> point(0,0); +set enable_seqscan = off; + +-- The ORDER BY value is in every plan's target (as a sort column), so the +-- kNN scan used to be charged rows * cost(gist_costly_pt) for it, ~2.5e5 +-- here. It is now charged nothing for it; an expression that merely +-- contains it is still evaluated, and charged. +create function gist_costly_total(q text) returns float8 language plpgsql as +$$ declare j json; begin + execute 'explain (format json) ' || q into j; + return (j->0->'Plan'->>'Total Cost')::float8; + end $$; +set enable_sort = off; +select gist_costly_total('select id, gist_costly_pt(p) <-> point(0,0) + from gist_costly order by gist_costly_pt(p) <-> point(0,0)') < 10000 + as returned_is_free, + gist_costly_total('select id, (gist_costly_pt(p) <-> point(0,0)) * 2 + from gist_costly order by gist_costly_pt(p) <-> point(0,0)') > 100000 + as nested_is_charged; +reset enable_sort; +drop function gist_costly_total(text); + +reset enable_bitmapscan; +drop table gist_costly; +drop function gist_costly_pt(point); + +-- The ORDER BY expression written as "const OP key" is commuted into +-- "key OP const" for the index; a targetlist entry in the original form is +-- still the same value, and is still taken from the scan. +create temp table gist_commute as + select g as id, point(g % 101, g % 103) as p from generate_series(1, 1000) g; +create index on gist_commute using gist (p); +vacuum analyze gist_commute; +set enable_seqscan = off; +set enable_bitmapscan = off; +set enable_indexonlyscan = off; +explain (verbose, costs off) +select point(5,5) <-> p as d from gist_commute order by point(5,5) <-> p limit 3; +set extra_float_digits = 0; +select point(5,5) <-> p as d from gist_commute order by point(5,5) <-> p limit 3; +reset extra_float_digits; +reset enable_seqscan; +reset enable_bitmapscan; +reset enable_indexonlyscan; +drop table gist_commute; + +-- An ordering Index Scan below a join emits the ORDER BY value it returns, +-- so the join above reads it instead of evaluating the expression again. +-- The counting function shows the expression is evaluated zero times. +create sequence gist_join_cnt; +create function gist_join_pt(point) returns point + language plpgsql immutable strict cost 1000 + as $$ begin perform nextval('public.gist_join_cnt'); return $1; end $$; +create temp table gist_join_fact as + select g as id, point(g % 101, g % 103) as p from generate_series(1, 1000) g; +create index on gist_join_fact using gist (gist_join_pt(p)); +create temp table gist_join_dim as select g as id from generate_series(1, 1000, 2) g; +create index on gist_join_dim (id); +vacuum analyze gist_join_fact; +vacuum analyze gist_join_dim; +set enable_seqscan = off; +set enable_bitmapscan = off; +set enable_indexonlyscan = off; +set enable_hashjoin = off; +set enable_mergejoin = off; +set enable_sort = off; +explain (verbose, costs off) +select f.id, gist_join_pt(f.p) <-> point(5,5) as d + from gist_join_fact f join gist_join_dim j on j.id = f.id + order by gist_join_pt(f.p) <-> point(5,5) limit 3; +select setval('gist_join_cnt', 1, false); +set extra_float_digits = 0; +select f.id, gist_join_pt(f.p) <-> point(5,5) as d + from gist_join_fact f join gist_join_dim j on j.id = f.id + order by gist_join_pt(f.p) <-> point(5,5) limit 3; +reset extra_float_digits; +select nextval('gist_join_cnt') - 1 as calls_during_join; +-- Nothing extra is emitted when the query doesn't need the value above the +-- scan. +explain (verbose, costs off) +select f.id from gist_join_fact f join gist_join_dim j on j.id = f.id + order by gist_join_pt(f.p) <-> point(5,5) limit 3; +-- Nor for a cheap ORDER BY expression: the plain point distance is computed +-- again above the join, which costs less than carrying it up. +create index on gist_join_fact using gist (p); +explain (verbose, costs off) +select f.id, f.p <-> point(5,5) as d + from gist_join_fact f join gist_join_dim j on j.id = f.id + order by f.p <-> point(5,5) limit 3; +reset enable_seqscan; +reset enable_bitmapscan; +reset enable_indexonlyscan; +reset enable_hashjoin; +reset enable_mergejoin; +reset enable_sort; +drop table gist_join_fact; +drop table gist_join_dim; +drop function gist_join_pt(point); +drop sequence gist_join_cnt; + +-- An index-only scan isn't charged for evaluating a target expression it +-- reads from a returnable index expression column: it reads the stored +-- value instead. (Not GiST-specific; btree returns expression columns.) +create function gist_ios_costly(int) returns int + language plpgsql immutable strict cost 100000 + as $$ begin return $1; end $$; +create temp table gist_ios_cost (i int); +insert into gist_ios_cost select g from generate_series(1, 10000) g; +create index on gist_ios_cost (i, gist_ios_costly(i)); +vacuum analyze gist_ios_cost; +create function gist_ios_total(q text) returns float8 language plpgsql as +$$ declare j json; begin + execute 'explain (format json) ' || q into j; + return (j->0->'Plan'->>'Total Cost')::float8; + end $$; +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost order by i; +select gist_ios_total('select gist_ios_costly(i) from gist_ios_cost order by i') + < 10000 as ios_expr_is_free; +-- With nothing else to recommend it (no qual, no useful order), an +-- index-only scan that reads the expression still beats a seq scan that has +-- to compute it: add_path() keeps the path that emits the value. +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost order by gist_ios_costly(i); +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost; +-- Ordered by the index, the scan emits the final targetlist directly and +-- still reads the expression from the index. +explain (verbose, costs off) +select gist_ios_costly(i) from gist_ios_cost order by i desc limit 3; +-- The expression may be part of a larger one, or appear in a qual. +explain (verbose, costs off) +select gist_ios_costly(i) + 1 from gist_ios_cost + where gist_ios_costly(i) % 3 = 0; +-- Below a join, the scan emits it and the join passes it up, through more +-- than one join level. +create temp table gist_ios_dim (i int, tag text); +insert into gist_ios_dim select g, 't' || g from generate_series(1, 10000, 7) g; +create index on gist_ios_dim (i); +create temp table gist_ios_dim2 (k int, i int); +insert into gist_ios_dim2 select g, g * 3 from generate_series(1, 3000) g; +vacuum analyze gist_ios_dim, gist_ios_dim2; +-- (Each join method is pinned; it's the value passing up that's tested.) +set enable_hashjoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(a.i), d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i; +reset enable_nestloop; +set enable_mergejoin = off; +explain (verbose, costs off) +select gist_ios_costly(a.i), d.tag, n.k from gist_ios_cost a + join gist_ios_dim d on d.i = a.i join gist_ios_dim2 n on n.i = a.i; +reset enable_hashjoin; +reset enable_mergejoin; +-- When everything the scan emits is an index column, it returns the +-- index's own tlist without projecting (here including k, which nothing +-- needs); the value is still read above the join. +create temp table gist_ios_wide (k int, i int); +insert into gist_ios_wide select g, g from generate_series(1, 10000) g; +create index on gist_ios_wide (k, gist_ios_costly(i)) include (i); +vacuum analyze gist_ios_wide; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(w.i), d.tag from gist_ios_wide w + join gist_ios_dim d on d.i = w.i; +reset enable_mergejoin; +reset enable_nestloop; +drop table gist_ios_wide; +-- Also from the nullable side of an outer join, because the function is +-- strict: a null-extended row gets NULL for the value, which is what the +-- function returns for a NULL argument. The value is used inside a larger, +-- non-strict expression above the join. +set enable_hashjoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select coalesce(gist_ios_costly(a.i), -1) from gist_ios_dim d + left join gist_ios_cost a on a.i = d.i * 2; +reset enable_hashjoin; +reset enable_nestloop; +-- And from both sides' nullable rows of a FULL JOIN. +set enable_hashjoin = off; +explain (verbose, costs off) +select gist_ios_costly(a.i), d.tag from gist_ios_dim d + full join gist_ios_cost a on a.i = d.i * 2; +reset enable_hashjoin; +select count(*) as total, count(v) as with_value, + count(*) filter (where v = i) as same + from (select gist_ios_costly(a.i) as v, a.i from gist_ios_dim d + full join gist_ios_cost a on a.i = d.i * 2) s; +-- A function that isn't strict may return something else for NULL, so it's +-- computed above the join. +create function gist_ios_nonstrict(int) returns int + language plpgsql immutable called on null input cost 100000 + as $$ begin return coalesce($1, -1); end $$; +create index on gist_ios_cost (i, gist_ios_nonstrict(i)); +vacuum analyze gist_ios_cost; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_nonstrict(a.i) from gist_ios_dim d + left join gist_ios_cost a on a.i = d.i * 2; +reset enable_mergejoin; +reset enable_nestloop; +select count(*) filter (where v = -1) as null_extended + from (select gist_ios_nonstrict(a.i) as v from gist_ios_dim d + left join gist_ios_cost a on a.i = d.i * 2) s; +drop function gist_ios_nonstrict(int) cascade; +-- A parallel-restricted function: the value is read from the index inside +-- the workers, not evaluated there, and passed up through the Gather. (Not +-- a temp table, which can't be scanned in parallel.) +create function gist_ios_restricted(int) returns int + language plpgsql immutable strict parallel restricted cost 100000 + as $$ begin return $1; end $$; +create table gist_ios_par (i int); +insert into gist_ios_par select g from generate_series(1, 10000) g; +create index on gist_ios_par (i, gist_ios_restricted(i)); +vacuum analyze gist_ios_par; +set parallel_setup_cost = 0; +set parallel_tuple_cost = 0; +set min_parallel_index_scan_size = 0; +set max_parallel_workers_per_gather = 2; +set parallel_leader_participation = off; +-- (Pin the scan type: these tables aren't temp, so VACUUM may not get to +-- mark them all-visible while other sessions hold snapshots.) +set enable_seqscan = off; +set enable_bitmapscan = off; +explain (verbose, costs off) +select gist_ios_restricted(i) from gist_ios_par; +select count(*), sum(v) from (select gist_ios_restricted(i) as v from gist_ios_par) s; +-- Also through a Gather Merge, which sorts the workers' rows by a column +-- the index doesn't lead with. +create table gist_ios_gm (k int, i int); +insert into gist_ios_gm select g % 97, g from generate_series(1, 20000) g; +create index on gist_ios_gm (i, gist_ios_restricted(i)) include (k); +vacuum analyze gist_ios_gm; +explain (verbose, costs off) +select k, gist_ios_restricted(i) from gist_ios_gm where i > 100 order by k; +select count(*) filter (where v = i) as same, count(*) as total + from (select k, i, gist_ios_restricted(i) as v from gist_ios_gm + where i > 100 order by k) s; +drop table gist_ios_gm; +-- And through a Parallel Append over partitions that all have the index. +-- (Different sizes, so that the children's cost order, which is the plan's +-- order, doesn't depend on how many pages VACUUM could mark all-visible +-- when other sessions hold snapshots.) +create table gist_ios_ppart (i int) partition by range (i); +create table gist_ios_ppart1 partition of gist_ios_ppart + for values from (1) to (2001); +create table gist_ios_ppart2 partition of gist_ios_ppart + for values from (2001) to (10001); +insert into gist_ios_ppart select g from generate_series(1, 10000) g; +create index on gist_ios_ppart (i, gist_ios_restricted(i)); +vacuum analyze gist_ios_ppart; +explain (verbose, costs off) +select gist_ios_restricted(i) from gist_ios_ppart; +select count(*), sum(v) from (select gist_ios_restricted(i) as v from gist_ios_ppart) s; +drop table gist_ios_ppart; +reset enable_seqscan; +reset enable_bitmapscan; +reset parallel_setup_cost; +reset parallel_tuple_cost; +reset min_parallel_index_scan_size; +reset max_parallel_workers_per_gather; +reset parallel_leader_participation; +drop table gist_ios_par; +drop function gist_ios_restricted(int); +-- A partitioned table whose partitions all have the index: each child reads +-- the value and the Append passes it up, even when a partition's columns +-- are in a different order. +create temp table gist_ios_part (i int, pad text) partition by range (i); +create temp table gist_ios_part1 (pad text, i int); +alter table gist_ios_part attach partition gist_ios_part1 + for values from (1) to (5001); +create temp table gist_ios_part2 partition of gist_ios_part + for values from (5001) to (10001); +insert into gist_ios_part select g, 'p' from generate_series(1, 10000) g; +create index on gist_ios_part (i, gist_ios_costly(i)); +vacuum analyze gist_ios_part; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(p.i), d.tag from gist_ios_part p + join gist_ios_dim d on d.i = p.i; +reset enable_mergejoin; +reset enable_nestloop; +-- An ordered Append passes the value up as well, here into a merge join. +set enable_hashjoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(p.i), d.tag from gist_ios_part p + join gist_ios_dim d on d.i = p.i order by p.i; +reset enable_hashjoin; +reset enable_nestloop; +-- An index created on only one partition doesn't count: every child must +-- emit the value for the Append to. +create temp table gist_ios_part_x (i int) partition by range (i); +create temp table gist_ios_part_x1 partition of gist_ios_part_x + for values from (1) to (5001); +create temp table gist_ios_part_x2 partition of gist_ios_part_x + for values from (5001) to (10001); +insert into gist_ios_part_x select g from generate_series(1, 10000) g; +create index on gist_ios_part_x1 (i, gist_ios_costly(i)); +vacuum analyze gist_ios_part_x; +set enable_mergejoin = off; +set enable_nestloop = off; +explain (verbose, costs off) +select gist_ios_costly(p.i), d.tag from gist_ios_part_x p + join gist_ios_dim d on d.i = p.i; +reset enable_mergejoin; +reset enable_nestloop; +-- Each of these returns what the same query does without index-only scans. +create temp table gist_ios_check as +select gist_ios_costly(a.i) as e, d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i; +set enable_indexonlyscan = off; +select count(*) as mismatches from ( + (select gist_ios_costly(a.i) as e, d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i + except all select * from gist_ios_check) + union all + (select * from gist_ios_check + except all select gist_ios_costly(a.i), d.tag from gist_ios_cost a + join gist_ios_dim d on d.i = a.i)) s; +reset enable_indexonlyscan; +drop table gist_ios_check, gist_ios_part, gist_ios_part_x, gist_ios_dim, + gist_ios_dim2; +drop function gist_ios_total(text); +drop table gist_ios_cost; +drop function gist_ios_costly(int); + +-- (The tests above reset these to their defaults; the rest of this file +-- wants them as set at the top.) +set enable_seqscan = off; +set enable_bitmapscan = off; + -- Test that an index-only scan deforms the tuple it reconstructs with the -- descriptor the AM formed it with, not the scan slot's descriptor. create temp table gist_ios_tupdesc (a inet, r numrange); diff --git a/src/tools/pgindent/pgindent b/src/tools/pgindent/pgindent index fa2041dc34b93..92526f6248581 100755 --- a/src/tools/pgindent/pgindent +++ b/src/tools/pgindent/pgindent @@ -1,4 +1,4 @@ -#!/usr/bin/perl +#!/usr/bin/env perl # Copyright (c) 2021-2026, PostgreSQL Global Development Group diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index a7a8dbdd23f79..702b19b97be1f 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -3567,6 +3567,7 @@ ambuildempty_function ambuildphasename_function ambulkdelete_function amcanreturn_function +amcanreturnorderby_function amcostestimate_function amendscan_function amestimateparallelscan_function @@ -3688,6 +3689,7 @@ count_param_references_context cp_hash_func create_upper_paths_hook_type createdb_failure_params +credit_free_context crosstab_HashEnt crosstab_cat_desc curl_infotype @@ -4221,6 +4223,7 @@ regexp regexp_matches_ctx registered_buffer regproc +relabel_context relopt_bool relopt_enum relopt_enum_elt_def